feat: 样式修改

This commit is contained in:
Blizzard
2026-02-10 17:22:53 +08:00
parent 6f88bc656b
commit e97fd30fa3
28 changed files with 2097 additions and 903 deletions
+14 -4
View File
@@ -8,13 +8,23 @@ App({
// Send res.code to backend to swap for openId, sessionKey, unionId // Send res.code to backend to swap for openId, sessionKey, unionId
if (res.code) { if (res.code) {
request.get('/auth/miniLogin', { code: res.code }).then(data => { request.get('/auth/miniLogin', { code: res.code }).then(data => {
// Assuming the token is in data.token or data itself // Response structure based on user input: { user: {...}, token: "...", expiresAt: ... }
const token = data.token || data; // Note: request.js might return data.user directly if it unwraps 'data'
// But looking at previous request.js usage, it seems to return the 'data' field of the response.
// Let's handle both cases safely.
const token = data.token;
const user = data.user;
if (token && typeof token === 'string') { if (token && typeof token === 'string') {
wx.setStorageSync('token', token); wx.setStorageSync('token', token);
console.log('Login successful, token stored'); if (user) {
wx.setStorageSync('userInfo', user);
this.globalData.userInfo = user;
}
console.log('Login successful, user info stored');
} else { } else {
console.warn('Login response did not contain a valid token string', data); console.warn('Login response did not contain a valid token', data);
} }
}).catch(err => { }).catch(err => {
console.error('Login failed', err); console.error('Login failed', err);
+2 -1
View File
@@ -10,7 +10,8 @@
"pages/plant-detail/edit/index", "pages/plant-detail/edit/index",
"pages/plant-detail/index", "pages/plant-detail/index",
"pages/wiki/detail/index", "pages/wiki/detail/index",
"pages/wiki/identify/index" "pages/wiki/identify/index",
"pages/profile/identify-history/index"
], ],
"window": { "window": {
"backgroundTextStyle": "light", "backgroundTextStyle": "light",
+1 -1
View File
@@ -4,7 +4,7 @@ page {
--primary-light: #9CCC65; --primary-light: #9CCC65;
--primary-dark: #33691E; --primary-dark: #33691E;
--secondary: #8D6E63; --secondary: #8D6E63;
--bg-garden: #F1F8E9; --bg-garden: #F4F6F0;
--bg-card: rgba(255, 255, 255, 0.9); --bg-card: rgba(255, 255, 255, 0.9);
--text-main: #263238; --text-main: #263238;
--text-muted: #78909C; --text-muted: #78909C;
+1 -1
View File
@@ -1,7 +1,7 @@
/** pages/community/create/index.wxss **/ /** pages/community/create/index.wxss **/
page { page {
height: 100%; height: 100%;
background: #fff; background: #F4F6F0;
} }
.create-post-page { .create-post-page {
+3 -2
View File
@@ -125,8 +125,9 @@
</scroll-view> </scroll-view>
<!-- Floating Action Button --> <!-- Floating Action Button -->
<view class="fab" bindtap="goToCreatePost"> <view class="floating-add-btn" bindtap="goToCreatePost">
<t-icon name="add" size="48rpx" color="#fff" /> <t-icon name="add" size="40rpx" color="#FFF" />
<text>发布动态</text>
</view> </view>
<!-- Comment Input Mask --> <!-- Comment Input Mask -->
+13 -11
View File
@@ -5,7 +5,7 @@ page {
} }
.community-page { .community-page {
background-color: #fff; background-color: #F4F6F0;
height: 100vh; height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -298,25 +298,27 @@ page {
} }
/* Floating Action Button */ /* Floating Action Button */
.fab { .floating-add-btn {
position: fixed; position: fixed;
right: 40rpx; right: 40rpx;
bottom: 200rpx; bottom: 60rpx;
width: 112rpx; background: #558B2F;
height: 112rpx; color: white;
background: linear-gradient(135deg, #689F38, #558B2F); padding: 24rpx 40rpx;
border-radius: 50%; border-radius: 60rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; gap: 12rpx;
box-shadow: 0 12rpx 32rpx rgba(85, 139, 47, 0.4); box-shadow: 0 12rpx 32rpx rgba(85, 139, 47, 0.4);
z-index: 100; z-index: 100;
transition: all 0.2s; font-size: 28rpx;
font-weight: 700;
transition: all 0.2s ease;
} }
.fab:active { .floating-add-btn:active {
transform: scale(0.92); transform: scale(0.92);
box-shadow: 0 6rpx 16rpx rgba(85, 139, 47, 0.3); box-shadow: 0 4rpx 16rpx rgba(85, 139, 47, 0.2);
} }
/* WeChat Style Action Container */ /* WeChat Style Action Container */
-1
View File
@@ -1,7 +1,6 @@
{ {
"navigationBarTitleText": "我的花园", "navigationBarTitleText": "我的花园",
"usingComponents": { "usingComponents": {
"t-fab": "tdesign-miniprogram/fab/fab",
"t-popup": "tdesign-miniprogram/popup/popup", "t-popup": "tdesign-miniprogram/popup/popup",
"t-input": "tdesign-miniprogram/input/input", "t-input": "tdesign-miniprogram/input/input",
"t-button": "tdesign-miniprogram/button/button", "t-button": "tdesign-miniprogram/button/button",
+2 -2
View File
@@ -5,7 +5,7 @@ page {
} }
.add-plant-page { .add-plant-page {
background-color: #F5F7F5; background-color: #F4F6F0;
height: 100vh; height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -15,7 +15,7 @@ page {
.page-content { .page-content {
height: calc(100vh - 140rpx - env(safe-area-inset-bottom)); height: calc(100vh - 140rpx - env(safe-area-inset-bottom));
padding: 24rpx 32rpx; padding: 24rpx 32rpx;
background: #F5F7F5; background: #F4F6F0;
box-sizing: border-box; box-sizing: border-box;
} }
+3 -3
View File
@@ -17,9 +17,9 @@
/> />
<view class="header-gradient"></view> <view class="header-gradient"></view>
<!-- Custom Carousel Indicators --> <!-- Image Counter -->
<view class="carousel-indicators"> <view class="carousel-counter" wx:if="{{swiperImages.length > 0}}">
<view wx:for="{{swiperImages}}" wx:key="index" class="carousel-dot {{index === activeImageIndex ? 'active' : ''}}"></view> <text>{{activeImageIndex + 1}} / {{swiperImages.length}}</text>
</view> </view>
</view> </view>
+11 -19
View File
@@ -56,29 +56,21 @@ page {
z-index: 10; z-index: 10;
} }
/* Carousel Indicators */ /* Image Counter Badge */
.carousel-indicators { .carousel-counter {
position: absolute; position: absolute;
bottom: 100rpx; bottom: 100rpx;
right: 48rpx; right: 48rpx;
display: flex; background: rgba(0, 0, 0, 0.45);
gap: 12rpx; backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
color: white;
font-size: 22rpx;
font-weight: 600;
padding: 6rpx 18rpx;
border-radius: 12rpx;
z-index: 30; z-index: 30;
} letter-spacing: 2rpx;
.carousel-dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.4);
transition: all 0.3s;
box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.2);
}
.carousel-dot.active {
background: white;
width: 24rpx;
border-radius: 8rpx;
} }
.header-info { .header-info {
+102
View File
@@ -0,0 +1,102 @@
import request from '../../../utils/request';
Page({
data: {
records: [],
loading: true,
page: 1,
pageSize: 20,
total: 0,
hasMore: true,
expandedId: '', // Track which card is expanded
},
onLoad() {
this.loadRecords();
},
async loadRecords() {
this.setData({ loading: true });
try {
const res = await request.post('/classify/myClassifyLog', {
page: this.data.page,
pageSize: this.data.pageSize,
});
const list = (res.list || []).map(item => this._transformRecord(item));
this.setData({
records: this.data.page === 1 ? list : [...this.data.records, ...list],
total: res.total || 0,
hasMore: list.length >= this.data.pageSize,
loading: false,
});
} catch (err) {
console.error('Load identify history failed', err);
this.setData({ loading: false });
wx.showToast({ title: '加载失败', icon: 'none' });
}
},
_transformRecord(item) {
const allResults = item.allResults || [];
const topResult = allResults[0] || {};
const otherResults = allResults.slice(1);
return {
id: item.id,
time: this._formatTime(item.createdAt),
dateStr: item.createdAtStr || '',
topName: topResult.name || '未知植物',
topScore: topResult.score ? Math.round(topResult.score * 100) : 0,
topImage: topResult.baike_info?.image_url || '',
topDesc: topResult.baike_info?.description || '',
topBaikeUrl: topResult.baike_info?.baike_url || '',
otherResults: otherResults.map(r => ({
name: r.name || '未知',
score: r.score ? Math.round(r.score * 100) : 0,
hasInfo: !!r.baike_info,
})),
};
},
toggleExpand(e) {
const id = e.currentTarget.dataset.id;
this.setData({
expandedId: this.data.expandedId === id ? '' : id,
});
},
onReachBottom() {
if (!this.data.hasMore || this.data.loading) return;
this.setData({ page: this.data.page + 1 }, () => {
this.loadRecords();
});
},
onPullDownRefresh() {
this.setData({ page: 1, hasMore: true }, () => {
this.loadRecords().then(() => {
wx.stopPullDownRefresh();
});
});
},
_formatTime(dateStr) {
if (!dateStr) return '';
const d = new Date(dateStr);
const now = new Date();
const diffMs = now - d;
const diffMin = Math.floor(diffMs / 60000);
if (diffMin < 1) return '刚刚';
if (diffMin < 60) return diffMin + '分钟前';
const diffHour = Math.floor(diffMin / 60);
if (diffHour < 24) return diffHour + '小时前';
const diffDay = Math.floor(diffHour / 24);
if (diffDay < 7) return diffDay + '天前';
const month = (d.getMonth() + 1).toString().padStart(2, '0');
const day = d.getDate().toString().padStart(2, '0');
const hour = d.getHours().toString().padStart(2, '0');
const min = d.getMinutes().toString().padStart(2, '0');
return `${month}-${day} ${hour}:${min}`;
},
});
@@ -0,0 +1,9 @@
{
"navigationBarTitleText": "识别记录",
"usingComponents": {
"t-icon": "tdesign-miniprogram/icon/icon",
"t-image": "tdesign-miniprogram/image/image",
"t-empty": "tdesign-miniprogram/empty/empty",
"t-loading": "tdesign-miniprogram/loading/loading"
}
}
+103
View File
@@ -0,0 +1,103 @@
<view class="history-page">
<!-- Loading -->
<view wx:if="{{loading && records.length === 0}}" class="loading-wrap">
<t-loading theme="circular" size="48rpx" text="加载中..." />
</view>
<!-- Empty -->
<view wx:elif="{{!loading && records.length === 0}}" class="empty-wrap">
<view class="empty-icon">
<t-icon name="scan" size="120rpx" color="#D1D5DB" />
</view>
<text class="empty-title">暂无识别记录</text>
<text class="empty-hint">去百科页面拍照识别植物吧</text>
</view>
<!-- Record List -->
<view wx:else class="record-list">
<view wx:for="{{records}}" wx:key="id" class="record-card" bindtap="toggleExpand" data-id="{{item.id}}">
<!-- Card Header -->
<view class="card-header">
<view class="card-thumb">
<t-image
wx:if="{{item.topImage}}"
src="{{item.topImage}}"
mode="aspectFill"
width="120rpx"
height="120rpx"
shape="round"
/>
<view wx:else class="thumb-placeholder">
<t-icon name="image" size="48rpx" color="#D1D5DB" />
</view>
</view>
<view class="card-info">
<view class="card-name-row">
<text class="card-name">{{item.topName}}</text>
<view class="score-badge" style="background: {{item.topScore >= 80 ? '#E8F5E9' : item.topScore >= 50 ? '#FFF8E1' : '#FBE9E7'}}; color: {{item.topScore >= 80 ? '#2E7D32' : item.topScore >= 50 ? '#F57F17' : '#D84315'}}">
{{item.topScore}}%
</view>
</view>
<text class="card-time">{{item.time}}</text>
<view wx:if="{{item.otherResults.length > 0}}" class="other-hint">
<text>还可能是: </text>
<text wx:for="{{item.otherResults}}" wx:for-item="other" wx:key="name" class="other-name">{{other.name}}{{index < item.otherResults.length - 1 ? '、' : ''}}</text>
</view>
</view>
<view class="expand-arrow {{expandedId === item.id ? 'expanded' : ''}}">
<t-icon name="chevron-down" size="32rpx" color="#C5C5C5" />
</view>
</view>
<!-- Expanded Detail -->
<view wx:if="{{expandedId === item.id}}" class="card-detail">
<!-- Description -->
<view wx:if="{{item.topDesc}}" class="detail-desc">
<text class="desc-text">{{item.topDesc}}</text>
</view>
<!-- All Results -->
<view class="detail-results">
<text class="detail-label">识别结果排名</text>
<view class="result-bars">
<!-- Top result -->
<view class="result-bar-item">
<text class="bar-name">{{item.topName}}</text>
<view class="bar-track">
<view class="bar-fill top" style="width: {{item.topScore}}%"></view>
</view>
<text class="bar-score">{{item.topScore}}%</text>
</view>
<!-- Other results -->
<view wx:for="{{item.otherResults}}" wx:for-item="other" wx:key="name" class="result-bar-item">
<text class="bar-name">{{other.name}}</text>
<view class="bar-track">
<view class="bar-fill" style="width: {{other.score}}%"></view>
</view>
<text class="bar-score">{{other.score}}%</text>
</view>
</view>
</view>
<view class="detail-meta">
<t-icon name="time" size="24rpx" color="#9CA3AF" />
<text class="meta-text">{{item.dateStr}}</text>
</view>
</view>
</view>
<!-- Load More -->
<view wx:if="{{loading && records.length > 0}}" class="load-more">
<t-loading theme="circular" size="36rpx" text="加载更多..." />
</view>
<view wx:elif="{{!hasMore && records.length > 0}}" class="no-more">
<text>— 没有更多了 —</text>
</view>
</view>
</view>
+278
View File
@@ -0,0 +1,278 @@
/* pages/profile/identify-history/index.wxss */
.history-page {
min-height: 100vh;
background: #F4F6F0;
padding: 24rpx;
box-sizing: border-box;
}
/* Loading & Empty */
.loading-wrap {
display: flex;
justify-content: center;
align-items: center;
padding-top: 200rpx;
}
.empty-wrap {
display: flex;
flex-direction: column;
align-items: center;
padding-top: 200rpx;
}
.empty-icon {
width: 200rpx;
height: 200rpx;
background: #F3F4F6;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 40rpx;
}
.empty-title {
font-size: 32rpx;
font-weight: 700;
color: #6B7280;
margin-bottom: 12rpx;
}
.empty-hint {
font-size: 26rpx;
color: #9CA3AF;
}
/* Record List */
.record-list {
display: flex;
flex-direction: column;
gap: 24rpx;
padding-bottom: 60rpx;
}
.record-card {
background: #fff;
border-radius: 28rpx;
padding: 32rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.02);
transition: box-shadow 0.2s;
}
.record-card:active {
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
}
/* Card Header */
.card-header {
display: flex;
align-items: center;
gap: 24rpx;
}
.card-thumb {
flex-shrink: 0;
width: 120rpx;
height: 120rpx;
border-radius: 20rpx;
overflow: hidden;
}
.thumb-placeholder {
width: 100%;
height: 100%;
background: #F3F4F6;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
}
.card-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 8rpx;
}
.card-name-row {
display: flex;
align-items: center;
gap: 16rpx;
}
.card-name {
font-size: 32rpx;
font-weight: 700;
color: #1F2937;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.score-badge {
flex-shrink: 0;
font-size: 22rpx;
font-weight: 700;
padding: 4rpx 14rpx;
border-radius: 12rpx;
}
.card-time {
font-size: 24rpx;
color: #9CA3AF;
}
.other-hint {
font-size: 22rpx;
color: #9CA3AF;
display: flex;
flex-wrap: wrap;
gap: 2rpx;
margin-top: 4rpx;
}
.other-name {
color: #6B7280;
}
.expand-arrow {
flex-shrink: 0;
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
padding: 8rpx;
}
.expand-arrow.expanded {
transform: rotate(180deg);
}
/* Card Detail (Expanded) */
.card-detail {
margin-top: 28rpx;
padding-top: 28rpx;
border-top: 2rpx solid #F3F4F6;
animation: fadeSlideDown 0.3s ease;
}
@keyframes fadeSlideDown {
from {
opacity: 0;
transform: translateY(-16rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Description */
.detail-desc {
background: #F4F6F0;
border-radius: 20rpx;
padding: 24rpx;
margin-bottom: 28rpx;
}
.desc-text {
font-size: 26rpx;
line-height: 1.7;
color: #4B5563;
display: -webkit-box;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* Result Bars */
.detail-results {
margin-bottom: 24rpx;
}
.detail-label {
font-size: 24rpx;
font-weight: 600;
color: #6B7280;
margin-bottom: 20rpx;
display: block;
}
.result-bars {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.result-bar-item {
display: flex;
align-items: center;
gap: 16rpx;
}
.bar-name {
width: 120rpx;
font-size: 26rpx;
font-weight: 600;
color: #374151;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex-shrink: 0;
}
.bar-track {
flex: 1;
height: 16rpx;
background: #F3F4F6;
border-radius: 8rpx;
overflow: hidden;
}
.bar-fill {
height: 100%;
border-radius: 8rpx;
background: linear-gradient(90deg, #A5D6A7, #66BB6A);
transition: width 0.5s ease;
}
.bar-fill.top {
background: linear-gradient(90deg, #66BB6A, #388E3C);
}
.bar-score {
width: 80rpx;
font-size: 24rpx;
font-weight: 600;
color: #6B7280;
text-align: right;
flex-shrink: 0;
}
/* Meta */
.detail-meta {
display: flex;
align-items: center;
gap: 8rpx;
padding-top: 16rpx;
}
.meta-text {
font-size: 22rpx;
color: #9CA3AF;
}
/* Load More */
.load-more {
display: flex;
justify-content: center;
padding: 32rpx 0;
}
.no-more {
text-align: center;
padding: 32rpx 0;
font-size: 24rpx;
color: #D1D5DB;
}
+290 -74
View File
@@ -1,52 +1,163 @@
// pages/profile/index.js // pages/profile/index.js
import { MOCK_FAVORITES, MOCK_BADGES, MOCK_POSTS } from '../../utils/mockData'; import request from '../../utils/request';
const app = getApp();
Page({ Page({
data: { data: {
view: 'profile', // profile, favorites, posts, badges view: 'profile', // profile, favorites, posts, about
favTab: 'all', // all, plant, article
postsTab: 'published', // published, drafts
// User Info
userName: '植物爱好者',
userAvatar: '',
userLevel: '', // Reserved for future level system
userLevelTag: '', // e.g. 'Lv.4 资深植人'
// Stats
plantCount: 0,
taskDoneCount: 0,
postCount: 0,
// Favorites
favTab: 'all',
favorites: [], favorites: [],
filteredFavorites: [], filteredFavorites: [],
// Posts
postsTab: 'published',
myPublishedPosts: [], myPublishedPosts: [],
myDrafts: [], myDrafts: [],
badges: []
// App version
appVersion: '1.0.0'
}, },
onLoad(options) { onLoad() {
this.setData({ this.loadUserInfo();
favorites: MOCK_FAVORITES,
badges: MOCK_BADGES
});
this.filterFavorites();
}, },
onShow() { onShow() {
if (typeof this.getTabBar === 'function' && if (typeof this.getTabBar === 'function' && this.getTabBar()) {
this.getTabBar()) { this.getTabBar().setData({ selected: 4 });
this.getTabBar().setData({
selected: 4 // Index 4 is Profile
})
} }
// Refresh posts data
this.loadMyPosts();
this.loadDrafts();
}, },
// ======== User Info ========
loadUserInfo() {
// Try to get from globalData or storage
const userInfo = app.globalData.userInfo || wx.getStorageSync('userInfo');
if (userInfo && userInfo.name) {
this.setData({
userName: userInfo.name || '植物爱好者',
userAvatar: userInfo.avatarUrl || userInfo.avatar || ''
});
return; // Use cached data, no API call
}
// Only fetch from backend if no cached info
request.get('/user/info').then(user => {
if (!user) return;
const avatarUrl = user.avatar ? user.avatar.url : '';
this.setData({
userName: user.name || '植物爱好者',
userAvatar: avatarUrl
});
const info = {
id: user.id,
name: user.name,
avatarUrl: avatarUrl,
account: user.account,
phone: user.phone,
avatarId: user.avatarId
};
app.globalData.userInfo = info;
wx.setStorageSync('userInfo', info);
}).catch(() => { });
},
// ======== Stats ========
loadStats() {
// Fetch plant count
request.post('/plant/page', { current: 1, pageSize: 1 }).then(res => {
this.setData({ plantCount: res.total || 0 });
}).catch(() => { });
// Fetch post count - user's own posts
request.post('/post/page', { current: 1, pageSize: 1, onlyMine: true }).then(res => {
this.setData({ postCount: res.total || 0 });
}).catch(() => { });
// Fetch completed tasks count
request.get('/plant/taskCount').then(res => {
this.setData({ taskDoneCount: res || 0 });
}).catch(() => { });
},
// ======== Navigation ========
setView(e) {
const view = e.currentTarget.dataset.view;
this.setData({ view });
if (view === 'favorites') {
this.loadFavorites();
} else if (view === 'posts') {
this.loadMyPosts();
this.loadDrafts();
}
},
goBack() {
this.setData({ view: 'profile' });
},
// ======== Favorites ========
loadFavorites() {
// TODO: Call favorites API when available
// request.get('/user/favorites').then(...)
this.filterFavorites();
},
onFavTabChange(e) {
this.setData({ favTab: e.detail.value }, () => {
this.filterFavorites();
});
},
filterFavorites() {
const { favorites, favTab } = this.data;
const filtered = favorites.filter(item => {
if (favTab === 'all') return true;
return item.type === favTab;
});
this.setData({ filteredFavorites: filtered });
},
// ======== Posts ========
loadMyPosts() { loadMyPosts() {
// Get published posts by current user request.post('/post/page', { current: 1, pageSize: 50, onlyMine: true }).then(res => {
const myPosts = MOCK_POSTS.filter(p => p.user === '我的花园'); const records = res.records || res.list || [];
this.setData({ myPublishedPosts: myPosts }); const posts = records.map(item => {
const publisher = item.publisher || {};
const imgList = item.imgList || [];
return {
id: item.id,
content: item.content || '',
time: this._formatTime(item.createdAt || item.createTime),
images: imgList.map(img => img.url),
likes: item.likeList || [],
comments: item.commentList || []
};
});
this.setData({ myPublishedPosts: posts });
}).catch(() => {
this.setData({ myPublishedPosts: [] });
});
}, },
loadDrafts() { loadDrafts() {
// Load drafts from storage
try { try {
const draft = wx.getStorageSync('post_draft'); const draft = wx.getStorageSync('post_draft');
if (draft && (draft.content || (draft.images && draft.images.length > 0))) { if (draft && (draft.content || (draft.images && draft.images.length > 0))) {
// Convert single draft to array for consistency
this.setData({ this.setData({
myDrafts: [{ myDrafts: [{
id: 'draft_1', id: 'draft_1',
@@ -63,39 +174,10 @@ Page({
} }
}, },
setView(e) {
const view = e.currentTarget.dataset.view;
this.setData({ view });
// Refresh data when entering posts view
if (view === 'posts') {
this.loadMyPosts();
this.loadDrafts();
}
},
onFavTabChange(e) {
const tab = e.detail.value;
this.setData({ favTab: tab }, () => {
this.filterFavorites();
});
},
onPostsTabChange(e) { onPostsTabChange(e) {
const tab = e.detail.value; this.setData({ postsTab: e.detail.value });
this.setData({ postsTab: tab });
}, },
filterFavorites() {
const { favorites, favTab } = this.data;
const filtered = favorites.filter(item => {
if (favTab === 'all') return true;
return item.type === favTab;
});
this.setData({ filteredFavorites: filtered });
},
// Delete a published post
deletePost(e) { deletePost(e) {
const postId = e.currentTarget.dataset.id; const postId = e.currentTarget.dataset.id;
wx.showModal({ wx.showModal({
@@ -103,42 +185,176 @@ Page({
content: '确定要删除这条动态吗?', content: '确定要删除这条动态吗?',
confirmColor: '#EF5350', confirmColor: '#EF5350',
success: (res) => { success: (res) => {
if (res.confirm) { if (!res.confirm) return;
// Remove from MOCK_POSTS wx.showLoading({ title: '删除中...' });
const idx = MOCK_POSTS.findIndex(p => p.id === postId); request.get('/post/delete', { id: postId }).then(() => {
if (idx > -1) { wx.hideLoading();
MOCK_POSTS.splice(idx, 1);
}
this.loadMyPosts(); this.loadMyPosts();
wx.showToast({ title: '已删除', icon: 'success' }); wx.showToast({ title: '已删除', icon: 'success' });
} }).catch(() => {
wx.hideLoading();
wx.showToast({ title: '删除失败', icon: 'none' });
});
} }
}); });
}, },
// Edit a draft editDraft() {
editDraft(e) { wx.navigateTo({ url: '/pages/community/create/index' });
// Navigate to create page, which will load the draft
wx.navigateTo({
url: '/pages/community/create/index'
});
}, },
// Delete a draft deleteDraft() {
deleteDraft(e) {
wx.showModal({ wx.showModal({
title: '删除草稿', title: '删除草稿',
content: '确定要删除这份草稿吗?', content: '确定要删除这份草稿吗?',
confirmColor: '#EF5350', confirmColor: '#EF5350',
success: (res) => { success: (res) => {
if (res.confirm) { if (!res.confirm) return;
try { try { wx.removeStorageSync('post_draft'); } catch (e) { }
wx.removeStorageSync('post_draft');
} catch (e) { }
this.setData({ myDrafts: [] }); this.setData({ myDrafts: [] });
wx.showToast({ title: '已删除', icon: 'success' }); wx.showToast({ title: '已删除', icon: 'success' });
} }
});
},
// ======== Menu Actions ========
goToIdentifyHistory() {
wx.navigateTo({ url: '/pages/profile/identify-history/index' });
},
goToNotificationSettings() {
// Open WeChat notification settings
wx.openSetting({
success: (res) => {
console.log('Settings opened', res);
} }
}); });
},
goToAbout() {
this.setData({ view: 'about' });
},
goToAgreement() {
// TODO: Navigate to agreement page or show inline
wx.showToast({ title: '功能开发中', icon: 'none' });
},
goToPrivacy() {
// TODO: Navigate to privacy page or show inline
wx.showToast({ title: '功能开发中', icon: 'none' });
},
// ======== Profile Editor Popup ========
openProfileEditor() {
this.setData({
showProfileEditor: true,
tempAvatar: '',
tempNickname: this.data.userName === '植物爱好者' ? '' : this.data.userName
});
},
closeProfileEditor() {
this.setData({ showProfileEditor: false });
},
onProfilePopupChange(e) {
if (!e.detail.visible) {
this.setData({ showProfileEditor: false });
} }
},
// WeChat native chooseAvatar callback
onChooseAvatar(e) {
const avatarUrl = e.detail.avatarUrl;
if (avatarUrl) {
this.setData({ tempAvatar: avatarUrl });
}
},
onNicknameInput(e) {
this.setData({ tempNickname: e.detail.value });
},
onNicknameBlur(e) {
// WeChat nickname type may return value on blur
if (e.detail.value) {
this.setData({ tempNickname: e.detail.value });
}
},
async saveProfile() {
const { tempAvatar, tempNickname } = this.data;
if (!tempAvatar && !tempNickname) {
wx.showToast({ title: '请选择头像或输入昵称', icon: 'none' });
return;
}
wx.showLoading({ title: '保存中...', mask: true });
try {
const updatePayload = {};
// 1. Upload avatar if changed
if (tempAvatar) {
const data = await request.upload(tempAvatar);
const fileData = data?.file || {};
if (fileData.id) {
updatePayload.avatar_id = fileData.id;
// Update local display
this.setData({ userAvatar: fileData.url || tempAvatar });
}
}
// 2. Set name if provided
if (tempNickname) {
updatePayload.name = tempNickname;
this.setData({ userName: tempNickname });
}
// 3. Call update API
if (Object.keys(updatePayload).length > 0) {
await request.post('/user/update', updatePayload);
}
wx.hideLoading();
this.setData({ showProfileEditor: false });
wx.showToast({ title: '资料已更新', icon: 'success' });
// Update globalData
const userInfo = app.globalData.userInfo || {};
if (updatePayload.name) userInfo.name = updatePayload.name;
if (updatePayload.avatar_id) userInfo.avatarId = updatePayload.avatar_id;
app.globalData.userInfo = userInfo;
} catch (err) {
wx.hideLoading();
console.error('Save profile failed', err);
wx.showToast({ title: '保存失败', icon: 'none' });
}
},
// ======== Utilities ========
_formatTime(dateStr) {
if (!dateStr) return '';
const d = new Date(dateStr);
const now = new Date();
const diffMs = now - d;
const diffMin = Math.floor(diffMs / 60000);
if (diffMin < 1) return '刚刚';
if (diffMin < 60) return diffMin + '分钟前';
const diffHour = Math.floor(diffMin / 60);
if (diffHour < 24) return diffHour + '小时前';
const diffDay = Math.floor(diffHour / 24);
if (diffDay < 7) return diffDay + '天前';
const month = (d.getMonth() + 1).toString().padStart(2, '0');
const day = d.getDate().toString().padStart(2, '0');
return `${month}-${day}`;
},
// ======== Reserved: Future Level/Badge System ========
// These methods will be implemented when the backend supports level/badge APIs
// loadLevelInfo() { request.get('/user/level').then(...) },
// loadBadges() { request.get('/user/badges').then(...) },
}) })
+5 -4
View File
@@ -1,15 +1,16 @@
{ {
"navigationBarTitleText": "个人中心", "navigationBarTitleText": "个人中心",
"disableScroll": true,
"usingComponents": { "usingComponents": {
"t-grid": "tdesign-miniprogram/grid/grid",
"t-grid-item": "tdesign-miniprogram/grid-item/grid-item",
"t-cell": "tdesign-miniprogram/cell/cell", "t-cell": "tdesign-miniprogram/cell/cell",
"t-cell-group": "tdesign-miniprogram/cell-group/cell-group", "t-cell-group": "tdesign-miniprogram/cell-group/cell-group",
"t-avatar": "tdesign-miniprogram/avatar/avatar", "t-avatar": "tdesign-miniprogram/avatar/avatar",
"t-image": "tdesign-miniprogram/image/image", "t-image": "tdesign-miniprogram/image/image",
"t-tag": "tdesign-miniprogram/tag/tag", "t-tag": "tdesign-miniprogram/tag/tag",
"t-icon": "tdesign-miniprogram/icon/icon", "t-icon": "tdesign-miniprogram/icon/icon",
"t-badge": "tdesign-miniprogram/badge/badge", "t-tabs": "tdesign-miniprogram/tabs/tabs",
"t-progress": "tdesign-miniprogram/progress/progress" "t-tab-panel": "tdesign-miniprogram/tab-panel/tab-panel",
"t-button": "tdesign-miniprogram/button/button",
"t-popup": "tdesign-miniprogram/popup/popup"
} }
} }
+203 -123
View File
@@ -1,117 +1,77 @@
<wxs src="../../utils/tools.wxs" module="tools" />
<view class="profile-page"> <view class="profile-page">
<!-- Sub-views handled by conditional rendering to match prototype single-page feel -->
<!-- FAVORITES VIEW --> <!-- ======== FAVORITES VIEW ======== -->
<view wx:if="{{view === 'favorites'}}" class="favorites-page info-view-anim"> <view wx:if="{{view === 'favorites'}}" class="sub-view info-view-anim">
<view class="back-nav sticky-nav"> <view class="sub-nav" bindtap="goBack">
<t-button variant="text" icon="arrow-left" bind:tap="setView" data-view="profile">我的收藏</t-button> <t-icon name="chevron-left" size="40rpx" />
<text class="sub-nav-title">我的收藏</text>
</view> </view>
<t-tabs value="{{favTab}}" bind:change="onFavTabChange" theme="card"> <view class="category-filter">
<t-tab-panel label="全部" value="all" /> <view class="filter-chip {{favTab === 'all' ? 'active' : ''}}" bindtap="onFavTabChange" data-value="all">全部</view>
<t-tab-panel label="植物" value="plant" /> <view class="filter-chip {{favTab === 'plant' ? 'active' : ''}}" bindtap="onFavTabChange" data-value="plant">植物</view>
<t-tab-panel label="文章" value="article" /> <view class="filter-chip {{favTab === 'article' ? 'active' : ''}}" bindtap="onFavTabChange" data-value="article">文章</view>
</t-tabs> </view>
<view class="tab-content"> <scroll-view scroll-y class="sub-scroll">
<view class="fav-grid"> <view wx:if="{{filteredFavorites.length > 0}}" class="fav-grid">
<block wx:if="{{filteredFavorites.length > 0}}">
<view wx:for="{{filteredFavorites}}" wx:key="id" class="fav-card"> <view wx:for="{{filteredFavorites}}" wx:key="id" class="fav-card">
<t-image src="{{item.image}}" class="fav-img" mode="aspectFill" width="100%" height="240rpx" /> <t-image src="{{item.image}}" mode="aspectFill" width="100%" height="240rpx" class="fav-img" />
<view class="fav-info"> <view class="fav-info">
<text class="fav-name">{{item.name}}</text> <text class="fav-name">{{item.name}}</text>
<view class="fav-meta-row"> <view class="fav-meta-row">
<t-icon name="{{item.type === 'plant' ? 'heart' : 'book'}}" size="32rpx" color="#90A4AE" /> <t-icon name="{{item.type === 'plant' ? 'heart' : 'book'}}" size="28rpx" color="#90A4AE" />
<text class="fav-type">{{item.meta}}</text> <text class="fav-type">{{item.meta}}</text>
</view> </view>
</view> </view>
</view> </view>
</block>
<block wx:else>
<view class="empty-state">
<t-icon name="star" size="64rpx" color="#ccc" />
<text style="margin-top: 16rpx;">暂无收藏内容</text>
</view>
</block>
</view> </view>
<view wx:else class="empty-state">
<text class="empty-text">暂无收藏内容</text>
</view> </view>
</scroll-view>
</view> </view>
<!-- POSTS VIEW --> <!-- ======== POSTS VIEW ======== -->
<view wx:elif="{{view === 'posts'}}" class="posts-page-detail info-view-anim"> <view wx:elif="{{view === 'posts'}}" class="sub-view info-view-anim">
<view class="back-nav sticky-nav"> <view class="sub-nav" bindtap="goBack">
<t-button variant="text" icon="arrow-left" bind:tap="setView" data-view="profile">我的发布</t-button> <t-icon name="chevron-left" size="40rpx" />
<text class="sub-nav-title">我的发布</text>
</view> </view>
<!-- Tabs for Published / Drafts --> <scroll-view scroll-y class="sub-scroll">
<t-tabs value="{{postsTab}}" bind:change="onPostsTabChange" theme="card">
<t-tab-panel label="已发布" value="published" />
<t-tab-panel label="草稿箱" value="drafts" />
</t-tabs>
<!-- Published Posts -->
<view wx:if="{{postsTab === 'published'}}" class="my-posts-list">
<block wx:if="{{myPublishedPosts.length > 0}}">
<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>
<view class="my-post-content-wrap"> <view class="my-post-content-wrap">
<text class="post-text">{{item.content}}</text> <text class="post-text">{{item.content}}</text>
<view wx:if="{{item.images.length > 0}}" class="my-post-images"> <view wx:if="{{item.images.length > 0}}" class="my-post-images">
<t-image wx:for="{{item.images}}" wx:for-item="img" wx:key="*this" src="{{img}}" mode="aspectFill" width="160rpx" height="160rpx" style="margin-right: 16rpx; display: inline-block; border-radius: 8rpx;" /> <t-image wx:for="{{item.images}}" wx:for-item="img" wx:key="*this"
src="{{img}}" mode="aspectFill" width="140rpx" height="140rpx"
shape="round" />
</view> </view>
<view class="my-post-footer"> <view class="my-post-footer">
<view class="footer-item"><t-icon name="heart" size="32rpx" /> <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="32rpx" /> <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 delete-btn" bindtap="deletePost" data-id="{{item.id}}"><t-icon name="delete" size="32rpx" color="#EF5350" /></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> </view>
</block>
<view wx:else class="empty-state">
<t-icon name="file-copy" size="64rpx" color="#ccc" />
<text style="margin-top: 16rpx;">暂无已发布的动态</text>
</view> </view>
</scroll-view>
</view> </view>
<!-- Draft Posts --> <!-- ======== BADGES VIEW ======== -->
<view wx:if="{{postsTab === 'drafts'}}" class="my-posts-list"> <view wx:elif="{{view === 'badges'}}" class="sub-view info-view-anim">
<block wx:if="{{myDrafts.length > 0}}"> <view class="sub-nav" bindtap="goBack">
<view wx:for="{{myDrafts}}" wx:key="id" class="my-post-card draft-card"> <t-icon name="chevron-left" size="40rpx" />
<view class="draft-badge">草稿</view> <text class="sub-nav-title">成就徽章</text>
<view class="my-post-content-wrap">
<text class="post-text">{{item.content || '(无文字内容)'}}</text>
<view wx:if="{{item.images.length > 0}}" class="my-post-images">
<t-image wx:for="{{item.images}}" wx:for-item="img" wx:key="*this" src="{{img}}" mode="aspectFill" width="160rpx" height="160rpx" style="margin-right: 16rpx; display: inline-block; border-radius: 8rpx;" />
</view>
<view class="my-post-footer">
<view class="footer-item edit-btn" bindtap="editDraft" data-index="{{index}}">
<t-icon name="edit" size="32rpx" color="#558B2F" />
<text style="color: #558B2F;">编辑</text>
</view>
<view class="footer-item delete-btn" bindtap="deleteDraft" data-index="{{index}}">
<t-icon name="delete" size="32rpx" color="#EF5350" />
<text style="color: #EF5350;">删除</text>
</view>
</view>
</view>
</view>
</block>
<view wx:else class="empty-state">
<t-icon name="file-add" size="64rpx" color="#ccc" />
<text style="margin-top: 16rpx;">暂无草稿</text>
</view>
</view>
</view> </view>
<!-- BADGES VIEW --> <scroll-view scroll-y class="sub-scroll">
<view wx:elif="{{view === 'badges'}}" class="badges-page info-view-anim"> <!-- Level Card -->
<view class="back-nav sticky-nav">
<t-button variant="text" icon="arrow-left" bind:tap="setView" data-view="profile">成就徽章</t-button>
</view>
<scroll-view scroll-y class="badges-content">
<view class="level-card-large"> <view class="level-card-large">
<view class="level-card-bg"></view>
<view class="level-header"> <view class="level-header">
<view class="level-info-large"> <view class="level-info-large">
<text class="level-label">当前等级</text> <text class="level-label">当前等级</text>
@@ -119,82 +79,202 @@
</view> </view>
<t-icon name="trophy" size="80rpx" color="#FFD700" /> <t-icon name="trophy" size="80rpx" color="#FFD700" />
</view> </view>
<view class="level-progress-section"> <view class="level-progress-section">
<view class="progress-text"> <view class="progress-text">
<text>经验值</text> <text>经验值</text>
<text>350 / 500</text> <text>350 / 500</text>
</view> </view>
<t-progress percentage="70" theme="plump" color="#FFD700" track-color="rgba(255,255,255,0.3)" /> <view class="level-progress-bar-bg">
<view class="level-progress-bar-fill" style="width: 70%;"></view>
</view>
<text class="next-level-tip">距离 Lv.5 园艺大师 还需 150 经验</text> <text class="next-level-tip">距离 Lv.5 园艺大师 还需 150 经验</text>
</view> </view>
</view> </view>
<view class="section-title-badges">所有徽章 (3/6)</view> <view class="section-title-badges">所有徽章 (3/6)</view>
<t-grid column="{{3}}" gutter="24rpx">
<t-grid-item wx:for="{{badges}}" wx:key="id" text="{{item.name}}" description="{{item.desc}}" image="{{item.unlocked ? '/assets/icons/'+item.icon+'.png' : '/assets/icons/lock.png'}}" /> <view class="badges-grid">
<!-- TDesign grid item image might need full path or use slot for svg if needed, using png for now --> <view wx:for="{{badges}}" wx:key="id" class="badge-item {{item.unlocked ? 'unlocked' : 'locked'}}">
</t-grid> <view class="badge-icon-circle" style="background: {{item.unlocked ? item.color + '20' : '#F5F5F5'}}">
<t-icon wx:if="{{item.unlocked}}" name="{{item.iconName}}" size="48rpx" color="{{item.color}}" />
<t-icon wx:else name="lock-on" size="40rpx" color="#BDBDBD" />
</view>
<text class="badge-name">{{item.name}}</text>
<text class="badge-desc">{{item.desc}}</text>
<text wx:if="{{item.progress}}" class="badge-progress">{{item.progress}}</text>
</view>
</view>
</scroll-view> </scroll-view>
</view> </view>
<!-- MAIN PROFILE VIEW --> <!-- ======== ABOUT VIEW ======== -->
<view wx:elif="{{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-footer">
<text class="about-copyright">© 2026 Sundynix · All Rights Reserved</text>
</view>
</view>
</scroll-view>
</view>
<!-- ======== MAIN PROFILE VIEW ======== -->
<view wx:else class="main-profile-view"> <view wx:else class="main-profile-view">
<!-- Header -->
<view class="profile-header"> <view class="profile-header">
<view class="user-main"> <view class="user-main">
<view class="user-avatar"> <view class="user-avatar" bindtap="openProfileEditor">
<t-avatar image="https://api.dicebear.com/7.x/avataaars/svg?seed=Lucky" size="large" /> <t-avatar wx:if="{{userAvatar}}" image="{{userAvatar}}" size="120rpx" />
<t-avatar wx:else icon="user" size="120rpx" />
</view> </view>
<view class="user-text"> <view class="user-text" bindtap="openProfileEditor">
<text class="user-name">布偶猫园长</text> <view class="user-name">{{userName}}</view>
<t-tag theme="warning" variant="light" size="small">Lv.4 资深植人</t-tag> <view class="level-badge">Lv.4 资深植人</view>
</view> </view>
</view> </view>
<t-icon name="setting" size="48rpx" /> <view class="settings-btn" bindtap="goToNotificationSettings">
<t-icon name="setting" size="40rpx" color="#666" />
</view>
</view> </view>
<scroll-view scroll-y class="profile-content"> <!-- Stats Card (Fixed) -->
<view class="stats-grid"> <view class="stats-section">
<view class="stat-col"> <view class="stats-card">
<text class="stat-num">12</text> <view class="stat-item">
<text class="stat-num">{{plantCount}}</text>
<text class="stat-label">植物</text> <text class="stat-label">植物</text>
</view> </view>
<view class="stat-col"> <view class="stat-divider"></view>
<text class="stat-num">328</text> <view class="stat-item">
<text class="stat-num">{{taskDoneCount}}</text>
<text class="stat-label">养护</text> <text class="stat-label">养护</text>
</view> </view>
<view class="stat-col"> <view class="stat-divider"></view>
<text class="stat-num">15</text> <view class="stat-item">
<text class="stat-label">关注</text> <text class="stat-num">{{postCount}}</text>
<text class="stat-label">动态</text>
</view>
</view> </view>
</view> </view>
<scroll-view scroll-y class="profile-content" enhanced show-scrollbar="{{false}}">
<!-- Menu -->
<view class="profile-menu"> <view class="profile-menu">
<t-cell-group title="常用功能" theme="card"> <view class="menu-group-title">常用功能</view>
<t-cell title="我的收藏" hover arrow bind:tap="setView" data-view="favorites">
<t-icon slot="left-icon" name="star" color="#FFA000" style="margin-right: 16rpx;" />
</t-cell>
<t-cell title="我的发布" hover arrow bind:tap="setView" data-view="posts">
<t-icon slot="left-icon" name="file-copy" color="#1976D2" style="margin-right: 16rpx;" />
</t-cell>
<t-cell title="识别记录" hover arrow>
<t-icon slot="left-icon" name="scan" color="#388E3C" style="margin-right: 16rpx;" />
</t-cell>
<t-cell title="成就徽章" note="已获 3 个" hover arrow bind:tap="setView" data-view="badges">
<t-icon slot="left-icon" name="control-platform" color="#AB47BC" style="margin-right: 16rpx;" />
</t-cell>
</t-cell-group>
<t-cell-group title="更多服务" theme="card" style="margin-top: 24rpx;">
<t-cell title="帮助与反馈" hover arrow>
<t-icon slot="left-icon" name="help-circle" color="#757575" style="margin-right: 16rpx;" />
</t-cell>
</t-cell-group>
<view class="menu-item" bindtap="setView" data-view="favorites">
<view class="menu-left">
<view class="menu-icon-bg" style="background: #FFF3E0">
<t-icon name="star" size="36rpx" color="#FF9800" />
</view>
<text class="menu-text">我的收藏</text>
</view>
<t-icon name="chevron-right" size="36rpx" color="#ccc" />
</view> </view>
<!-- Spacer --> <view class="menu-item" bindtap="setView" data-view="posts">
<view style="height: 40rpx;"></view> <view class="menu-left">
<view class="menu-icon-bg" style="background: #E3F2FD">
<t-icon name="file-copy" size="36rpx" color="#2196F3" />
</view>
<text class="menu-text">我的发布</text>
</view>
<t-icon name="chevron-right" size="36rpx" color="#ccc" />
</view>
<view class="menu-item" bindtap="goToIdentifyHistory">
<view class="menu-left">
<view class="menu-icon-bg" style="background: #E8F5E9">
<t-icon name="scan" size="36rpx" color="#4CAF50" />
</view>
<text class="menu-text">识别记录</text>
</view>
<t-icon name="chevron-right" size="36rpx" color="#ccc" />
</view>
<view class="menu-item" bindtap="setView" data-view="badges">
<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-item" bindtap="goToAbout">
<view class="menu-left">
<view class="menu-icon-bg" style="background: #F5F5F5">
<t-icon name="help-circle" size="36rpx" color="#616161" />
</view>
<text class="menu-text">帮助与关于</text>
</view>
<t-icon name="chevron-right" size="36rpx" color="#ccc" />
</view>
</view>
<view style="height: 100rpx;"></view>
</scroll-view> </scroll-view>
</view> </view>
<!-- Profile Edit Popup -->
<t-popup visible="{{showProfileEditor}}" placement="bottom" bind:visible-change="onProfilePopupChange">
<view class="profile-edit-popup">
<view class="popup-header">
<text class="popup-title">编辑资料</text>
<view class="popup-close" bindtap="closeProfileEditor">
<t-icon name="close" size="40rpx" color="#999" />
</view>
</view>
<view class="edit-row" bindtap="onChooseAvatar">
<text class="edit-row-label">头像</text>
<view class="edit-row-right">
<t-avatar
wx:if="{{tempAvatar || userAvatar}}"
image="{{tempAvatar || userAvatar}}"
size="96rpx"
/>
<t-avatar wx:else icon="user" size="96rpx" />
<t-icon name="chevron-right" size="32rpx" color="#C5C5C5" />
</view>
</view>
<view class="edit-row">
<text class="edit-row-label">昵称</text>
<input
class="nickname-input"
type="text"
placeholder="请输入昵称"
placeholder-style="color: #C5C5C5;"
value="{{tempNickname}}"
bindinput="onNicknameInput"
/>
</view>
<view class="edit-actions">
<t-button theme="primary" block shape="round" bind:tap="saveProfile">保存</t-button>
</view>
</view>
</t-popup>
</view> </view>
+578 -104
View File
@@ -1,15 +1,49 @@
/** pages/profile/index.wxss **/ /** pages/profile/index.wxss **/
.profile-page { .profile-page {
background-color: #F4F6F0; background: #F4F6F0;
min-height: 100vh; height: 100vh;
position: relative; display: flex;
flex-direction: column;
overflow: hidden; overflow: hidden;
display: flex; flex-direction: column;
} }
/* Animations */ /* ======== Sub-view Navigation ======== */
.sub-view {
background: #F4F6F0;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
.sub-nav {
display: flex;
align-items: center;
gap: 12rpx;
padding: 30rpx 24rpx;
background: #fff;
font-size: 34rpx;
font-weight: 700;
color: #111827;
position: sticky;
top: 0;
z-index: 100;
}
.sub-nav-title {
margin-left: 8rpx;
}
.sub-scroll {
flex: 1;
padding: 24rpx;
box-sizing: border-box;
padding-bottom: 80rpx;
}
.info-view-anim { .info-view-anim {
animation: slideInRight 0.3s cubic-bezier(0.25, 1, 0.5, 1); animation: slideInRight 0.3s cubic-bezier(0.16, 1, 0.3, 1);
} }
@keyframes slideInRight { @keyframes slideInRight {
@@ -17,133 +51,573 @@
to { transform: translateX(0); opacity: 1; } to { transform: translateX(0); opacity: 1; }
} }
.sticky-nav { /* ======== Category Filter (Custom Chips) ======== */
position: sticky; top: 0; z-index: 100; background: white; .category-filter {
border-bottom: 2rpx solid #f0f0f0; display: flex;
padding: 20rpx; gap: 16rpx;
padding: 0 24rpx 16rpx;
background: #fff;
margin-bottom: 16rpx;
} }
.tab-content { padding: 32rpx; } .filter-chip {
padding: 8rpx 24rpx;
background: #fff;
border: 2rpx solid #E5E7EB;
border-radius: 40rpx;
font-size: 26rpx;
color: #6B7280;
transition: all 0.2s;
}
/* Favorites Grid */ .filter-chip.active {
background: #333;
color: #fff;
border-color: #333;
}
/* ======== Favorites Grid ======== */
.fav-grid { .fav-grid {
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: 1fr 1fr;
gap: 24rpx; gap: 20rpx;
margin-top: 24rpx;
} }
.fav-card { .fav-card {
background: white; background: white;
border-radius: 24rpx; border-radius: 20rpx;
overflow: hidden; overflow: hidden;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.03); box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.03);
} }
.fav-img { width: 100%; display: block; background: #f0f0f0; } .fav-img {
background: #f0f0f0;
.fav-info { padding: 20rpx; }
.fav-name { font-size: 28rpx; font-weight: 700; color: #37474F; margin-bottom: 12rpx; display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.fav-meta-row { display: flex; align-items: center; gap: 8rpx; }
.fav-type { font-size: 20rpx; color: #90A4AE; }
.empty-state {
display: flex; flex-direction: column; align-items: center; justify-content: center;
padding: 80rpx 0; color: #B0BEC5; font-size: 28rpx;
} }
/* Posts View */ .fav-info {
.my-posts-list { padding: 40rpx; } padding: 16rpx 20rpx;
.my-post-card { display: flex; gap: 24rpx; margin-bottom: 48rpx; position: relative; }
.my-post-time { font-size: 24rpx; color: #B0BEC5; width: 140rpx; flex-shrink: 0; text-align: right; }
.my-post-content-wrap { flex: 1; border-left: 4rpx solid #ECEFF1; padding-left: 24rpx; padding-bottom: 24rpx; }
.my-post-images { margin: 16rpx 0; white-space: nowrap; overflow-x: auto; }
.my-post-footer { display: flex; gap: 32rpx; margin-top: 16rpx; }
.footer-item { display: flex; align-items: center; gap: 8rpx; font-size: 24rpx; color: #78909C; }
/* Draft Card */
.draft-card {
background: #FFFDE7;
border-radius: 16rpx;
padding: 20rpx;
margin-left: 0;
} }
.draft-card .my-post-content-wrap { .fav-name {
border-left: 4rpx solid #FFC107; display: block;
} font-size: 28rpx;
.draft-badge {
position: absolute;
top: 0;
left: 0;
background: #FFC107;
color: #fff;
font-size: 20rpx;
padding: 4rpx 16rpx;
border-radius: 8rpx 0 8rpx 0;
font-weight: 600; font-weight: 600;
color: #1F2937;
margin-bottom: 8rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
/* Action Buttons */ .fav-meta-row {
.edit-btn, .delete-btn { display: flex;
cursor: pointer; align-items: center;
gap: 8rpx;
} }
.edit-btn:active, .delete-btn:active { .fav-type {
opacity: 0.7; font-size: 22rpx;
color: #9CA3AF;
} }
/* Badges View */ /* ======== Posts Styles (Refined) ======== */
.badges-content { padding: 40rpx; background: white; height: 100%; } .my-post-card {
display: flex;
gap: 24rpx;
margin-bottom: 32rpx;
}
.my-post-time {
width: 100rpx;
font-size: 24rpx;
color: #9CA3AF;
font-weight: 500;
padding-top: 8rpx;
flex-shrink: 0;
text-align: right;
}
.my-post-content-wrap {
flex: 1;
background: #fff;
padding: 24rpx;
border-radius: 20rpx;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.02);
}
.post-text {
font-size: 28rpx;
line-height: 1.5;
color: #1F2937;
margin-bottom: 16rpx;
display: block;
}
.my-post-images {
display: flex;
gap: 12rpx;
margin-bottom: 16rpx;
flex-wrap: wrap;
}
.my-post-footer {
display: flex;
align-items: center;
gap: 32rpx;
border-top: 1rpx solid #F3F4F6;
padding-top: 16rpx;
}
.footer-item {
display: flex;
align-items: center;
gap: 8rpx;
font-size: 24rpx;
color: #9CA3AF;
}
/* ======== Badges View ======== */
.level-card-large { .level-card-large {
background: linear-gradient(135deg, #FFF3E0 0%, #FFE0B2 100%); background: linear-gradient(135deg, #2c3e50 0%, #4ca1af 100%);
border-radius: 40rpx; border-radius: 40rpx;
padding: 48rpx;
margin-bottom: 60rpx;
color: #E65100;
}
.level-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 40rpx; }
.level-label { font-size: 26rpx; opacity: 0.8; display: block; }
.level-value { font-size: 48rpx; font-weight: 800; display: block; }
.level-progress-section { }
.progress-text { display: flex; justify-content: space-between; font-size: 24rpx; font-weight: 600; margin-bottom: 12rpx; }
.next-level-tip { font-size: 22rpx; margin-top: 16rpx; display: block; opacity: 0.8; }
.section-title-badges { font-size: 32rpx; font-weight: 700; color: #333; margin-bottom: 32rpx; }
/* Basic TDesign Grid Item styling override if needed */
.t-grid-item__content { padding: 24rpx 0 !important; }
/* Main Profile */
.main-profile-view { display: flex; flex-direction: column; height: 100%; }
.profile-header {
padding: 40rpx 48rpx;
background: white;
display: flex; justify-content: space-between; align-items: flex-start;
}
.user-main { display: flex; align-items: center; gap: 32rpx; }
.user-text { display: flex; flex-direction: column; gap: 12rpx; }
.user-name { font-size: 40rpx; font-weight: 800; color: var(--text-main); }
.stats-grid {
display: flex; justify-content: space-around;
padding: 40rpx; padding: 40rpx;
margin: 24rpx 40rpx; color: white;
background: white; margin-bottom: 48rpx;
border-radius: 32rpx; box-shadow: 0 20rpx 40rpx rgba(44, 62, 80, 0.2);
position: relative;
overflow: hidden;
}
.level-card-bg {
position: absolute;
top: -100rpx;
right: -100rpx;
width: 300rpx;
height: 300rpx;
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
border-radius: 50%;
}
.level-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 40rpx;
position: relative;
z-index: 2;
}
.level-info-large {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.level-label {
font-size: 24rpx;
opacity: 0.8;
letter-spacing: 2rpx;
text-transform: uppercase;
}
.level-value {
font-size: 48rpx;
font-weight: 800;
}
.level-progress-section {
position: relative;
z-index: 2;
}
.progress-text {
display: flex;
justify-content: space-between;
font-size: 26rpx;
margin-bottom: 16rpx;
font-weight: 600;
opacity: 0.9;
}
.level-progress-bar-bg {
height: 16rpx;
background: rgba(0,0,0,0.2);
border-radius: 8rpx;
margin-bottom: 24rpx;
border: 2rpx solid rgba(255,255,255,0.1);
}
.level-progress-bar-fill {
height: 100%;
background: linear-gradient(90deg, #FFD700, #FDB931);
border-radius: 8rpx;
box-shadow: 0 0 12rpx rgba(255, 215, 0, 0.4);
}
.next-level-tip {
font-size: 24rpx;
color: rgba(255,255,255,0.7);
display: block;
text-align: right;
}
.section-title-badges {
font-size: 32rpx;
font-weight: 700;
color: #1F2937;
margin-bottom: 32rpx;
margin-left: 12rpx;
}
.badges-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20rpx;
}
.badge-item {
background: #fff;
border-radius: 24rpx;
padding: 32rpx 16rpx;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.02); box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.02);
} }
.stat-col { display: flex; flex-direction: column; align-items: center; gap: 4rpx; } .badge-item.locked {
.stat-num { font-size: 36rpx; font-weight: 800; color: var(--text-main); } background: #F8F9FA;
.stat-label { font-size: 22rpx; color: #90A4AE; } border: 2rpx dashed #E5E7EB;
box-shadow: none;
.profile-menu { }
padding: 0 32rpx;
.badge-icon-circle {
width: 88rpx;
height: 88rpx;
border-radius: 30rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20rpx;
}
.badge-name {
font-size: 26rpx;
font-weight: 700;
color: #374151;
margin-bottom: 6rpx;
}
.badge-desc {
font-size: 20rpx;
color: #9CA3AF;
}
.badge-progress {
margin-top: 12rpx;
font-size: 20rpx;
background: #F3F4F6;
padding: 4rpx 12rpx;
border-radius: 12rpx;
color: #6B7280;
}
/* ======== Main Profile View ======== */
.main-profile-view {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
height: 0;
}
.profile-header {
background: linear-gradient(180deg, #E8F5E9 0%, #FFFFFF 100%);
padding: 32rpx 40rpx;
/* Extra padding top handled by structure relative to status bar usually,
but standard padding is fine here */
display: flex;
justify-content: space-between;
align-items: center;
border-bottom-left-radius: 48rpx;
border-bottom-right-radius: 48rpx;
box-shadow: 0 8rpx 30rpx rgba(0,0,0,0.02);
margin-bottom: 24rpx;
flex-shrink: 0;
}
.user-main {
display: flex;
align-items: center;
gap: 32rpx;
}
.user-avatar {
width: 128rpx;
height: 128rpx;
border-radius: 50%;
border: 6rpx solid #fff;
box-shadow: 0 8rpx 20rpx rgba(0,0,0,0.08);
overflow: hidden;
}
.user-text {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.user-name {
font-size: 40rpx;
font-weight: 800;
color: #1F2937;
}
.level-badge {
align-self: flex-start;
font-size: 22rpx;
background: #DCEDC8;
color: #33691E;
padding: 6rpx 16rpx;
border-radius: 20rpx;
font-weight: 600;
}
.settings-btn {
padding: 16rpx;
}
.profile-content {
flex: 1;
height: 0;
padding: 0 32rpx;
padding-bottom: 120rpx;
box-sizing: border-box;
}
/* Hide all scrollbars globally on this page */
.profile-page ::-webkit-scrollbar {
display: none !important;
width: 0 !important;
height: 0 !important;
}
.profile-page scroll-view {
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 {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.menu-group-title {
font-size: 26rpx;
color: #9CA3AF;
font-weight: 600;
margin-left: 12rpx;
}
.menu-item {
background: #fff;
padding: 32rpx;
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.015);
}
.menu-item:active {
background: #FAFAFA;
}
.menu-left {
display: flex;
align-items: center;
gap: 24rpx;
}
.menu-icon-bg {
width: 72rpx;
height: 72rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
}
.menu-text {
font-size: 30rpx;
font-weight: 600;
color: #374151;
}
.menu-right-info {
display: flex;
align-items: center;
gap: 8rpx;
}
.menu-badge-text {
font-size: 26rpx;
color: #6B7280;
}
/* Edit Popup Styles */
.profile-edit-popup {
background: #fff;
border-radius: 40rpx 40rpx 0 0;
padding: 0 48rpx;
padding-bottom: calc(48rpx + env(safe-area-inset-bottom));
}
.popup-header {
display: flex;
justify-content: center;
align-items: center;
padding: 40rpx 0 20rpx;
position: relative;
}
.popup-title {
font-size: 36rpx;
font-weight: 800;
color: #111827;
}
.popup-close {
position: absolute;
right: 0;
top: 40rpx;
}
/* Edit Form Rows */
.edit-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx 0;
border-bottom: 2rpx solid #F3F4F6;
}
.edit-row-label {
font-size: 32rpx;
font-weight: 600;
color: #374151;
flex-shrink: 0;
}
.edit-row-right {
display: flex;
align-items: center;
gap: 16rpx;
}
.nickname-input {
flex: 1;
font-size: 32rpx;
font-weight: 500;
color: #111827;
text-align: right;
min-width: 0;
}
.edit-actions {
padding-top: 60rpx;
padding-bottom: 20rpx;
}
/* About Section */
.about-section {
padding: 24rpx;
}
.about-logo-area {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 60rpx;
margin-top: 40rpx;
}
.about-logo {
width: 160rpx;
height: 160rpx;
background: linear-gradient(135deg, #E8F5E9 0%, #C8E6C9 100%);
border-radius: 48rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 12rpx 32rpx rgba(85, 139, 47, 0.1);
margin-bottom: 32rpx;
}
.about-app-name {
font-size: 40rpx;
font-weight: 700;
color: #1F2937;
letter-spacing: 2rpx;
}
.about-version {
font-size: 24rpx;
color: #9CA3AF;
margin-top: 8rpx;
}
.about-desc {
background: #fff;
padding: 40rpx;
border-radius: 24rpx;
font-size: 30rpx;
line-height: 1.7;
color: #4B5563;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.02);
margin-bottom: 40rpx;
} }
+1 -1
View File
@@ -3,7 +3,7 @@
height: 100vh; height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background-color: #F8F9FA; background-color: #F4F6F0;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
+37 -45
View File
@@ -5,63 +5,75 @@ Page({
data: { data: {
plant: null, plant: null,
activeImageIndex: 0, activeImageIndex: 0,
swiperList: [] swiperList: [],
}, },
onLoad(options) { onLoad(options) {
const eventChannel = this.getOpenerEventChannel();
let loadedFromEvent = false;
if (eventChannel && eventChannel.on) {
eventChannel.on('acceptDataFromOpenerPage', (res) => {
if (res.data) {
this.setPlantData(res.data);
loadedFromEvent = true;
}
});
}
if (options.id) { if (options.id) {
// Give event channel a chance to fire
setTimeout(() => {
if (!loadedFromEvent && !this.data.plant) {
this.loadPlantDetail(options.id); this.loadPlantDetail(options.id);
} }
}, 100);
}
}, },
loadPlantDetail(id) { loadPlantDetail(id) {
// Fetch detail via wiki/page with specific ID request.get('/wiki/detail', { id: id }).then(res => {
// Since there's no /wiki/detail endpoint, we use /wiki/page to get it const item = res || null;
request.post('/wiki/page', {
current: 1,
pageSize: 1,
id: id
}).then(res => {
const data = res || {};
const list = data.list || [];
const item = list.length > 0 ? list[0] : null;
if (!item) { if (!item) {
wx.showToast({ title: '未找到该植物', icon: 'none' }); wx.showToast({ title: '未找到该植物', icon: 'none' });
return; return;
} }
// Set Page Title this.setPlantData(item);
}).catch(err => {
console.error('Load plant detail failed', err);
wx.showToast({ title: '加载失败', icon: 'none' });
});
},
setPlantData(item) {
if (!item) return;
wx.setNavigationBarTitle({ title: item.name }); wx.setNavigationBarTitle({ title: item.name });
// Prepare swiper list from imgList // Prepare swiper list
const swiperList = (item.imgList || []).map(img => img.url); const swiperList = (item.imgList || []).map(img => img.url);
// Parse pest/disease list // Parse lists
const commonPests = item.pestsDiseases const commonPests = item.pestsDiseases
? item.pestsDiseases.split(',').map(s => s.trim()).filter(Boolean) ? item.pestsDiseases.split(',').map(s => s.trim()).filter(Boolean)
: []; : [];
// Parse aliases
const aliasesList = item.aliases const aliasesList = item.aliases
? item.aliases.split(/[,,、]/).map(s => s.trim()).filter(Boolean) ? item.aliases.split(/[,,、]/).map(s => s.trim()).filter(Boolean)
: []; : [];
// Parse reproduction methods
const reproductionList = item.reproductionMethod const reproductionList = item.reproductionMethod
? item.reproductionMethod.split(/[,,、]/).map(s => s.trim()).filter(Boolean) ? item.reproductionMethod.split(/[,,、]/).map(s => s.trim()).filter(Boolean)
: []; : [];
// Difficulty label
const diffLabels = { 1: '简单', 2: '中等', 3: '较难', 4: '困难', 5: '专家' }; const diffLabels = { 1: '简单', 2: '中等', 3: '较难', 4: '困难', 5: '专家' };
// Map API data to display model
const plant = { const plant = {
id: item.id, id: item.id,
name: item.name, name: item.name,
latinName: item.latinName || '', latinName: item.latinName || '',
aliases: item.aliases || '', aliases: item.aliases || '',
aliasesList: aliasesList, aliasesList,
genus: item.genus || '', genus: item.genus || '',
distributionArea: item.distributionArea || '', distributionArea: item.distributionArea || '',
difficulty: item.difficulty || 0, difficulty: item.difficulty || 0,
@@ -70,49 +82,29 @@ Page({
lifeCycle: item.lifeCycle || '', lifeCycle: item.lifeCycle || '',
growthHabit: item.growthHabit || '', growthHabit: item.growthHabit || '',
reproductionMethod: item.reproductionMethod || '', reproductionMethod: item.reproductionMethod || '',
reproductionList: reproductionList, reproductionList,
// Light
lightIntensity: item.lightIntensity || '', lightIntensity: item.lightIntensity || '',
lightType: item.lightType || '', lightType: item.lightType || '',
// Temperature
optimalTempPeriod: item.optimalTempPeriod || '', optimalTempPeriod: item.optimalTempPeriod || '',
// Morphology
stem: item.stem || '', stem: item.stem || '',
foliageType: item.foliageType || '', foliageType: item.foliageType || '',
foliageColor: item.foliageColor || '', foliageColor: item.foliageColor || '',
foliageShape: item.foliageShape || '', foliageShape: item.foliageShape || '',
height: item.height || 0, height: item.height || 0,
// Flowering
floweringPeriod: item.floweringPeriod || '', floweringPeriod: item.floweringPeriod || '',
floweringColor: item.floweringColor || '', floweringColor: item.floweringColor || '',
floweringShape: item.floweringShape || '', floweringShape: item.floweringShape || '',
flowerDiameter: item.flowerDiameter || 0, flowerDiameter: item.flowerDiameter || 0,
// Fruit
fruit: item.fruit || '', fruit: item.fruit || '',
// Pests
pestsDiseases: item.pestsDiseases || '', pestsDiseases: item.pestsDiseases || '',
commonPests: commonPests, commonPests,
// Classes
classes: (item.classes || []).map(c => c.name), classes: (item.classes || []).map(c => c.name),
// Images
imgList: item.imgList || [] imgList: item.imgList || []
}; };
this.setData({ this.setData({
plant: plant, plant,
swiperList: swiperList swiperList
});
}).catch(err => {
console.error('Load plant detail failed', err);
wx.showToast({ title: '加载失败', icon: 'none' });
}); });
}, },
+42 -59
View File
@@ -1,55 +1,45 @@
<!--pages/wiki/detail/index.wxml--> <!--pages/wiki/detail/index.wxml-->
<view class="wiki-detail" wx:if="{{plant}}"> <view class="wiki-detail" wx:if="{{plant}}">
<!-- Header Area --> <!-- Image Carousel -->
<view class="wd-header"> <view class="wd-header">
<view class="wd-gallery-container">
<t-swiper <t-swiper
t-class="custom-swiper" t-class="custom-swiper"
current="{{activeImageIndex}}" current="{{activeImageIndex}}"
bind:change="onSwiperChange" bind:change="onSwiperChange"
height="500rpx" height="480rpx"
list="{{swiperList}}" list="{{swiperList}}"
navigation="{{ { type: '' } }}" navigation="{{ { type: '' } }}"
/> />
<view class="wd-gradient-overlay"></view> <view class="wd-counter" wx:if="{{swiperList.length > 0}}">
<text>{{activeImageIndex + 1}} / {{swiperList.length}}</text>
</view>
</view> </view>
<!-- Custom Indicators --> <!-- Plant Name Card -->
<view class="wd-indicators" wx:if="{{swiperList.length > 1}}"> <view class="wd-name-card">
<view <view class="wd-name-row">
wx:for="{{swiperList}}"
wx:key="index"
class="wd-dot {{index === activeImageIndex ? 'active' : ''}}"
></view>
</view>
<!-- Header Overlay Info -->
<view class="wd-overlay">
<view class="wd-title">
<text class="wd-name">{{plant.name}}</text> <text class="wd-name">{{plant.name}}</text>
<text class="wd-scientific">{{plant.latinName}}</text> <text class="wd-scientific" wx:if="{{plant.latinName}}">{{plant.latinName}}</text>
</view> </view>
<view class="wd-badges"> <view class="wd-badges" wx:if="{{plant.genus || plant.classes.length > 0}}">
<text class="wd-badge" wx:if="{{plant.genus}}">{{plant.genus}}</text> <text class="wd-badge" wx:if="{{plant.genus}}">{{plant.genus}}</text>
<text class="wd-badge" wx:for="{{plant.classes}}" wx:key="*this">{{item}}</text> <text class="wd-badge" wx:for="{{plant.classes}}" wx:key="*this">{{item}}</text>
<text class="wd-badge">难度: {{plant.difficultyLabel}}</text> <text class="wd-badge difficulty">难度: {{plant.difficultyLabel}}</text>
</view>
</view> </view>
</view> </view>
<!-- Content Area --> <!-- Content Scroll -->
<view class="wd-content-wrapper">
<scroll-view class="wd-content" scroll-y enhanced show-scrollbar="{{false}}"> <scroll-view class="wd-content" scroll-y enhanced show-scrollbar="{{false}}">
<!-- Growth Habit Section --> <!-- Growth Habit -->
<section class="wd-section" wx:if="{{plant.growthHabit}}"> <view class="wd-section" wx:if="{{plant.growthHabit}}">
<view class="wd-card"> <view class="wd-card">
<text class="wd-text">{{plant.growthHabit}}</text> <text class="wd-text">{{plant.growthHabit}}</text>
</view> </view>
</section> </view>
<!-- Basic Info Section --> <!-- Basic Info -->
<section class="wd-section"> <view class="wd-section">
<view class="section-title"> <view class="section-title">
<t-icon name="info-circle" size="40rpx" color="#558B2F" /> <t-icon name="info-circle" size="40rpx" color="#558B2F" />
<text>基础档案</text> <text>基础档案</text>
@@ -74,40 +64,35 @@
</view> </view>
</view> </view>
</view> </view>
</section> </view>
<!-- Care Guide Section --> <!-- Care Guide -->
<section class="wd-section"> <view class="wd-section">
<view class="section-title"> <view class="section-title">
<t-icon name="sunny" size="40rpx" color="#558B2F" /> <t-icon name="sunny" size="40rpx" color="#558B2F" />
<text>养护指南</text> <text>养护指南</text>
</view> </view>
<view class="wd-card"> <view class="wd-card">
<!-- Light -->
<view class="requirement-item" wx:if="{{plant.lightIntensity}}"> <view class="requirement-item" wx:if="{{plant.lightIntensity}}">
<view class="req-icon"> <view class="req-icon light">
<t-icon name="sunny" size="40rpx" color="#558B2F" /> <t-icon name="sunny" size="40rpx" color="#F59E0B" />
</view> </view>
<view class="req-content"> <view class="req-content">
<text class="req-title">光照</text> <text class="req-title">光照</text>
<text class="req-desc">{{plant.lightIntensity}}</text> <text class="req-desc">{{plant.lightIntensity}}</text>
</view> </view>
</view> </view>
<!-- Temperature -->
<view class="requirement-item" wx:if="{{plant.optimalTempPeriod}}"> <view class="requirement-item" wx:if="{{plant.optimalTempPeriod}}">
<view class="req-icon"> <view class="req-icon temp">
<t-icon name="pin" size="40rpx" color="#558B2F" /> <t-icon name="pin" size="40rpx" color="#EF4444" />
</view> </view>
<view class="req-content"> <view class="req-content">
<text class="req-title">适宜温度</text> <text class="req-title">适宜温度</text>
<text class="req-desc">{{plant.optimalTempPeriod}}</text> <text class="req-desc">{{plant.optimalTempPeriod}}</text>
</view> </view>
</view> </view>
<!-- Reproduction -->
<view class="requirement-item" wx:if="{{plant.reproductionMethod}}"> <view class="requirement-item" wx:if="{{plant.reproductionMethod}}">
<view class="req-icon"> <view class="req-icon repro">
<t-icon name="fork-node" size="40rpx" color="#558B2F" /> <t-icon name="fork-node" size="40rpx" color="#558B2F" />
</view> </view>
<view class="req-content"> <view class="req-content">
@@ -116,10 +101,10 @@
</view> </view>
</view> </view>
</view> </view>
</section> </view>
<!-- Morphology Section --> <!-- Morphology -->
<section class="wd-section" wx:if="{{plant.stem || plant.foliageShape || plant.foliageColor}}"> <view class="wd-section" wx:if="{{plant.stem || plant.foliageShape || plant.foliageColor}}">
<view class="section-title"> <view class="section-title">
<t-icon name="tree-round-dot" size="40rpx" color="#558B2F" /> <t-icon name="tree-round-dot" size="40rpx" color="#558B2F" />
<text>形态特征</text> <text>形态特征</text>
@@ -144,10 +129,10 @@
</view> </view>
</view> </view>
</view> </view>
</section> </view>
<!-- Flowering Section --> <!-- Flowering -->
<section class="wd-section" wx:if="{{plant.floweringPeriod || plant.floweringColor}}"> <view class="wd-section" wx:if="{{plant.floweringPeriod || plant.floweringColor}}">
<view class="section-title"> <view class="section-title">
<t-icon name="flower" size="40rpx" color="#558B2F" /> <t-icon name="flower" size="40rpx" color="#558B2F" />
<text>开花信息</text> <text>开花信息</text>
@@ -172,10 +157,10 @@
</view> </view>
</view> </view>
</view> </view>
</section> </view>
<!-- Fruit Section --> <!-- Fruit -->
<section class="wd-section" wx:if="{{plant.fruit}}"> <view class="wd-section" wx:if="{{plant.fruit}}">
<view class="section-title"> <view class="section-title">
<t-icon name="apple" size="40rpx" color="#558B2F" /> <t-icon name="apple" size="40rpx" color="#558B2F" />
<text>果实</text> <text>果实</text>
@@ -183,12 +168,12 @@
<view class="wd-card"> <view class="wd-card">
<text class="wd-text">{{plant.fruit}}</text> <text class="wd-text">{{plant.fruit}}</text>
</view> </view>
</section> </view>
<!-- Pests & Diseases Section --> <!-- Pests -->
<section class="wd-section" wx:if="{{plant.commonPests.length > 0}}"> <view class="wd-section" wx:if="{{plant.commonPests.length > 0}}">
<view class="section-title"> <view class="section-title">
<t-icon name="error-circle" size="40rpx" color="#558B2F" /> <t-icon name="error-circle" size="40rpx" color="#EF4444" />
<text>常见病虫害</text> <text>常见病虫害</text>
</view> </view>
<view class="wd-card"> <view class="wd-card">
@@ -196,15 +181,13 @@
<text wx:for="{{plant.commonPests}}" wx:key="*this" class="pest-tag">{{item}}</text> <text wx:for="{{plant.commonPests}}" wx:key="*this" class="pest-tag">{{item}}</text>
</view> </view>
</view> </view>
</section>
<!-- Bottom Spacer -->
<view style="height: 100rpx;"></view>
</scroll-view>
</view> </view>
<view style="height: 120rpx;"></view>
</scroll-view>
</view> </view>
<!-- Loading State --> <!-- Loading -->
<view wx:if="{{!plant}}" class="wiki-detail-loading"> <view wx:if="{{!plant}}" class="wiki-detail-loading">
<t-loading theme="circular" size="64rpx" text="加载中..." /> <t-loading theme="circular" size="64rpx" text="加载中..." />
</view> </view>
+109 -187
View File
@@ -3,201 +3,139 @@
height: 100vh; height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background: #F9FAFB; background: #F4F6F0;
} }
/* Page Layout */
page { page {
height: 100vh; height: 100vh;
overflow: hidden; overflow: hidden;
} }
/* Hide Scrollbar Globally */ /* ======== Image Carousel ======== */
::-webkit-scrollbar {
display: none !important;
width: 0 !important;
height: 0 !important;
color: transparent !important;
}
scroll-view ::-webkit-scrollbar {
display: none !important;
width: 0 !important;
height: 0 !important;
color: transparent !important;
}
/* Header Area */
.wd-header { .wd-header {
height: 500rpx;
position: relative; position: relative;
background: #000; flex-shrink: 0;
background: #E8E8E8;
} }
/* Content Wrapper handles the flex growth and positioning overlap */
.wd-content-wrapper {
flex: 1;
position: relative;
margin-top: -32rpx;
z-index: 20;
overflow: hidden; /* Ensure scroll-view is contained */
}
/* Content Scroll View fills the wrapper */
.wd-content {
width: 100%;
height: 100%;
}
/* Force override TDesign swiper radius */
.custom-swiper { .custom-swiper {
border-radius: 0 !important; border-radius: 0 !important;
overflow: hidden;
--td-swiper-radius: 0px !important; --td-swiper-radius: 0px !important;
} }
.custom-swiper .t-swiper { .wd-counter {
border-radius: 0 !important;
}
t-swiper {
border-radius: 0 !important;
}
.wd-gallery-container {
width: 100%;
height: 100%;
position: relative;
}
.wd-gradient-overlay {
position: absolute; position: absolute;
top: 0; bottom: 20rpx;
left: 0; right: 24rpx;
right: 0; background: rgba(0, 0, 0, 0.45);
height: 240rpx; backdrop-filter: blur(6px);
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.7), transparent); -webkit-backdrop-filter: blur(6px);
pointer-events: none; color: white;
font-size: 22rpx;
font-weight: 600;
padding: 6rpx 18rpx;
border-radius: 12rpx;
z-index: 20;
letter-spacing: 2rpx;
}
/* ======== Name Card ======== */
.wd-name-card {
background: white;
padding: 32rpx 36rpx 28rpx;
margin: 0 0 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
position: relative;
z-index: 10; z-index: 10;
} }
.wd-indicators { .wd-name-row {
position: absolute; margin-bottom: 16rpx;
bottom: 24rpx;
right: 24rpx;
display: flex;
gap: 12rpx;
z-index: 20;
}
.wd-dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.4);
transition: all 0.3s;
}
.wd-dot.active {
background: white;
width: 24rpx;
border-radius: 8rpx;
}
.wd-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
padding: 32rpx;
display: flex;
flex-direction: column;
justify-content: flex-start;
pointer-events: none;
z-index: 20;
} }
.wd-name { .wd-name {
font-size: 56rpx; display: block;
color: #FFFFFF; font-size: 48rpx;
font-weight: 800; font-weight: 800;
margin-bottom: 8rpx; color: #1F2937;
line-height: 1.3;
margin-bottom: 6rpx;
} }
.wd-scientific { .wd-scientific {
font-size: 32rpx; display: block;
color: rgba(255, 255, 255, 0.8); font-size: 28rpx;
color: #9CA3AF;
font-style: italic; font-style: italic;
font-family: serif; font-family: Georgia, 'Times New Roman', serif;
margin-bottom: 24rpx;
} }
.wd-badges { .wd-badges {
display: flex; display: flex;
gap: 16rpx; gap: 12rpx;
flex-wrap: wrap; flex-wrap: wrap;
} }
.wd-badge { .wd-badge {
background: rgba(255, 255, 255, 0.2); background: #F0F7EB;
backdrop-filter: blur(4px); color: #558B2F;
padding: 8rpx 20rpx; padding: 8rpx 20rpx;
border-radius: 24rpx; border-radius: 20rpx;
font-size: 24rpx; font-size: 22rpx;
color: #FFFFFF; font-weight: 600;
border: 1px solid rgba(255, 255, 255, 0.2);
} }
.wd-badge.difficulty {
background: #FFF8E1;
color: #F57F17;
}
/* ======== Scrollable Content ======== */
.wd-content {
flex: 1;
width: 100%;
height: 0;
}
/* ======== Sections ======== */
.wd-section { .wd-section {
margin-bottom: 32rpx; margin-bottom: 28rpx;
animation: fadeIn 0.5s ease-out; padding: 0 28rpx;
padding: 0 32rpx;
}
/* First section specific override: Adjust padding and background to create the seamless rounded look */
.wd-section:first-child {
padding: 0;
margin-bottom: 48rpx;
}
.wd-section:first-child .wd-card {
border-top-left-radius: 40rpx;
border-top-right-radius: 40rpx;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
padding: 48rpx 32rpx;
box-shadow: none; /* Seamless blend */
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20rpx); }
to { opacity: 1; transform: translateY(0); }
} }
.section-title { .section-title {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 16rpx; gap: 14rpx;
margin-bottom: 24rpx; margin-bottom: 20rpx;
padding-left: 8rpx; padding-left: 4rpx;
} }
.section-title text { .section-title text {
font-size: 34rpx; font-size: 32rpx;
font-weight: 700; font-weight: 700;
color: #111827; color: #1F2937;
}
/* ======== Cards ======== */
.wd-card {
background: white;
border-radius: 24rpx;
padding: 28rpx 32rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.03);
} }
.wd-text { .wd-text {
font-size: 30rpx; font-size: 28rpx;
line-height: 1.6; line-height: 1.7;
color: #4B5563; color: #4B5563;
} }
/* ======== Info Grid ======== */
.wd-grid { .wd-grid {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
gap: 32rpx; gap: 28rpx 32rpx;
} }
.wd-stat-item { .wd-stat-item {
@@ -207,62 +145,72 @@ t-swiper {
} }
.wd-label { .wd-label {
font-size: 24rpx; font-size: 22rpx;
color: #9CA3AF; color: #9CA3AF;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 1rpx; letter-spacing: 1rpx;
font-weight: 500;
} }
.wd-value { .wd-value {
font-size: 28rpx; font-size: 28rpx;
font-weight: 600; font-weight: 600;
color: #1F2937; color: #1F2937;
line-height: 1.4;
} }
.wd-card { /* ======== Requirement Items ======== */
background: white;
border-radius: 24rpx;
padding: 24rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.02);
transition: all 0.3s;
}
/* Requirement Items (Compact) */
.requirement-item { .requirement-item {
display: flex; display: flex;
gap: 24rpx; gap: 24rpx;
margin-bottom: 24rpx; padding: 24rpx 0;
padding-bottom: 24rpx;
border-bottom: 2rpx solid #F3F4F6; border-bottom: 2rpx solid #F3F4F6;
} }
.requirement-item:first-child {
padding-top: 0;
}
.requirement-item:last-child { .requirement-item:last-child {
margin-bottom: 0;
padding-bottom: 0; padding-bottom: 0;
border-bottom: none; border-bottom: none;
} }
.req-icon { .req-icon {
width: 80rpx; width: 84rpx;
height: 80rpx; height: 84rpx;
background: #F1F8E9; border-radius: 22rpx;
border-radius: 20rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
flex-shrink: 0; flex-shrink: 0;
} }
.req-icon.light {
background: #FFFBEB;
}
.req-icon.temp {
background: #FEF2F2;
}
.req-icon.repro {
background: #F0FDF4;
}
.req-content { .req-content {
flex: 1; flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
gap: 6rpx;
} }
.req-title { .req-title {
display: block; display: block;
font-size: 30rpx; font-size: 28rpx;
font-weight: 700; font-weight: 700;
color: #1F2937; color: #1F2937;
margin-bottom: 8rpx;
} }
.req-desc { .req-desc {
@@ -271,7 +219,7 @@ t-swiper {
line-height: 1.5; line-height: 1.5;
} }
/* FAQ / Pests */ /* ======== Pest Tags ======== */
.pest-tags { .pest-tags {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -283,41 +231,15 @@ t-swiper {
color: #DC2626; color: #DC2626;
padding: 12rpx 24rpx; padding: 12rpx 24rpx;
border-radius: 16rpx; border-radius: 16rpx;
font-size: 26rpx; font-size: 24rpx;
font-weight: 500; font-weight: 600;
}
.care-tips-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.tip-item {
display: flex;
gap: 16rpx;
align-items: flex-start;
}
.tip-dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background: #558B2F;
margin-top: 14rpx;
flex-shrink: 0;
}
.tip-text {
font-size: 28rpx;
color: #4B5563;
line-height: 1.5;
} }
/* ======== Loading ======== */
.wiki-detail-loading { .wiki-detail-loading {
height: 100vh; height: 100vh;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: #F9FAFB; background: #F4F6F0;
} }
+1 -1
View File
@@ -2,7 +2,7 @@
.identify-page { .identify-page {
min-height: 100vh; min-height: 100vh;
background: #F5F7F5; background: #F4F6F0;
} }
/* ========== Shared State Container ========== */ /* ========== Shared State Container ========== */
+4 -1
View File
@@ -150,7 +150,10 @@ Page({
goToDetail(e) { goToDetail(e) {
const item = e.currentTarget.dataset.item; const item = e.currentTarget.dataset.item;
wx.navigateTo({ wx.navigateTo({
url: `/pages/wiki/detail/index?id=${item.id}` url: `/pages/wiki/detail/index?id=${item.id}`,
success: (res) => {
res.eventChannel.emit('acceptDataFromOpenerPage', { data: item.raw });
}
}); });
}, },
-1
View File
@@ -4,7 +4,6 @@
"t-search": "tdesign-miniprogram/search/search", "t-search": "tdesign-miniprogram/search/search",
"t-tag": "tdesign-miniprogram/tag/tag", "t-tag": "tdesign-miniprogram/tag/tag",
"t-image": "tdesign-miniprogram/image/image", "t-image": "tdesign-miniprogram/image/image",
"t-fab": "tdesign-miniprogram/fab/fab",
"t-popup": "tdesign-miniprogram/popup/popup", "t-popup": "tdesign-miniprogram/popup/popup",
"t-cell": "tdesign-miniprogram/cell/cell", "t-cell": "tdesign-miniprogram/cell/cell",
"t-cell-group": "tdesign-miniprogram/cell-group/cell-group", "t-cell-group": "tdesign-miniprogram/cell-group/cell-group",
+4 -1
View File
@@ -113,7 +113,10 @@
<view style="height: 160rpx;"></view> <view style="height: 160rpx;"></view>
</scroll-view> </scroll-view>
<t-fab icon="scan" text="植物识别" bind:click="openIdentifyModal" aria-label="植物识别"></t-fab> <view class="floating-add-btn" bindtap="openIdentifyModal">
<t-icon name="scan" size="40rpx" color="#FFF" />
<text>植物识别</text>
</view>
<!-- Identify Popup --> <!-- Identify Popup -->
<t-popup visible="{{showIdentifyModal}}" bind:visible-change="onPopupVisibleChange" placement="bottom"> <t-popup visible="{{showIdentifyModal}}" bind:visible-change="onPopupVisibleChange" placement="bottom">
+26 -2
View File
@@ -3,7 +3,7 @@
height: 100vh; height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background-color: #F9FAFB; background-color: #F4F6F0;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
@@ -101,6 +101,30 @@
font-size: 28rpx; font-size: 28rpx;
} }
/* Floating Action Button */
.floating-add-btn {
position: fixed;
right: 40rpx;
bottom: 60rpx;
background: #558B2F;
color: white;
padding: 24rpx 40rpx;
border-radius: 60rpx;
display: flex;
align-items: center;
gap: 12rpx;
box-shadow: 0 12rpx 32rpx rgba(85, 139, 47, 0.4);
z-index: 1000;
font-size: 28rpx;
font-weight: 700;
transition: all 0.2s ease;
}
.floating-add-btn:active {
transform: scale(0.92);
box-shadow: 0 4rpx 16rpx rgba(85, 139, 47, 0.2);
}
/* Popup Styles */ /* Popup Styles */
.popup-content { .popup-content {
background: white; background: white;
@@ -144,7 +168,7 @@
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 20rpx; gap: 20rpx;
background: #F9FAFB; background: #F4F6F0;
border-radius: 32rpx; border-radius: 32rpx;
padding: 40rpx 24rpx; padding: 40rpx 24rpx;
transition: all 0.2s; transition: all 0.2s;
+6 -6
View File
@@ -72,12 +72,12 @@ class WxRequest {
} }
} else { } else {
// Handle non-200 HTTP errors // Handle non-200 HTTP errors
this.handleError({ errMsg: `HTTP Error: ${statusCode}`, ...res }); this.handleError({ ...res, errMsg: `HTTP Error: ${statusCode}` });
reject(res); reject(res);
} }
}, },
fail: (err) => { fail: (err) => {
this.handleError({ errMsg: 'Network Error', ...err }); this.handleError({ ...err, errMsg: 'Network Error' });
reject(err); reject(err);
} }
}); });
@@ -154,12 +154,12 @@ class WxRequest {
reject(finalData); reject(finalData);
} }
} else { } else {
this.handleError({ errMsg: `HTTP Error: ${statusCode}`, ...res }); this.handleError({ ...res, errMsg: `HTTP Error: ${statusCode}` });
reject(res); reject(res);
} }
}, },
fail: (err) => { fail: (err) => {
this.handleError({ errMsg: 'Upload Network Error', ...err }); this.handleError({ ...err, errMsg: 'Upload Network Error' });
reject(err); reject(err);
} }
}); });
@@ -215,12 +215,12 @@ class WxRequest {
reject(finalData); reject(finalData);
} }
} else { } else {
this.handleError({ errMsg: `HTTP Error: ${statusCode}`, ...res }); this.handleError({ ...res, errMsg: `HTTP Error: ${statusCode}` });
reject(res); reject(res);
} }
}, },
fail: (err) => { fail: (err) => {
this.handleError({ errMsg: 'Upload Network Error', ...err }); this.handleError({ ...err, errMsg: 'Upload Network Error' });
reject(err); reject(err);
} }
}); });