feat: 调整样式

This commit is contained in:
Blizzard
2026-02-14 11:31:44 +08:00
parent cbbe82ef63
commit d6f781a666
42 changed files with 827 additions and 293 deletions
+3 -1
View File
@@ -17,7 +17,9 @@
"pages/profile/badges/level-detail/index", "pages/profile/badges/level-detail/index",
"pages/profile/badges/badge-wall/index", "pages/profile/badges/badge-wall/index",
"pages/profile/favorites/index", "pages/profile/favorites/index",
"pages/profile/posts/index" "pages/profile/posts/index",
"pages/profile/about/index",
"pages/profile/exchange/index"
], ],
"window": { "window": {
"backgroundTextStyle": "light", "backgroundTextStyle": "light",
+38 -10
View File
@@ -13,16 +13,26 @@ Page({
showImageSheet: false, showImageSheet: false,
imageSheetItems: [ imageSheetItems: [
{ label: '拍照', value: 'camera' }, { label: '拍照', value: 'camera' },
{ label: '从相册选', value: 'album' } { label: '从相册选', value: 'album' }
] ],
topicColors: ['#558B2F', '#1976D2', '#7B1FA2', '#F57C00', '#C2185B', '#00796B'],
topicBgColors: ['#E8F5E9', '#E3F2FD', '#F3E5F5', '#FFF3E0', '#FCE4EC', '#E0F2F1']
}, },
onLoad() { onLoad() {
// No draft loading this.fetchTopics();
}, },
onUnload() { fetchTopics() {
// No draft saving request.get('/topic/list').then(res => {
const list = res.list || [];
const topics = list.map(t => t.title);
if (topics.length > 0) {
this.setData({ suggestedTopics: topics });
}
}).catch(err => {
console.error('Fetch topics failed', err);
});
}, },
onContentInput(e) { onContentInput(e) {
@@ -105,7 +115,6 @@ Page({
itemList: ['设为封面', '删除'], itemList: ['设为封面', '删除'],
success: (res) => { success: (res) => {
if (res.tapIndex === 0) { if (res.tapIndex === 0) {
// Move to first position
const images = [...this.data.images]; const images = [...this.data.images];
const [img] = images.splice(index, 1); const [img] = images.splice(index, 1);
images.unshift(img); images.unshift(img);
@@ -121,14 +130,33 @@ Page({
chooseLocation() { chooseLocation() {
wx.chooseLocation({ wx.chooseLocation({
success: (res) => { success: (res) => {
this.setData({ location: res.name || res.address }); const formatted = this.formatLocation(res.address, res.name);
this.setData({ location: formatted });
}, },
fail: () => { fail: () => { }
// User cancelled or no permission
}
}); });
}, },
formatLocation(address, name) {
if (!address) return name || '';
// Municipalities
const munis = ['北京', '上海', '天津', '重庆'];
for (let m of munis) {
if (address.startsWith(m)) {
return m;
}
}
// Standard: Prov + City (Simplify names)
const match = address.match(/^(.+?)(?:省|自治区)(.+?)(?:市|自治州|地区|盟)/);
if (match) {
return `${match[1]}.${match[2]}`;
}
return name || address;
},
toggleTopic(e) { toggleTopic(e) {
const topic = e.currentTarget.dataset.topic; const topic = e.currentTarget.dataset.topic;
const hashtag = `#${topic} `; const hashtag = `#${topic} `;
+1
View File
@@ -78,6 +78,7 @@
</view> </view>
<view class="topic-list"> <view class="topic-list">
<view class="topic-tag {{selectedTopics.includes(item) ? 'selected' : ''}}" <view class="topic-tag {{selectedTopics.includes(item) ? 'selected' : ''}}"
style="{{selectedTopics.includes(item) ? 'color:' + topicColors[index % topicColors.length] + '; background-color:' + topicBgColors[index % topicBgColors.length] : ''}}"
wx:for="{{suggestedTopics}}" wx:for="{{suggestedTopics}}"
wx:key="*this" wx:key="*this"
bindtap="toggleTopic" bindtap="toggleTopic"
+1
View File
@@ -87,6 +87,7 @@ Page({
avatar: avatarObj.url, avatar: avatarObj.url,
content: item.content, content: item.content,
images: (item.imgList || []).map(img => img.url), images: (item.imgList || []).map(img => img.url),
location: item.location || '',
time: item.createdAtStr || '刚刚', time: item.createdAtStr || '刚刚',
likes: item.hasLiked === 1 ? ['我'] : [], likes: item.hasLiked === 1 ? ['我'] : [],
comments: (item.commentList || []).map(c => ({ comments: (item.commentList || []).map(c => ({
+5
View File
@@ -45,6 +45,11 @@
</view> </view>
</view> </view>
<!-- Location -->
<view wx:if="{{item.location}}" class="post-location">
<text>{{item.location}}</text>
</view>
<!-- Meta: Time + Action --> <!-- Meta: Time + Action -->
<view class="post-meta"> <view class="post-meta">
<text class="post-time">{{item.time}}</text> <text class="post-time">{{item.time}}</text>
+7
View File
@@ -165,6 +165,13 @@ page {
aspect-ratio: 1; aspect-ratio: 1;
} }
/* Location */
.post-location {
font-size: 24rpx;
color: #576b95;
margin-bottom: 12rpx;
}
/* Post Meta */ /* Post Meta */
.post-meta { .post-meta {
display: flex; display: flex;
+2 -2
View File
@@ -23,8 +23,8 @@ Page({
showActionSheet: false, showActionSheet: false,
actionSheetItems: [ actionSheetItems: [
{ label: '拍', value: 'camera' }, { label: '拍', value: 'camera' },
{ label: '从手机相册选取', value: 'album' } { label: '从相册选取', value: 'album' }
], ],
// Icon picker // Icon picker
+2 -11
View File
@@ -1,5 +1,6 @@
// pages/garden/index.js // pages/garden/index.js
import request from '../../utils/request'; import request from '../../utils/request';
import { calculateDaysSince } from '../../utils/dateUtil';
Page({ Page({
data: { data: {
@@ -71,17 +72,7 @@ Page({
} }
// Calculate days // Calculate days
let days = 1; const days = calculateDaysSince(item.plantTime);
if (item.plantTime) {
try {
const start = new Date(item.plantTime).getTime();
const now = Date.now();
const diff = now - start;
if (diff > 0) {
days = Math.ceil(diff / (1000 * 60 * 60 * 24));
}
} catch (e) { }
}
return { ...item, images: [imageUrl], daysPlanted: days }; return { ...item, images: [imageUrl], daysPlanted: days };
}); });
+2 -2
View File
@@ -22,8 +22,8 @@ Page({
showActionSheet: false, showActionSheet: false,
actionSheetItems: [ actionSheetItems: [
{ label: '拍', value: 'camera' }, { label: '拍', value: 'camera' },
{ label: '从手机相册选取', value: 'album' } { label: '从相册选取', value: 'album' }
], ],
careTaskIcons: [], careTaskIcons: [],
+4 -7
View File
@@ -1,5 +1,6 @@
// pages/plant-detail/index.js // pages/plant-detail/index.js
import request from '../../utils/request'; import request from '../../utils/request';
import { calculateDaysSince, getPlantAgeBadge } from '../../utils/dateUtil';
Page({ Page({
data: { data: {
@@ -53,14 +54,9 @@ Page({
// Calculate days planted and format date // Calculate days planted and format date
let adoptionDate = '未知'; let adoptionDate = '未知';
let daysPlanted = 0; const daysPlanted = calculateDaysSince(plant.plantTime);
const ageBadge = getPlantAgeBadge(daysPlanted);
if (plant.plantTime) { if (plant.plantTime) {
const start = new Date(plant.plantTime);
const now = new Date();
const diffTime = now - start;
if (diffTime > 0) {
daysPlanted = Math.floor(diffTime / (1000 * 60 * 60 * 24));
}
adoptionDate = plant.plantTime.split('T')[0]; adoptionDate = plant.plantTime.split('T')[0];
} }
@@ -70,6 +66,7 @@ Page({
location: plant.placement || '', location: plant.placement || '',
adoptionDate: adoptionDate, adoptionDate: adoptionDate,
daysPlanted: daysPlanted, daysPlanted: daysPlanted,
ageBadge: ageBadge,
careSchedule: carePlans careSchedule: carePlans
}, },
swiperImages: swiperImages, swiperImages: swiperImages,
+4 -1
View File
@@ -54,12 +54,15 @@
<view class="archive-view fadeIn"> <view class="archive-view fadeIn">
<view class="archive-identity-card"> <view class="archive-identity-card">
<view class="aic-header"> <view class="aic-header">
<view class="aic-badge">🏆 元老级植物</view> <view class="aic-badge">{{currentPlant.ageBadge.icon || '🌱'}} {{currentPlant.ageBadge.title || '植物新人'}}</view>
<view class="aic-location"> <view class="aic-location">
<t-icon name="location" size="28rpx" color="#388E3C" /> <t-icon name="location" size="28rpx" color="#388E3C" />
<text>{{currentPlant.location || '位置未定'}}</text> <text>{{currentPlant.location || '位置未定'}}</text>
</view> </view>
</view> </view>
<view class="aic-desc-box">
<text>{{currentPlant.ageBadge.desc}}</text>
</view>
<view class="aic-stats"> <view class="aic-stats">
<view class="aic-stat-item"> <view class="aic-stat-item">
<text class="label">入家时间</text> <text class="label">入家时间</text>
+11
View File
@@ -343,6 +343,17 @@ page {
gap: 8rpx; gap: 8rpx;
} }
.aic-desc-box {
background: #F1F8E9;
padding: 24rpx;
border-radius: 24rpx;
font-size: 26rpx;
color: #33691E;
line-height: 1.5;
text-align: left;
margin-bottom: 32rpx;
}
.aic-stats { .aic-stats {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
+24
View File
@@ -0,0 +1,24 @@
Page({
data: {
appVersion: '1.0.0'
},
onLoad() {
const accountInfo = wx.getAccountInfoSync();
if (accountInfo && accountInfo.miniProgram) {
this.setData({
appVersion: accountInfo.miniProgram.version || '1.0.0'
});
}
},
openDoc(e) {
if (wx.openPrivacyContract) {
wx.openPrivacyContract({
fail: () => {
wx.showToast({ title: '无法打开协议', icon: 'none' });
}
});
} else {
wx.showToast({ title: '当前微信版本不支持查看', icon: 'none' });
}
}
});
+8
View File
@@ -0,0 +1,8 @@
{
"navigationBarTitleText": "关于植趣",
"usingComponents": {
"t-icon": "tdesign-miniprogram/icon/icon",
"t-cell": "tdesign-miniprogram/cell/cell",
"t-cell-group": "tdesign-miniprogram/cell-group/cell-group"
}
}
+25
View File
@@ -0,0 +1,25 @@
<view class="about-container">
<view class="logo-section">
<view class="logo-bg">
<t-icon name="flower" size="80rpx" color="#558B2F" />
</view>
<text class="app-title">植趣</text>
<text class="app-version">Version {{appVersion}}</text>
</view>
<view class="desc-content">
一款专注于家庭植物养护的小程序。帮助你记录植物成长、制定养护计划、识别未知植物,与花友们分享养花心得。
</view>
<view class="menu-list">
<t-cell-group theme="card">
<t-cell title="用户协议" arrow hover bind:click="openDoc" data-type="terms" left-icon="file-copy" />
</t-cell-group>
</view>
<view class="footer-spacer"></view>
<view class="footer">
<text class="copyright">© 2026 Sundynix · All Rights Reserved</text>
</view>
</view>
+69
View File
@@ -0,0 +1,69 @@
page {
background: #F4F6F0;
}
.about-container {
display: flex;
flex-direction: column;
align-items: center;
padding: 48rpx 32rpx;
min-height: 100vh;
box-sizing: border-box;
}
.logo-section {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 64rpx;
margin-bottom: 48rpx;
}
.logo-bg {
width: 160rpx;
height: 160rpx;
background: #fff;
border-radius: 32rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 32rpx rgba(85, 139, 47, 0.1);
margin-bottom: 24rpx;
}
.app-title {
font-size: 40rpx;
font-weight: 600;
color: #333;
margin-bottom: 8rpx;
}
.app-version {
font-size: 24rpx;
color: #999;
}
.desc-content {
font-size: 28rpx;
color: #666;
line-height: 1.6;
text-align: center;
margin-bottom: 64rpx;
padding: 0 40rpx;
}
.menu-list {
width: 100%;
}
.footer-spacer {
flex: 1;
}
.footer {
padding: 32rpx 0;
}
.copyright {
font-size: 22rpx;
color: #ccc;
}
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"navigationBarTitleText": "成就徽章", "navigationBarTitleText": "等级徽章",
"navigationBarBackgroundColor": "#F4F6F0", "navigationBarBackgroundColor": "#F4F6F0",
"navigationBarTextStyle": "black", "navigationBarTextStyle": "black",
"usingComponents": { "usingComponents": {
+3 -3
View File
@@ -24,7 +24,7 @@
<view class="click-hint">点击查看等级详情 ></view> <view class="click-hint">点击查看等级详情 ></view>
</view> </view>
<view class="section-title-badges">我的成就</view> <view class="section-title-badges">我的徽章</view>
<view class="badge-wall-entry" bindtap="openBadgeWall"> <view class="badge-wall-entry" bindtap="openBadgeWall">
<view class="entry-bg-bloom"></view> <view class="entry-bg-bloom"></view>
@@ -33,8 +33,8 @@
<t-icon name="achievement" size="56rpx" color="#FFD700" /> <t-icon name="achievement" size="56rpx" color="#FFD700" />
</view> </view>
<view class="wall-entry-text"> <view class="wall-entry-text">
<text class="entry-title">成就徽章墙</text> <text class="entry-title">徽章墙</text>
<text class="entry-desc">查看所有成就与收集进度</text> <text class="entry-desc">查看所有徽章与收集进度</text>
</view> </view>
</view> </view>
<view class="wall-entry-right"> <view class="wall-entry-right">
+1
View File
@@ -0,0 +1 @@
Page({});
+6
View File
@@ -0,0 +1,6 @@
{
"navigationBarTitleText": "兑换中心",
"usingComponents": {
"t-empty": "tdesign-miniprogram/empty/empty"
}
}
+3
View File
@@ -0,0 +1,3 @@
<view class="exchange-page">
<t-empty icon="info-circle-filled" description="兑换中心功能正在开发中,敬请期待" />
</view>
+9
View File
@@ -0,0 +1,9 @@
page {
background: #F4F6F0;
}
.exchange-page {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
}
+128 -15
View File
@@ -2,33 +2,146 @@ import request from '../../../utils/request';
Page({ Page({
data: { data: {
favTab: 'all', favTab: 'all', // 'all', 'plant', 'post'
favorites: [], favorites: [],
filteredFavorites: [] current: 1,
pageSize: 10,
hasMore: true,
isLoading: false
}, },
onLoad() { onLoad() {
this.loadFavorites(); this.fetchFavorites(true);
},
loadFavorites() {
// TODO: Call API
this.filterFavorites();
}, },
onFavTabChange(e) { onFavTabChange(e) {
const val = e.currentTarget.dataset.value; const val = e.currentTarget.dataset.value;
if (val === this.data.favTab) return;
this.setData({ favTab: val }, () => { this.setData({ favTab: val }, () => {
this.filterFavorites(); this.fetchFavorites(true);
}); });
}, },
filterFavorites() { fetchFavorites(reset = false) {
const { favorites, favTab } = this.data; if (this.data.isLoading) return;
const filtered = favorites.filter(item => { if (!reset && !this.data.hasMore) return;
if (favTab === 'all') return true;
return item.type === favTab; this.setData({ isLoading: true });
const current = reset ? 1 : this.data.current;
let classType = 0;
if (this.data.favTab === 'plant') classType = 1;
if (this.data.favTab === 'post') classType = 2;
request.post('/profile/star', {
current,
pageSize: this.data.pageSize,
class: classType
}).then(res => {
const list = res.list || [];
const total = res.total || 0;
const mappedList = list.map(wrapper => {
const type = wrapper.type; // 1=Wiki, 2=Post
const isPlant = type === 1;
const entity = isPlant ? wrapper.wiki : wrapper.post;
if (!entity) return null;
// Image Extraction
let image = '';
if (entity.imgList && entity.imgList.length > 0) {
image = entity.imgList[0].url;
}
if (isPlant) {
return {
id: entity.id,
type: 'plant',
name: entity.name,
latinName: entity.latinName,
difficultyLabel: this.getDifficultyLabel(entity.difficulty),
image: image
};
} else {
// Post
return {
id: entity.id,
type: 'post',
content: entity.content,
postImage: image,
avatar: entity.publisher && entity.publisher.avatar ? entity.publisher.avatar : '', // Publisher is null in sample
nickname: entity.publisher && entity.publisher.nickname ? entity.publisher.nickname : '花友',
time: wrapper.createdAtStr
};
}
}).filter(item => item !== null);
if (reset) {
this.setData({
favorites: mappedList,
current: 2,
hasMore: mappedList.length < total,
isLoading: false
}); });
this.setData({ filteredFavorites: filtered }); } else {
this.setData({
favorites: [...this.data.favorites, ...mappedList],
current: current + 1,
hasMore: (this.data.favorites.length + mappedList.length) < total,
isLoading: false
});
}
}).catch(err => {
console.error('Fetch favorites failed', err);
this.setData({ isLoading: false });
});
},
getDifficultyLabel(level) {
const labels = { 1: '简单', 2: '中等', 3: '较难', 4: '困难', 5: '专家' };
return labels[level] || '未知';
},
onSwipeClick(e) {
// Handle Delete
// Data item is bound to the swipe-cell or passed via dataset
// If bind:click is on swipe-cell, e.target might not have the item if not set.
// We set data-item on swipe-cell.
const item = e.currentTarget.dataset.item;
const { id, type } = item;
const apiPath = type === 'plant' ? '/wiki/star' : '/post/star';
wx.showModal({
title: '提示',
content: '确定要取消收藏吗?',
success: (modRes) => {
if (modRes.confirm) {
request.get(apiPath, { id, type: 2 }).then(() => {
wx.showToast({ title: '已删除', icon: 'success' });
// Remove from list
const newList = this.data.favorites.filter(i => i.id !== id);
this.setData({ favorites: newList });
}).catch(err => {
console.error('Delete favorite failed', err);
wx.showToast({ title: '删除失败', icon: 'none' });
});
}
}
});
},
goToDetail(e) {
const item = e.currentTarget.dataset.item;
if (item.type === 'plant') {
wx.navigateTo({ url: `/pages/wiki/detail/index?id=${item.id}` });
} else {
wx.showToast({ title: '暂不支持查看动态详情', icon: 'none' });
}
},
onReachBottom() {
this.fetchFavorites(false);
} }
}); });
+5 -1
View File
@@ -2,6 +2,10 @@
"navigationBarTitleText": "我的收藏", "navigationBarTitleText": "我的收藏",
"usingComponents": { "usingComponents": {
"t-icon": "tdesign-miniprogram/icon/icon", "t-icon": "tdesign-miniprogram/icon/icon",
"t-image": "tdesign-miniprogram/image/image" "t-image": "tdesign-miniprogram/image/image",
"t-swipe-cell": "tdesign-miniprogram/swipe-cell/swipe-cell",
"t-tag": "tdesign-miniprogram/tag/tag",
"t-avatar": "tdesign-miniprogram/avatar/avatar",
"t-loading": "tdesign-miniprogram/loading/loading"
} }
} }
+57 -11
View File
@@ -1,27 +1,73 @@
<view class="favorites-page"> <view class="favorites-page">
<view class="category-filter"> <view class="category-filter">
<view class="filter-chip {{favTab === 'all' ? 'active' : ''}}" bindtap="onFavTabChange" data-value="all">全部</view> <view class="filter-chip {{favTab === 'all' ? 'active' : ''}}" bindtap="onFavTabChange" data-value="all">全部</view>
<view class="filter-chip {{favTab === 'plant' ? 'active' : ''}}" bindtap="onFavTabChange" data-value="plant">植物</view> <view class="filter-chip {{favTab === 'plant' ? 'active' : ''}}" bindtap="onFavTabChange" data-value="plant">百科</view>
<view class="filter-chip {{favTab === 'article' ? 'active' : ''}}" bindtap="onFavTabChange" data-value="article">文章</view> <view class="filter-chip {{favTab === 'post' ? 'active' : ''}}" bindtap="onFavTabChange" data-value="post">动态</view>
</view> </view>
<scroll-view scroll-y class="fav-scroll" enhanced show-scrollbar="{{false}}"> <scroll-view
<view wx:if="{{filteredFavorites.length > 0}}" class="fav-grid"> scroll-y
<view wx:for="{{filteredFavorites}}" wx:key="id" class="fav-card"> class="fav-scroll"
<t-image src="{{item.image}}" mode="aspectFill" width="100%" height="240rpx" class="fav-img" /> enhanced
show-scrollbar="{{false}}"
bindscrolltolower="onReachBottom"
>
<view wx:if="{{favorites.length > 0}}" class="fav-list">
<t-swipe-cell wx:for="{{favorites}}" wx:key="id">
<view class="fav-item" bindtap="goToDetail" data-item="{{item}}">
<!-- Render Wiki Plant -->
<block wx:if="{{item.type === 'plant'}}">
<t-image src="{{item.image}}" mode="aspectFill" class="fav-img plant-img" width="160rpx" height="160rpx" />
<view class="fav-info"> <view class="fav-info">
<text class="fav-name">{{item.name}}</text> <text class="fav-title">{{item.name}}</text>
<view class="fav-meta-row"> <text class="fav-subtitle">{{item.latinName}}</text>
<t-icon name="{{item.type === 'plant' ? 'heart' : 'book'}}" size="28rpx" color="#90A4AE" /> <view class="fav-meta">
<text class="fav-type">{{item.meta}}</text> <t-tag size="small" variant="light" theme="success">植物百科</t-tag>
<text wx:if="{{item.difficultyLabel}}" class="meta-diff">难度: {{item.difficultyLabel}}</text>
</view> </view>
</view> </view>
</block>
<!-- Render Community Post -->
<block wx:else>
<t-image
src="{{item.postImage || item.avatar}}"
mode="aspectFill"
class="fav-img post-img"
width="160rpx" height="160rpx"
/>
<view class="fav-info">
<text class="fav-title post-title">{{item.content || '无标题'}}</text>
<view class="fav-user-row">
<t-avatar image="{{item.avatar}}" size="small" />
<text class="user-nickname">{{item.nickname}}</text>
</view>
<view class="fav-meta">
<t-tag size="small" variant="light" theme="primary">社区动态</t-tag>
<text class="meta-time">{{item.time}}</text>
</view> </view>
</view> </view>
<view wx:else class="empty-state"> </block>
</view>
<view slot="right" class="delete-btn" catchtap="onSwipeClick" data-item="{{item}}">
<text>删除</text>
</view>
</t-swipe-cell>
</view>
<!-- Empty State -->
<view wx:elif="{{!isLoading}}" class="empty-state">
<t-icon name="star" size="80rpx" color="#E0E0E0" style="margin-bottom: 24rpx;" /> <t-icon name="star" size="80rpx" color="#E0E0E0" style="margin-bottom: 24rpx;" />
<text class="empty-text">暂无收藏内容</text> <text class="empty-text">暂无收藏内容</text>
</view> </view>
<!-- Loading State -->
<view class="loading-footer" wx:if="{{isLoading}}">
<t-loading theme="circular" size="40rpx" text="加载中..." />
</view>
<view wx:if="{{!hasMore && favorites.length > 0}}" class="no-more-text">没有更多了</view>
<view style="height: 60rpx;"></view> <view style="height: 60rpx;"></view>
</scroll-view> </scroll-view>
</view> </view>
+95 -23
View File
@@ -12,6 +12,7 @@
display: flex; display: flex;
gap: 16rpx; gap: 16rpx;
margin-bottom: 24rpx; margin-bottom: 24rpx;
flex-shrink: 0;
} }
.filter-chip { .filter-chip {
@@ -26,10 +27,10 @@
} }
.filter-chip.active { .filter-chip.active {
background: #333; background: #558B2F;
color: #fff; color: #fff;
font-weight: 600; font-weight: 600;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.1); box-shadow: 0 4rpx 12rpx rgba(85, 139, 47, 0.2);
} }
.fav-scroll { .fav-scroll {
@@ -38,47 +39,91 @@
width: 100%; width: 100%;
} }
.fav-grid { .fav-list {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20rpx;
}
.fav-card {
background: white;
border-radius: 20rpx;
overflow: hidden;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.03);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 24rpx;
}
.fav-item {
background: white;
border-radius: 20rpx;
padding: 24rpx;
display: flex;
gap: 24rpx;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.02);
}
.fav-item:active {
background: #fafafa;
} }
.fav-img { .fav-img {
width: 160rpx;
height: 160rpx;
border-radius: 16rpx;
flex-shrink: 0;
background: #f0f0f0; background: #f0f0f0;
} }
.fav-info { .fav-info {
padding: 16rpx 20rpx; flex: 1;
display: flex;
flex-direction: column;
justify-content: flex-start;
min-width: 0;
height: 160rpx;
} }
.fav-name { .fav-title {
display: block; font-size: 30rpx;
font-size: 28rpx;
font-weight: 600; font-weight: 600;
color: #1F2937; color: #1F2937;
margin-bottom: 8rpx; margin-bottom: 4rpx;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; line-height: 1.4;
white-space: nowrap;
} }
.fav-meta-row { .fav-subtitle {
font-size: 24rpx;
color: #9CA3AF;
font-style: italic;
margin-bottom: auto; /* Push meta down */
display: block;
}
.fav-user-row {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8rpx; gap: 12rpx;
margin-bottom: auto; /* Push meta down */
margin-top: 8rpx;
} }
.fav-type { .user-nickname {
font-size: 24rpx;
color: #6B7280;
}
.fav-meta {
display: flex;
align-items: center;
gap: 16rpx;
margin-top: 12rpx;
}
.meta-diff {
font-size: 22rpx;
color: #F59E0B;
background: #FFFBEB;
padding: 4rpx 12rpx;
border-radius: 8rpx;
}
.meta-time {
font-size: 22rpx; font-size: 22rpx;
color: #9CA3AF; color: #9CA3AF;
} }
@@ -96,3 +141,30 @@
color: #9CA3AF; color: #9CA3AF;
margin-top: 16rpx; margin-top: 16rpx;
} }
.loading-footer {
padding: 32rpx;
display: flex;
justify-content: center;
}
.no-more-text {
text-align: center;
padding: 32rpx;
color: #ccc;
font-size: 24rpx;
}
.delete-btn {
background-color: #EF4444;
color: white;
width: 140rpx;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
font-weight: 500;
border-top-right-radius: 20rpx;
border-bottom-right-radius: 20rpx;
}
+24
View File
@@ -80,6 +80,30 @@ Page({
}); });
}, },
deleteLog(e) {
const id = e.currentTarget.dataset.id;
wx.showModal({
title: '提示',
content: '确定要删除这条识别记录吗?',
confirmColor: '#EF5350',
success: (res) => {
if (res.confirm) {
wx.showLoading({ title: '删除中' });
request.post('/classify/deleteClassifyLog', { ids: [id] }).then(() => {
wx.hideLoading();
wx.showToast({ title: '删除成功', icon: 'success' });
const newRecords = this.data.records.filter(r => r.id !== id);
this.setData({ records: newRecords });
}).catch(err => {
wx.hideLoading();
console.error('Delete log failed', err);
wx.showToast({ title: '删除失败', icon: 'none' });
});
}
}
});
},
_formatTime(dateStr) { _formatTime(dateStr) {
if (!dateStr) return ''; if (!dateStr) return '';
const d = new Date(dateStr); const d = new Date(dateStr);
@@ -4,6 +4,7 @@
"t-icon": "tdesign-miniprogram/icon/icon", "t-icon": "tdesign-miniprogram/icon/icon",
"t-image": "tdesign-miniprogram/image/image", "t-image": "tdesign-miniprogram/image/image",
"t-empty": "tdesign-miniprogram/empty/empty", "t-empty": "tdesign-miniprogram/empty/empty",
"t-swipe-cell": "tdesign-miniprogram/swipe-cell/swipe-cell",
"t-loading": "tdesign-miniprogram/loading/loading" "t-loading": "tdesign-miniprogram/loading/loading"
} }
} }
+9 -1
View File
@@ -16,7 +16,8 @@
<!-- Record List --> <!-- Record List -->
<view wx:else class="record-list"> <view wx:else class="record-list">
<view wx:for="{{records}}" wx:key="id" class="record-card" bindtap="toggleExpand" data-id="{{item.id}}"> <t-swipe-cell wx:for="{{records}}" wx:key="id" class="record-swipe-cell">
<view class="record-card" bindtap="toggleExpand" data-id="{{item.id}}">
<!-- Card Header --> <!-- Card Header -->
<view class="card-header"> <view class="card-header">
@@ -91,6 +92,13 @@
</view> </view>
<view slot="right" class="delete-action" catchtap="deleteLog" data-id="{{item.id}}">
<view class="delete-btn">
<t-icon name="delete" size="40rpx" color="#fff" />
</view>
</view>
</t-swipe-cell>
<!-- Load More --> <!-- Load More -->
<view wx:if="{{loading && records.length > 0}}" class="load-more"> <view wx:if="{{loading && records.length > 0}}" class="load-more">
<t-loading theme="circular" size="36rpx" text="加载更多..." /> <t-loading theme="circular" size="36rpx" text="加载更多..." />
+20
View File
@@ -276,3 +276,23 @@
font-size: 24rpx; font-size: 24rpx;
color: #D1D5DB; color: #D1D5DB;
} }
.delete-action {
display: flex;
align-items: center;
justify-content: center;
width: 140rpx;
height: 100%;
padding-left: 20rpx;
}
.delete-btn {
width: 88rpx;
height: 88rpx;
border-radius: 50%;
background: #EF5350;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 12rpx rgba(239, 83, 80, 0.4);
}
+9 -2
View File
@@ -1,5 +1,6 @@
// pages/profile/index.js // pages/profile/index.js
import request from '../../utils/request'; import request from '../../utils/request';
import { calculateDaysSince } from '../../utils/dateUtil';
const app = getApp(); const app = getApp();
@@ -78,7 +79,9 @@ Page({
userLevelTag: levelTag, userLevelTag: levelTag,
// EXP / Sunlight // EXP / Sunlight
userExp: res.currentSunlight || 0 userExp: res.currentSunlight || 0,
userSunlight: res.currentSunlight || 0,
joinedDays: calculateDaysSince(res.createdAt)
}); });
// Update global cache // Update global cache
@@ -111,6 +114,10 @@ Page({
}, },
// ======== Menu Actions ======== // ======== Menu Actions ========
goToExchange() {
wx.navigateTo({ url: '/pages/profile/exchange/index' });
},
goToIdentifyHistory() { goToIdentifyHistory() {
wx.navigateTo({ url: '/pages/profile/identify-history/index' }); wx.navigateTo({ url: '/pages/profile/identify-history/index' });
}, },
@@ -129,7 +136,7 @@ Page({
}, },
goToAbout() { goToAbout() {
this.setData({ view: 'about' }); wx.navigateTo({ url: '/pages/profile/about/index' });
}, },
openDoc(e) { openDoc(e) {
+41 -67
View File
@@ -1,39 +1,7 @@
<view class="profile-page"> <view class="profile-page">
<!-- ======== ABOUT VIEW ======== -->
<view wx:if="{{view === 'about'}}" class="sub-view info-view-anim">
<view class="sub-nav" bindtap="goBack">
<t-icon name="chevron-left" size="40rpx" />
<text class="sub-nav-title">关于我们</text>
</view>
<scroll-view scroll-y class="sub-scroll">
<view class="about-section">
<view class="about-logo-area">
<view class="about-logo">
<t-icon name="flower" size="72rpx" color="#558B2F" />
</view>
<text class="about-app-name">植物护理助手</text>
<text class="about-version">版本 {{appVersion}}</text>
</view>
<view class="about-desc">
<text>一款专注于家庭植物养护的小程序。帮助你记录植物成长、制定养护计划、识别未知植物,与花友们分享养花心得。</text>
</view>
<view class="about-menu-list">
<view class="about-menu-item" bindtap="openDoc" data-type="terms">
<text>用户协议</text>
<t-icon name="chevron-right" size="36rpx" color="#CCC" />
</view>
</view>
<view class="about-footer">
<text class="about-copyright">© 2026 Sundynix · All Rights Reserved</text>
</view>
</view>
</scroll-view>
</view>
<!-- ======== MAIN PROFILE VIEW ======== --> <!-- ======== MAIN PROFILE VIEW ======== -->
<view wx:else class="main-profile-view"> <view class="main-profile-view">
<!-- Header --> <!-- Header -->
<view class="profile-header"> <view class="profile-header">
<view class="user-main"> <view class="user-main">
@@ -41,9 +9,20 @@
<t-avatar wx:if="{{userAvatar}}" image="{{userAvatar}}" size="120rpx" /> <t-avatar wx:if="{{userAvatar}}" image="{{userAvatar}}" size="120rpx" />
<t-avatar wx:else icon="user" size="120rpx" /> <t-avatar wx:else icon="user" size="120rpx" />
</view> </view>
<view class="user-text" bindtap="openProfileEditor"> <view class="user-text">
<view class="user-name">{{userName}}</view> <view class="user-name" bindtap="openProfileEditor">{{userName}}</view>
<view class="level-badge">{{userLevelTag || 'Lv.0 园艺新手'}}</view> <view class="level-badge" bindtap="goToBadges">{{userLevelTag || 'Lv.0 园艺新手'}}</view>
<view class="user-sun-days">
<view class="sun-days-item">
<t-icon name="sunny" size="24rpx" color="#FBC02D" />
<text>{{userSunlight}} 阳光</text>
</view>
<view class="sun-days-divider"></view>
<view class="sun-days-item">
<t-icon name="calendar" size="24rpx" color="#66BB6A" />
<text>加入 {{joinedDays}} 天</text>
</view>
</view>
</view> </view>
</view> </view>
<view class="settings-btn" bindtap="goToNotificationSettings"> <view class="settings-btn" bindtap="goToNotificationSettings">
@@ -52,30 +31,38 @@
</view> </view>
<!-- Stats Card (Fixed) --> <!-- Stats Card (Fixed) -->
<view class="stats-section">
<view class="stats-card">
<view class="stat-item">
<text class="stat-num">{{plantCount}}</text>
<text class="stat-label">植物</text>
</view>
<view class="stat-divider"></view>
<view class="stat-item">
<text class="stat-num">{{taskDoneCount}}</text>
<text class="stat-label">养护次数</text>
</view>
<view class="stat-divider"></view>
<view class="stat-item">
<text class="stat-num">{{postCount}}</text>
<text class="stat-label">动态</text>
</view>
</view>
</view>
<scroll-view scroll-y class="profile-content" enhanced show-scrollbar="{{false}}" scroll-top="{{scrollTop}}"> <scroll-view scroll-y class="profile-content" enhanced show-scrollbar="{{false}}" scroll-top="{{scrollTop}}">
<!-- Menu --> <!-- Menu -->
<view class="profile-menu"> <view class="profile-menu">
<view class="menu-group-title">常用功能</view> <view class="menu-group-title">常用功能</view>
<view class="menu-item" bindtap="goToExchange">
<view class="menu-left">
<view class="menu-icon-bg" style="background: #FCE4EC">
<t-icon name="gift" size="36rpx" color="#E91E63" />
</view>
<text class="menu-text">兑换中心</text>
</view>
<view class="menu-right-info">
<text class="menu-badge-text">开发中</text>
<t-icon name="chevron-right" size="36rpx" color="#ccc" />
</view>
</view>
<view class="menu-item" bindtap="goToBadges">
<view class="menu-left">
<view class="menu-icon-bg" style="background: #F3E5F5">
<t-icon name="award" size="36rpx" color="#9C27B0" />
</view>
<text class="menu-text">等级徽章</text>
</view>
<view class="menu-right-info">
<t-icon name="chevron-right" size="36rpx" color="#ccc" />
</view>
</view>
<view class="menu-item" bindtap="goToFavorites"> <view class="menu-item" bindtap="goToFavorites">
<view class="menu-left"> <view class="menu-left">
<view class="menu-icon-bg" style="background: #FFF3E0"> <view class="menu-icon-bg" style="background: #FFF3E0">
@@ -106,19 +93,6 @@
<t-icon name="chevron-right" size="36rpx" color="#ccc" /> <t-icon name="chevron-right" size="36rpx" color="#ccc" />
</view> </view>
<view class="menu-item" bindtap="goToBadges">
<view class="menu-left">
<view class="menu-icon-bg" style="background: #F3E5F5">
<t-icon name="award" size="36rpx" color="#9C27B0" />
</view>
<text class="menu-text">等级徽章</text>
</view>
<view class="menu-right-info">
<!-- <text class="menu-badge-text">已获 3 个</text> -->
<t-icon name="chevron-right" size="36rpx" color="#ccc" />
</view>
</view>
<view class="menu-group-title" style="margin-top: 32rpx;">更多服务</view> <view class="menu-group-title" style="margin-top: 32rpx;">更多服务</view>
<view class="menu-item" bindtap="goToAbout"> <view class="menu-item" bindtap="goToAbout">
+38 -41
View File
@@ -344,8 +344,10 @@
border-bottom-left-radius: 48rpx; border-bottom-left-radius: 48rpx;
border-bottom-right-radius: 48rpx; border-bottom-right-radius: 48rpx;
box-shadow: 0 8rpx 30rpx rgba(0,0,0,0.02); box-shadow: 0 8rpx 30rpx rgba(0,0,0,0.02);
margin-bottom: 24rpx; margin-bottom: 0;
flex-shrink: 0; flex-shrink: 0;
position: relative;
z-index: 102;
} }
.user-main { .user-main {
@@ -385,6 +387,32 @@
font-weight: 600; font-weight: 600;
} }
.user-sun-days {
display: flex;
align-items: center;
gap: 16rpx;
background: rgba(255,255,255,0.6);
padding: 4rpx 16rpx;
border-radius: 20rpx;
align-self: flex-start;
border: 1rpx solid rgba(0,0,0,0.05);
}
.sun-days-item {
display: flex;
align-items: center;
gap: 6rpx;
font-size: 22rpx;
color: #555;
font-weight: 500;
}
.sun-days-divider {
width: 2rpx;
height: 18rpx;
background: #DDD;
}
.settings-btn { .settings-btn {
padding: 16rpx; padding: 16rpx;
} }
@@ -408,46 +436,7 @@
scrollbar-width: none; scrollbar-width: none;
} }
.stats-section {
padding: 0 32rpx;
flex-shrink: 0;
}
/* Stats Card */
.stats-card {
display: flex;
background: #fff;
padding: 40rpx 0;
border-radius: 32rpx;
box-shadow: 0 8rpx 24rpx rgba(0,0,0,0.02);
margin-bottom: 32rpx;
}
.stat-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
}
.stat-num {
font-size: 40rpx;
font-weight: 800;
color: #374151;
}
.stat-label {
font-size: 24rpx;
color: #9CA3AF;
}
.stat-divider {
width: 2rpx;
height: 60%;
background: #F3F4F6;
align-self: center;
}
/* Profile Menu */ /* Profile Menu */
.profile-menu { .profile-menu {
@@ -460,7 +449,15 @@
font-size: 26rpx; font-size: 26rpx;
color: #9CA3AF; color: #9CA3AF;
font-weight: 600; font-weight: 600;
margin-left: 12rpx;
position: sticky;
top: 0;
z-index: 100;
background: #F4F6F0;
/* negative margin to cover side padding of parent */
margin: 0 -32rpx;
padding: 44rpx 32rpx 16rpx 44rpx; /* 32rpx padding + 12rpx visual indent */
} }
.menu-item { .menu-item {
+2 -1
View File
@@ -3,6 +3,7 @@
"disableScroll": true, "disableScroll": true,
"usingComponents": { "usingComponents": {
"t-icon": "tdesign-miniprogram/icon/icon", "t-icon": "tdesign-miniprogram/icon/icon",
"t-image": "tdesign-miniprogram/image/image" "t-image": "tdesign-miniprogram/image/image",
"t-swipe-cell": "tdesign-miniprogram/swipe-cell/swipe-cell"
} }
} }
+6 -2
View File
@@ -12,6 +12,7 @@
<view wx:if="{{myPublishedPosts.length > 0}}" class="posts-list"> <view wx:if="{{myPublishedPosts.length > 0}}" class="posts-list">
<view wx:for="{{myPublishedPosts}}" wx:key="id" class="my-post-card"> <view wx:for="{{myPublishedPosts}}" wx:key="id" class="my-post-card">
<view class="my-post-time">{{item.time}}</view> <view class="my-post-time">{{item.time}}</view>
<t-swipe-cell class="post-swipe-cell">
<view class="my-post-content-wrap"> <view class="my-post-content-wrap">
<view class="post-header" wx:if="{{item.hasReviewed !== undefined}}"> <view class="post-header" wx:if="{{item.hasReviewed !== undefined}}">
<view class="status-tag pending" wx:if="{{item.hasReviewed === 0}}">待审核</view> <view class="status-tag pending" wx:if="{{item.hasReviewed === 0}}">待审核</view>
@@ -26,12 +27,15 @@
<view class="my-post-footer"> <view class="my-post-footer">
<view class="footer-item"><t-icon name="heart" size="28rpx" /> <text>{{item.likes.length}}</text></view> <view class="footer-item"><t-icon name="heart" size="28rpx" /> <text>{{item.likes.length}}</text></view>
<view class="footer-item"><t-icon name="chat" size="28rpx" /> <text>{{item.comments.length}}</text></view> <view class="footer-item"><t-icon name="chat" size="28rpx" /> <text>{{item.comments.length}}</text></view>
<view class="footer-item" catchtap="deletePost" data-id="{{item.id}}" style="margin-left:auto; color: #EF5350;">
<t-icon name="delete" size="28rpx" />
</view> </view>
</view> </view>
<view slot="right" class="delete-action" catchtap="deletePost" data-id="{{item.id}}">
<view class="delete-btn">
<t-icon name="delete" size="40rpx" color="#fff" />
</view> </view>
</view> </view>
</t-swipe-cell>
</view>
</view> </view>
<view wx:else class="empty-state"> <view wx:else class="empty-state">
<t-icon name="file-copy" size="80rpx" color="#E0E0E0" style="margin-bottom: 24rpx;" /> <t-icon name="file-copy" size="80rpx" color="#E0E0E0" style="margin-bottom: 24rpx;" />
+25 -1
View File
@@ -161,10 +161,34 @@
/* Hide Scrollbar Globally */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 0; width: 0;
height: 0; height: 0;
color: transparent; color: transparent;
display: none; display: none;
} }
.post-swipe-cell {
flex: 1;
min-width: 0;
}
.delete-action {
display: flex;
align-items: center;
justify-content: center;
width: 140rpx;
height: 100%;
padding-left: 20rpx;
}
.delete-btn {
width: 88rpx;
height: 88rpx;
border-radius: 50%;
background: #EF5350;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 12rpx rgba(239, 83, 80, 0.4);
}
+11 -1
View File
@@ -110,10 +110,20 @@ Page({
const type = isFavorited ? 2 : 1; const type = isFavorited ? 2 : 1;
request.get('/wiki/star', { id, type }).then(() => { request.get('/wiki/star', { id, type }).then(() => {
const newStatus = !isFavorited;
this.setData({ this.setData({
'plant.isFavorited': !isFavorited 'plant.isFavorited': newStatus
}); });
wx.showToast({ title: type === 1 ? '已收藏' : '已取消', icon: 'success' }); wx.showToast({ title: type === 1 ? '已收藏' : '已取消', icon: 'success' });
// Sync with previous page (Wiki List)
const pages = getCurrentPages();
if (pages.length > 1) {
const prevPage = pages[pages.length - 2];
if (prevPage.updateItemFavoriteStatus) {
prevPage.updateItemFavoriteStatus(id, newStatus);
}
}
}).catch(err => { }).catch(err => {
console.error('Toggle favorite failed', err); console.error('Toggle favorite failed', err);
wx.showToast({ title: '操作失败', icon: 'none' }); wx.showToast({ title: '操作失败', icon: 'none' });
+10
View File
@@ -146,6 +146,16 @@ Page({
} }
}, },
updateItemFavoriteStatus(id, isFavorited) {
const index = this.data.displayedList.findIndex(i => i.id == id);
if (index === -1) return;
const key = `displayedList[${index}].isFavorited`;
this.setData({
[key]: isFavorited
});
},
// Search Input Handler (debounced) // Search Input Handler (debounced)
onSearchInput(e) { onSearchInput(e) {
const value = e.detail.value; const value = e.detail.value;
+1 -1
View File
@@ -53,7 +53,7 @@
}, },
"compileType": "miniprogram", "compileType": "miniprogram",
"libVersion": "3.7.1", "libVersion": "3.7.1",
"appid": "wx52dfc635739a9c19", "appid": "wxb463820bf36dd5d6",
"projectname": "plant-mp", "projectname": "plant-mp",
"isGameTourist": false, "isGameTourist": false,
"condition": { "condition": {
+1 -1
View File
@@ -3,7 +3,7 @@
"projectname": "plant-mp", "projectname": "plant-mp",
"condition": {}, "condition": {},
"setting": { "setting": {
"urlCheck": true, "urlCheck": false,
"coverView": true, "coverView": true,
"lazyloadPlaceholderEnable": false, "lazyloadPlaceholderEnable": false,
"skylineRenderEnable": false, "skylineRenderEnable": false,
+28
View File
@@ -0,0 +1,28 @@
const calculateDaysSince = (dateStr) => {
if (!dateStr) return 1; // Default to 1 day if date missing
// Use timestamps to calculate difference
const start = new Date(dateStr).getTime();
const now = new Date().getTime();
if (isNaN(start)) return 1;
const diffTime = Math.abs(now - start);
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
return diffDays || 1; // Return 1 even if diffDays is 0 (just joined)
};
const getPlantAgeBadge = (days) => {
if (days <= 15) return { icon: '🌱', title: '职场萌新', desc: '还在适应新家的光照和水土,随时可能“离职”。' };
if (days <= 60) return { icon: '🌿', title: '正式员工', desc: '已经度过了缓苗期,冒出了第一片新叶。' };
if (days <= 180) return { icon: '🪴', title: '资深住户', desc: '经历了季节交替的考验,已经完全融入了你的家居环境。' };
if (days <= 365) return { icon: '🌳', title: '元老级导师', desc: '跨越了冬夏两季,生命力顽强,建议作为传家宝培养。' };
if (days <= 1095) return { icon: '🏅', title: '荣誉守护神', desc: '它已经不是植物了,是这个家的“不动产”和家庭成员。' };
return { icon: '🧚‍♀️', title: '植物成精', desc: '建议尊称它一声“绿植大仙”,它可能比你更了解阳台的风水。' };
};
module.exports = {
calculateDaysSince,
getPlantAgeBadge
};
+2 -2
View File
@@ -270,8 +270,8 @@ class WxRequest {
// Initialize with default instance // Initialize with default instance
const request = new WxRequest({ const request = new WxRequest({
//baseUrl: 'http://192.168.0.184:8889', baseUrl: 'http://192.168.0.184:8889',
baseUrl: 'https://go.sundynix.cn/api', //baseUrl: 'https://go.sundynix.cn/api',
header: { header: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }