diff --git a/app.js b/app.js index 63e0f1e..e71910e 100644 --- a/app.js +++ b/app.js @@ -8,13 +8,23 @@ App({ // Send res.code to backend to swap for openId, sessionKey, unionId if (res.code) { request.get('/auth/miniLogin', { code: res.code }).then(data => { - // Assuming the token is in data.token or data itself - const token = data.token || data; + // Response structure based on user input: { user: {...}, token: "...", expiresAt: ... } + // 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') { 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 { - 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 => { console.error('Login failed', err); diff --git a/app.json b/app.json index 3a867fa..7ab7c5e 100644 --- a/app.json +++ b/app.json @@ -10,7 +10,8 @@ "pages/plant-detail/edit/index", "pages/plant-detail/index", "pages/wiki/detail/index", - "pages/wiki/identify/index" + "pages/wiki/identify/index", + "pages/profile/identify-history/index" ], "window": { "backgroundTextStyle": "light", diff --git a/app.wxss b/app.wxss index c31481e..e537fb9 100644 --- a/app.wxss +++ b/app.wxss @@ -4,7 +4,7 @@ page { --primary-light: #9CCC65; --primary-dark: #33691E; --secondary: #8D6E63; - --bg-garden: #F1F8E9; + --bg-garden: #F4F6F0; --bg-card: rgba(255, 255, 255, 0.9); --text-main: #263238; --text-muted: #78909C; diff --git a/pages/community/create/index.wxss b/pages/community/create/index.wxss index 5ba049e..cb7f145 100644 --- a/pages/community/create/index.wxss +++ b/pages/community/create/index.wxss @@ -1,7 +1,7 @@ /** pages/community/create/index.wxss **/ page { height: 100%; - background: #fff; + background: #F4F6F0; } .create-post-page { diff --git a/pages/community/index.wxml b/pages/community/index.wxml index 11a1358..1f8abec 100644 --- a/pages/community/index.wxml +++ b/pages/community/index.wxml @@ -125,8 +125,9 @@ - - + + + 发布动态 diff --git a/pages/community/index.wxss b/pages/community/index.wxss index be333f1..2b59586 100644 --- a/pages/community/index.wxss +++ b/pages/community/index.wxss @@ -5,7 +5,7 @@ page { } .community-page { - background-color: #fff; + background-color: #F4F6F0; height: 100vh; display: flex; flex-direction: column; @@ -298,25 +298,27 @@ page { } /* Floating Action Button */ -.fab { - position: fixed; - right: 40rpx; - bottom: 200rpx; - width: 112rpx; - height: 112rpx; - background: linear-gradient(135deg, #689F38, #558B2F); - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - box-shadow: 0 12rpx 32rpx rgba(85, 139, 47, 0.4); - z-index: 100; - transition: all 0.2s; +.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: 100; + font-size: 28rpx; + font-weight: 700; + transition: all 0.2s ease; } -.fab:active { - transform: scale(0.92); - box-shadow: 0 6rpx 16rpx rgba(85, 139, 47, 0.3); +.floating-add-btn:active { + transform: scale(0.92); + box-shadow: 0 4rpx 16rpx rgba(85, 139, 47, 0.2); } /* WeChat Style Action Container */ diff --git a/pages/garden/index.json b/pages/garden/index.json index 8b1336c..eec2832 100644 --- a/pages/garden/index.json +++ b/pages/garden/index.json @@ -1,7 +1,6 @@ { "navigationBarTitleText": "我的花园", "usingComponents": { - "t-fab": "tdesign-miniprogram/fab/fab", "t-popup": "tdesign-miniprogram/popup/popup", "t-input": "tdesign-miniprogram/input/input", "t-button": "tdesign-miniprogram/button/button", diff --git a/pages/plant-detail/edit/index.wxss b/pages/plant-detail/edit/index.wxss index 0d856f9..6cf74ad 100644 --- a/pages/plant-detail/edit/index.wxss +++ b/pages/plant-detail/edit/index.wxss @@ -5,7 +5,7 @@ page { } .add-plant-page { - background-color: #F5F7F5; + background-color: #F4F6F0; height: 100vh; display: flex; flex-direction: column; @@ -15,7 +15,7 @@ page { .page-content { height: calc(100vh - 140rpx - env(safe-area-inset-bottom)); padding: 24rpx 32rpx; - background: #F5F7F5; + background: #F4F6F0; box-sizing: border-box; } diff --git a/pages/plant-detail/index.wxml b/pages/plant-detail/index.wxml index 7a7129e..08a9f7b 100644 --- a/pages/plant-detail/index.wxml +++ b/pages/plant-detail/index.wxml @@ -17,9 +17,9 @@ /> - - - + + + {{activeImageIndex + 1}} / {{swiperImages.length}} diff --git a/pages/plant-detail/index.wxss b/pages/plant-detail/index.wxss index e70151a..6df68ec 100644 --- a/pages/plant-detail/index.wxss +++ b/pages/plant-detail/index.wxss @@ -56,29 +56,21 @@ page { z-index: 10; } -/* Carousel Indicators */ -.carousel-indicators { +/* Image Counter Badge */ +.carousel-counter { position: absolute; bottom: 100rpx; right: 48rpx; - display: flex; - gap: 12rpx; + background: rgba(0, 0, 0, 0.45); + 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; -} - -.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; + letter-spacing: 2rpx; } .header-info { diff --git a/pages/profile/identify-history/index.js b/pages/profile/identify-history/index.js new file mode 100644 index 0000000..0956e34 --- /dev/null +++ b/pages/profile/identify-history/index.js @@ -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}`; + }, +}); diff --git a/pages/profile/identify-history/index.json b/pages/profile/identify-history/index.json new file mode 100644 index 0000000..ef92009 --- /dev/null +++ b/pages/profile/identify-history/index.json @@ -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" + } +} \ No newline at end of file diff --git a/pages/profile/identify-history/index.wxml b/pages/profile/identify-history/index.wxml new file mode 100644 index 0000000..c5b5d1e --- /dev/null +++ b/pages/profile/identify-history/index.wxml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + 暂无识别记录 + 去百科页面拍照识别植物吧 + + + + + + + + + + + + + + + + + + {{item.topName}} + + {{item.topScore}}% + + + {{item.time}} + + 还可能是: + {{other.name}}{{index < item.otherResults.length - 1 ? '、' : ''}} + + + + + + + + + + + + + {{item.topDesc}} + + + + + 识别结果排名 + + + + {{item.topName}} + + + + {{item.topScore}}% + + + + {{other.name}} + + + + {{other.score}}% + + + + + + + {{item.dateStr}} + + + + + + + + + + + — 没有更多了 — + + + + diff --git a/pages/profile/identify-history/index.wxss b/pages/profile/identify-history/index.wxss new file mode 100644 index 0000000..d1716d9 --- /dev/null +++ b/pages/profile/identify-history/index.wxss @@ -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; +} diff --git a/pages/profile/index.js b/pages/profile/index.js index 831ba60..adde425 100644 --- a/pages/profile/index.js +++ b/pages/profile/index.js @@ -1,52 +1,163 @@ // pages/profile/index.js -import { MOCK_FAVORITES, MOCK_BADGES, MOCK_POSTS } from '../../utils/mockData'; +import request from '../../utils/request'; + +const app = getApp(); Page({ data: { - view: 'profile', // profile, favorites, posts, badges - favTab: 'all', // all, plant, article - postsTab: 'published', // published, drafts + view: 'profile', // profile, favorites, posts, about + // 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: [], filteredFavorites: [], + + // Posts + postsTab: 'published', myPublishedPosts: [], myDrafts: [], - badges: [] + + // App version + appVersion: '1.0.0' }, - onLoad(options) { - this.setData({ - favorites: MOCK_FAVORITES, - badges: MOCK_BADGES - }); - this.filterFavorites(); + onLoad() { + this.loadUserInfo(); }, onShow() { - if (typeof this.getTabBar === 'function' && - this.getTabBar()) { - this.getTabBar().setData({ - selected: 4 // Index 4 is Profile - }) + if (typeof this.getTabBar === 'function' && this.getTabBar()) { + this.getTabBar().setData({ selected: 4 }); } - - // 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() { - // Get published posts by current user - const myPosts = MOCK_POSTS.filter(p => p.user === '我的花园'); - this.setData({ myPublishedPosts: myPosts }); + request.post('/post/page', { current: 1, pageSize: 50, onlyMine: true }).then(res => { + const records = res.records || res.list || []; + 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() { - // Load drafts from storage try { const draft = wx.getStorageSync('post_draft'); if (draft && (draft.content || (draft.images && draft.images.length > 0))) { - // Convert single draft to array for consistency this.setData({ myDrafts: [{ 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) { - const tab = e.detail.value; - this.setData({ postsTab: tab }); + this.setData({ postsTab: e.detail.value }); }, - 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) { const postId = e.currentTarget.dataset.id; wx.showModal({ @@ -103,42 +185,176 @@ Page({ content: '确定要删除这条动态吗?', confirmColor: '#EF5350', success: (res) => { - if (res.confirm) { - // Remove from MOCK_POSTS - const idx = MOCK_POSTS.findIndex(p => p.id === postId); - if (idx > -1) { - MOCK_POSTS.splice(idx, 1); - } + if (!res.confirm) return; + wx.showLoading({ title: '删除中...' }); + request.get('/post/delete', { id: postId }).then(() => { + wx.hideLoading(); this.loadMyPosts(); wx.showToast({ title: '已删除', icon: 'success' }); - } + }).catch(() => { + wx.hideLoading(); + wx.showToast({ title: '删除失败', icon: 'none' }); + }); } }); }, - // Edit a draft - editDraft(e) { - // Navigate to create page, which will load the draft - wx.navigateTo({ - url: '/pages/community/create/index' - }); + editDraft() { + wx.navigateTo({ url: '/pages/community/create/index' }); }, - // Delete a draft - deleteDraft(e) { + deleteDraft() { wx.showModal({ title: '删除草稿', content: '确定要删除这份草稿吗?', confirmColor: '#EF5350', success: (res) => { - if (res.confirm) { - try { - wx.removeStorageSync('post_draft'); - } catch (e) { } - this.setData({ myDrafts: [] }); - wx.showToast({ title: '已删除', icon: 'success' }); - } + if (!res.confirm) return; + try { wx.removeStorageSync('post_draft'); } catch (e) { } + this.setData({ myDrafts: [] }); + 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(...) }, }) diff --git a/pages/profile/index.json b/pages/profile/index.json index f8a065d..b24f71b 100644 --- a/pages/profile/index.json +++ b/pages/profile/index.json @@ -1,15 +1,16 @@ { "navigationBarTitleText": "个人中心", + "disableScroll": true, "usingComponents": { - "t-grid": "tdesign-miniprogram/grid/grid", - "t-grid-item": "tdesign-miniprogram/grid-item/grid-item", "t-cell": "tdesign-miniprogram/cell/cell", "t-cell-group": "tdesign-miniprogram/cell-group/cell-group", "t-avatar": "tdesign-miniprogram/avatar/avatar", "t-image": "tdesign-miniprogram/image/image", "t-tag": "tdesign-miniprogram/tag/tag", "t-icon": "tdesign-miniprogram/icon/icon", - "t-badge": "tdesign-miniprogram/badge/badge", - "t-progress": "tdesign-miniprogram/progress/progress" + "t-tabs": "tdesign-miniprogram/tabs/tabs", + "t-tab-panel": "tdesign-miniprogram/tab-panel/tab-panel", + "t-button": "tdesign-miniprogram/button/button", + "t-popup": "tdesign-miniprogram/popup/popup" } } \ No newline at end of file diff --git a/pages/profile/index.wxml b/pages/profile/index.wxml index 3b897b2..96d3f5e 100644 --- a/pages/profile/index.wxml +++ b/pages/profile/index.wxml @@ -1,200 +1,280 @@ - - - - - - - 我的收藏 - - - - - - - - - - - - - - {{item.name}} - - - {{item.meta}} - + + + + + 我的收藏 + + + + 全部 + 植物 + 文章 + + + + + + + + {{item.name}} + + + {{item.meta}} - - - - - 暂无收藏内容 + + + + 暂无收藏内容 + + + + + + + + + 我的发布 + + + + + {{item.time}} + + {{item.content}} + + + + + - + - - - - 我的发布 - - - - - - - - - - - - - {{item.time}} - - {{item.content}} - - - - - - - - - - 暂无已发布的动态 - + + + + + 成就徽章 - - - - - 草稿 - - {{item.content || '(无文字内容)'}} - - - - - - - - - - 暂无草稿 - - - - - - - - 成就徽章 - - - + + - - - 当前等级 - Lv.4 资深植人 - - - - - - - 经验值 - 350 / 500 - - - 距离 Lv.5 园艺大师 还需 150 经验 - + + + + 当前等级 + Lv.4 资深植人 + + + + + + 经验值 + 350 / 500 + + + + + 距离 Lv.5 园艺大师 还需 150 经验 + - + 所有徽章 (3/6) - - - - + + + + + + + + {{item.name}} + {{item.desc}} + {{item.progress}} + + - - - - - - + + + + + 关于我们 + + + + + + 植物护理助手 + 版本 {{appVersion}} - - 布偶猫园长 - Lv.4 资深植人 + + 一款专注于家庭植物养护的小程序。帮助你记录植物成长、制定养护计划、识别未知植物,与花友们分享养花心得。 + + + © 2026 Sundynix · All Rights Reserved - - - - - - - 12 - 植物 - - - 328 - 养护 - - - 15 - 关注 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + {{userName}} + Lv.4 资深植人 + + + + + + + + + + + + {{plantCount}} + 植物 + + + + {{taskDoneCount}} + 养护 + + + + {{postCount}} + 动态 + + + + + + + + 常用功能 + + + + + + + 我的收藏 + + + + + + + + + + 我的发布 + + + + + + + + + + 识别记录 + + + + + + + + + + 成就徽章 + + + 已获 3 个 + + + + + 更多服务 + + + + + + + 帮助与关于 + + + + + + + + + + + + + + 编辑资料 + + + + + + + 头像 + + + + + + + + + 昵称 + + + + + 保存 + + + + diff --git a/pages/profile/index.wxss b/pages/profile/index.wxss index a126bcb..11a1f23 100644 --- a/pages/profile/index.wxss +++ b/pages/profile/index.wxss @@ -1,149 +1,623 @@ /** pages/profile/index.wxss **/ + .profile-page { - background-color: #F4F6F0; - min-height: 100vh; - position: relative; - overflow: hidden; - display: flex; flex-direction: column; + background: #F4F6F0; + height: 100vh; + display: flex; + flex-direction: column; + overflow: hidden; +} + +/* ======== 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; } -/* Animations */ .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 { - from { transform: translateX(100%); opacity: 0; } - to { transform: translateX(0); opacity: 1; } + from { transform: translateX(100%); opacity: 0; } + to { transform: translateX(0); opacity: 1; } } -.sticky-nav { - position: sticky; top: 0; z-index: 100; background: white; - border-bottom: 2rpx solid #f0f0f0; - padding: 20rpx; +/* ======== Category Filter (Custom Chips) ======== */ +.category-filter { + display: flex; + 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 { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 24rpx; - margin-top: 24rpx; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20rpx; } .fav-card { - background: white; - border-radius: 24rpx; - overflow: hidden; - box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.03); + background: white; + border-radius: 20rpx; + overflow: hidden; + box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.03); } -.fav-img { width: 100%; display: block; 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; +.fav-img { + background: #f0f0f0; } -/* Posts View */ -.my-posts-list { padding: 40rpx; } -.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; +.fav-info { + padding: 16rpx 20rpx; } -.draft-card .my-post-content-wrap { - border-left: 4rpx solid #FFC107; +.fav-name { + display: block; + font-size: 28rpx; + font-weight: 600; + color: #1F2937; + margin-bottom: 8rpx; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } -.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; +.fav-meta-row { + display: flex; + align-items: center; + gap: 8rpx; } -/* Action Buttons */ -.edit-btn, .delete-btn { - cursor: pointer; +.fav-type { + font-size: 22rpx; + color: #9CA3AF; } -.edit-btn:active, .delete-btn:active { - opacity: 0.7; +/* ======== Posts Styles (Refined) ======== */ +.my-post-card { + display: flex; + gap: 24rpx; + margin-bottom: 32rpx; } -/* Badges View */ -.badges-content { padding: 40rpx; background: white; height: 100%; } +.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 { - background: linear-gradient(135deg, #FFF3E0 0%, #FFE0B2 100%); - border-radius: 40rpx; - padding: 48rpx; - margin-bottom: 60rpx; - color: #E65100; + background: linear-gradient(135deg, #2c3e50 0%, #4ca1af 100%); + border-radius: 40rpx; + padding: 40rpx; + color: white; + margin-bottom: 48rpx; + box-shadow: 0 20rpx 40rpx rgba(44, 62, 80, 0.2); + position: relative; + overflow: hidden; } -.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-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-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; } +.level-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 40rpx; + position: relative; + z-index: 2; +} -.section-title-badges { font-size: 32rpx; font-weight: 700; color: #333; margin-bottom: 32rpx; } +.level-info-large { + display: flex; + flex-direction: column; + gap: 8rpx; +} -/* Basic TDesign Grid Item styling override if needed */ -.t-grid-item__content { padding: 24rpx 0 !important; } +.level-label { + font-size: 24rpx; + opacity: 0.8; + letter-spacing: 2rpx; + text-transform: uppercase; +} -/* Main Profile */ -.main-profile-view { display: flex; flex-direction: column; height: 100%; } +.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); +} + +.badge-item.locked { + background: #F8F9FA; + border: 2rpx dashed #E5E7EB; + box-shadow: none; +} + +.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 { - padding: 40rpx 48rpx; - background: white; - display: flex; justify-content: space-between; align-items: flex-start; + 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-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; - margin: 24rpx 40rpx; - background: white; - border-radius: 32rpx; - box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.02); +.user-main { + display: flex; + align-items: center; + gap: 32rpx; } -.stat-col { display: flex; flex-direction: column; align-items: center; gap: 4rpx; } -.stat-num { font-size: 36rpx; font-weight: 800; color: var(--text-main); } -.stat-label { font-size: 22rpx; color: #90A4AE; } +.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 { - padding: 0 32rpx; + 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; } diff --git a/pages/tasks/index.wxss b/pages/tasks/index.wxss index bd199c4..e1e93f1 100644 --- a/pages/tasks/index.wxss +++ b/pages/tasks/index.wxss @@ -3,7 +3,7 @@ height: 100vh; display: flex; flex-direction: column; - background-color: #F8F9FA; + background-color: #F4F6F0; position: relative; overflow: hidden; } diff --git a/pages/wiki/detail/index.js b/pages/wiki/detail/index.js index 06131ad..bbe029b 100644 --- a/pages/wiki/detail/index.js +++ b/pages/wiki/detail/index.js @@ -5,117 +5,109 @@ Page({ data: { plant: null, activeImageIndex: 0, - swiperList: [] + swiperList: [], }, 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) { - this.loadPlantDetail(options.id); + // Give event channel a chance to fire + setTimeout(() => { + if (!loadedFromEvent && !this.data.plant) { + this.loadPlantDetail(options.id); + } + }, 100); } }, loadPlantDetail(id) { - // Fetch detail via wiki/page with specific ID - // Since there's no /wiki/detail endpoint, we use /wiki/page to get it - 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; + request.get('/wiki/detail', { id: id }).then(res => { + const item = res || null; if (!item) { wx.showToast({ title: '未找到该植物', icon: 'none' }); return; } - // Set Page Title - wx.setNavigationBarTitle({ title: item.name }); - - // Prepare swiper list from imgList - const swiperList = (item.imgList || []).map(img => img.url); - - // Parse pest/disease list - const commonPests = item.pestsDiseases - ? item.pestsDiseases.split(',').map(s => s.trim()).filter(Boolean) - : []; - - // Parse aliases - const aliasesList = item.aliases - ? item.aliases.split(/[,,、]/).map(s => s.trim()).filter(Boolean) - : []; - - // Parse reproduction methods - const reproductionList = item.reproductionMethod - ? item.reproductionMethod.split(/[,,、]/).map(s => s.trim()).filter(Boolean) - : []; - - // Difficulty label - const diffLabels = { 1: '简单', 2: '中等', 3: '较难', 4: '困难', 5: '专家' }; - - // Map API data to display model - const plant = { - id: item.id, - name: item.name, - latinName: item.latinName || '', - aliases: item.aliases || '', - aliasesList: aliasesList, - genus: item.genus || '', - distributionArea: item.distributionArea || '', - difficulty: item.difficulty || 0, - difficultyLabel: diffLabels[item.difficulty] || '未知', - isHot: item.isHot === 1, - lifeCycle: item.lifeCycle || '', - growthHabit: item.growthHabit || '', - reproductionMethod: item.reproductionMethod || '', - reproductionList: reproductionList, - - // Light - lightIntensity: item.lightIntensity || '', - lightType: item.lightType || '', - - // Temperature - optimalTempPeriod: item.optimalTempPeriod || '', - - // Morphology - stem: item.stem || '', - foliageType: item.foliageType || '', - foliageColor: item.foliageColor || '', - foliageShape: item.foliageShape || '', - height: item.height || 0, - - // Flowering - floweringPeriod: item.floweringPeriod || '', - floweringColor: item.floweringColor || '', - floweringShape: item.floweringShape || '', - flowerDiameter: item.flowerDiameter || 0, - - // Fruit - fruit: item.fruit || '', - - // Pests - pestsDiseases: item.pestsDiseases || '', - commonPests: commonPests, - - // Classes - classes: (item.classes || []).map(c => c.name), - - // Images - imgList: item.imgList || [] - }; - - this.setData({ - plant: plant, - swiperList: swiperList - }); + 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 }); + + // Prepare swiper list + const swiperList = (item.imgList || []).map(img => img.url); + + // Parse lists + const commonPests = item.pestsDiseases + ? item.pestsDiseases.split(',').map(s => s.trim()).filter(Boolean) + : []; + const aliasesList = item.aliases + ? item.aliases.split(/[,,、]/).map(s => s.trim()).filter(Boolean) + : []; + const reproductionList = item.reproductionMethod + ? item.reproductionMethod.split(/[,,、]/).map(s => s.trim()).filter(Boolean) + : []; + + const diffLabels = { 1: '简单', 2: '中等', 3: '较难', 4: '困难', 5: '专家' }; + + const plant = { + id: item.id, + name: item.name, + latinName: item.latinName || '', + aliases: item.aliases || '', + aliasesList, + genus: item.genus || '', + distributionArea: item.distributionArea || '', + difficulty: item.difficulty || 0, + difficultyLabel: diffLabels[item.difficulty] || '未知', + isHot: item.isHot === 1, + lifeCycle: item.lifeCycle || '', + growthHabit: item.growthHabit || '', + reproductionMethod: item.reproductionMethod || '', + reproductionList, + lightIntensity: item.lightIntensity || '', + lightType: item.lightType || '', + optimalTempPeriod: item.optimalTempPeriod || '', + stem: item.stem || '', + foliageType: item.foliageType || '', + foliageColor: item.foliageColor || '', + foliageShape: item.foliageShape || '', + height: item.height || 0, + floweringPeriod: item.floweringPeriod || '', + floweringColor: item.floweringColor || '', + floweringShape: item.floweringShape || '', + flowerDiameter: item.flowerDiameter || 0, + fruit: item.fruit || '', + pestsDiseases: item.pestsDiseases || '', + commonPests, + classes: (item.classes || []).map(c => c.name), + imgList: item.imgList || [] + }; + + this.setData({ + plant, + swiperList + }); + }, + onSwiperChange(e) { this.setData({ activeImageIndex: e.detail.current diff --git a/pages/wiki/detail/index.wxml b/pages/wiki/detail/index.wxml index d34a608..bfcce72 100644 --- a/pages/wiki/detail/index.wxml +++ b/pages/wiki/detail/index.wxml @@ -1,210 +1,193 @@ - + - - - - - - - - - - - - - - {{plant.name}} - {{plant.latinName}} - - - {{plant.genus}} - {{item}} - 难度: {{plant.difficultyLabel}} - + + + {{activeImageIndex + 1}} / {{swiperList.length}} - - - - - -
- - {{plant.growthHabit}} - -
- - -
- - - 基础档案 - - - - - 分布区域 - {{plant.distributionArea}} - - - 别名 - {{plant.aliases}} - - - 生命周期 - {{plant.lifeCycle === 'perennial' ? '多年生' : (plant.lifeCycle === 'annual' ? '一年生' : plant.lifeCycle)}} - - - 株高 - 约 {{plant.height}} cm - - - -
- - -
- - - 养护指南 - - - - - - - - - 光照 - {{plant.lightIntensity}} - - - - - - - - - - 适宜温度 - {{plant.optimalTempPeriod}} - - - - - - - - - - 繁殖方式 - {{plant.reproductionMethod}} - - - -
- - -
- - - 形态特征 - - - - - - {{plant.stem}} - - - 叶形 - {{plant.foliageShape}} - - - 叶色 - {{plant.foliageColor}} - - - 叶质 - {{plant.foliageType}} - - - -
- - -
- - - 开花信息 - - - - - 花期 - {{plant.floweringPeriod}} - - - 花色 - {{plant.floweringColor}} - - - 花型 - {{plant.floweringShape}} - - - 花径 - 约 {{plant.flowerDiameter}} mm - - - -
- - -
- - - 果实 - - - {{plant.fruit}} - -
- - -
- - - 常见病虫害 - - - - {{item}} - - -
- - - -
+ + + + {{plant.name}} + {{plant.latinName}} + + + {{plant.genus}} + {{item}} + 难度: {{plant.difficultyLabel}} + + + + + + + + + {{plant.growthHabit}} + + + + + + + + 基础档案 + + + + + 分布区域 + {{plant.distributionArea}} + + + 别名 + {{plant.aliases}} + + + 生命周期 + {{plant.lifeCycle === 'perennial' ? '多年生' : (plant.lifeCycle === 'annual' ? '一年生' : plant.lifeCycle)}} + + + 株高 + 约 {{plant.height}} cm + + + + + + + + + + 养护指南 + + + + + + + + 光照 + {{plant.lightIntensity}} + + + + + + + + 适宜温度 + {{plant.optimalTempPeriod}} + + + + + + + + 繁殖方式 + {{plant.reproductionMethod}} + + + + + + + + + + 形态特征 + + + + + + {{plant.stem}} + + + 叶形 + {{plant.foliageShape}} + + + 叶色 + {{plant.foliageColor}} + + + 叶质 + {{plant.foliageType}} + + + + + + + + + + 开花信息 + + + + + 花期 + {{plant.floweringPeriod}} + + + 花色 + {{plant.floweringColor}} + + + 花型 + {{plant.floweringShape}} + + + 花径 + 约 {{plant.flowerDiameter}} mm + + + + + + + + + + 果实 + + + {{plant.fruit}} + + + + + + + + 常见病虫害 + + + + {{item}} + + + + + +
- + diff --git a/pages/wiki/detail/index.wxss b/pages/wiki/detail/index.wxss index d6caf80..829e84e 100644 --- a/pages/wiki/detail/index.wxss +++ b/pages/wiki/detail/index.wxss @@ -3,201 +3,139 @@ height: 100vh; display: flex; flex-direction: column; - background: #F9FAFB; + background: #F4F6F0; } -/* Page Layout */ page { height: 100vh; - overflow: hidden; + overflow: hidden; } -/* Hide Scrollbar Globally */ -::-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 */ +/* ======== Image Carousel ======== */ .wd-header { - height: 500rpx; 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 { border-radius: 0 !important; - overflow: hidden; --td-swiper-radius: 0px !important; } -.custom-swiper .t-swiper { - border-radius: 0 !important; -} - -t-swiper { - border-radius: 0 !important; -} - -.wd-gallery-container { - width: 100%; - height: 100%; - position: relative; -} - -.wd-gradient-overlay { +.wd-counter { position: absolute; - top: 0; - left: 0; - right: 0; - height: 240rpx; - background: linear-gradient(to bottom, rgba(0, 0, 0, 0.7), transparent); - pointer-events: none; + bottom: 20rpx; + right: 24rpx; + background: rgba(0, 0, 0, 0.45); + 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: 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; } -.wd-indicators { - position: absolute; - 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-row { + margin-bottom: 16rpx; } .wd-name { - font-size: 56rpx; - color: #FFFFFF; + display: block; + font-size: 48rpx; font-weight: 800; - margin-bottom: 8rpx; + color: #1F2937; + line-height: 1.3; + margin-bottom: 6rpx; } .wd-scientific { - font-size: 32rpx; - color: rgba(255, 255, 255, 0.8); + display: block; + font-size: 28rpx; + color: #9CA3AF; font-style: italic; - font-family: serif; - margin-bottom: 24rpx; + font-family: Georgia, 'Times New Roman', serif; } .wd-badges { display: flex; - gap: 16rpx; + gap: 12rpx; flex-wrap: wrap; } .wd-badge { - background: rgba(255, 255, 255, 0.2); - backdrop-filter: blur(4px); + background: #F0F7EB; + color: #558B2F; padding: 8rpx 20rpx; - border-radius: 24rpx; - font-size: 24rpx; - color: #FFFFFF; - border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 20rpx; + font-size: 22rpx; + font-weight: 600; } +.wd-badge.difficulty { + background: #FFF8E1; + color: #F57F17; +} + +/* ======== Scrollable Content ======== */ +.wd-content { + flex: 1; + width: 100%; + height: 0; +} + +/* ======== Sections ======== */ .wd-section { - margin-bottom: 32rpx; - animation: fadeIn 0.5s ease-out; - 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); } + margin-bottom: 28rpx; + padding: 0 28rpx; } .section-title { display: flex; align-items: center; - gap: 16rpx; - margin-bottom: 24rpx; - padding-left: 8rpx; + gap: 14rpx; + margin-bottom: 20rpx; + padding-left: 4rpx; } .section-title text { - font-size: 34rpx; + font-size: 32rpx; 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 { - font-size: 30rpx; - line-height: 1.6; + font-size: 28rpx; + line-height: 1.7; color: #4B5563; } +/* ======== Info Grid ======== */ .wd-grid { display: grid; grid-template-columns: 1fr 1fr; - gap: 32rpx; + gap: 28rpx 32rpx; } .wd-stat-item { @@ -207,62 +145,72 @@ t-swiper { } .wd-label { - font-size: 24rpx; + font-size: 22rpx; color: #9CA3AF; text-transform: uppercase; letter-spacing: 1rpx; + font-weight: 500; } .wd-value { font-size: 28rpx; font-weight: 600; color: #1F2937; + line-height: 1.4; } -.wd-card { - 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 Items ======== */ .requirement-item { display: flex; gap: 24rpx; - margin-bottom: 24rpx; - padding-bottom: 24rpx; + padding: 24rpx 0; border-bottom: 2rpx solid #F3F4F6; } +.requirement-item:first-child { + padding-top: 0; +} + .requirement-item:last-child { - margin-bottom: 0; padding-bottom: 0; border-bottom: none; } .req-icon { - width: 80rpx; - height: 80rpx; - background: #F1F8E9; - border-radius: 20rpx; + width: 84rpx; + height: 84rpx; + border-radius: 22rpx; display: flex; align-items: center; justify-content: center; flex-shrink: 0; } +.req-icon.light { + background: #FFFBEB; +} + +.req-icon.temp { + background: #FEF2F2; +} + +.req-icon.repro { + background: #F0FDF4; +} + .req-content { flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + gap: 6rpx; } .req-title { display: block; - font-size: 30rpx; + font-size: 28rpx; font-weight: 700; color: #1F2937; - margin-bottom: 8rpx; } .req-desc { @@ -271,7 +219,7 @@ t-swiper { line-height: 1.5; } -/* FAQ / Pests */ +/* ======== Pest Tags ======== */ .pest-tags { display: flex; flex-wrap: wrap; @@ -283,41 +231,15 @@ t-swiper { color: #DC2626; padding: 12rpx 24rpx; border-radius: 16rpx; - font-size: 26rpx; - font-weight: 500; -} - -.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; + font-size: 24rpx; + font-weight: 600; } +/* ======== Loading ======== */ .wiki-detail-loading { height: 100vh; display: flex; align-items: center; justify-content: center; - background: #F9FAFB; + background: #F4F6F0; } diff --git a/pages/wiki/identify/index.wxss b/pages/wiki/identify/index.wxss index aad983c..000e805 100644 --- a/pages/wiki/identify/index.wxss +++ b/pages/wiki/identify/index.wxss @@ -2,7 +2,7 @@ .identify-page { min-height: 100vh; - background: #F5F7F5; + background: #F4F6F0; } /* ========== Shared State Container ========== */ diff --git a/pages/wiki/index.js b/pages/wiki/index.js index 02e6c52..96c7c20 100644 --- a/pages/wiki/index.js +++ b/pages/wiki/index.js @@ -150,7 +150,10 @@ Page({ goToDetail(e) { const item = e.currentTarget.dataset.item; 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 }); + } }); }, diff --git a/pages/wiki/index.json b/pages/wiki/index.json index 53a3d2e..ab0c35d 100644 --- a/pages/wiki/index.json +++ b/pages/wiki/index.json @@ -4,7 +4,6 @@ "t-search": "tdesign-miniprogram/search/search", "t-tag": "tdesign-miniprogram/tag/tag", "t-image": "tdesign-miniprogram/image/image", - "t-fab": "tdesign-miniprogram/fab/fab", "t-popup": "tdesign-miniprogram/popup/popup", "t-cell": "tdesign-miniprogram/cell/cell", "t-cell-group": "tdesign-miniprogram/cell-group/cell-group", diff --git a/pages/wiki/index.wxml b/pages/wiki/index.wxml index ade0db0..bb3d707 100644 --- a/pages/wiki/index.wxml +++ b/pages/wiki/index.wxml @@ -113,7 +113,10 @@ - + + + 植物识别 + diff --git a/pages/wiki/index.wxss b/pages/wiki/index.wxss index e2e6c21..1d06bde 100644 --- a/pages/wiki/index.wxss +++ b/pages/wiki/index.wxss @@ -3,7 +3,7 @@ height: 100vh; display: flex; flex-direction: column; - background-color: #F9FAFB; + background-color: #F4F6F0; position: relative; overflow: hidden; } @@ -101,6 +101,30 @@ 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-content { background: white; @@ -144,7 +168,7 @@ flex-direction: column; align-items: center; gap: 20rpx; - background: #F9FAFB; + background: #F4F6F0; border-radius: 32rpx; padding: 40rpx 24rpx; transition: all 0.2s; diff --git a/utils/request.js b/utils/request.js index acf03f0..0ecef15 100644 --- a/utils/request.js +++ b/utils/request.js @@ -72,12 +72,12 @@ class WxRequest { } } else { // Handle non-200 HTTP errors - this.handleError({ errMsg: `HTTP Error: ${statusCode}`, ...res }); + this.handleError({ ...res, errMsg: `HTTP Error: ${statusCode}` }); reject(res); } }, fail: (err) => { - this.handleError({ errMsg: 'Network Error', ...err }); + this.handleError({ ...err, errMsg: 'Network Error' }); reject(err); } }); @@ -154,12 +154,12 @@ class WxRequest { reject(finalData); } } else { - this.handleError({ errMsg: `HTTP Error: ${statusCode}`, ...res }); + this.handleError({ ...res, errMsg: `HTTP Error: ${statusCode}` }); reject(res); } }, fail: (err) => { - this.handleError({ errMsg: 'Upload Network Error', ...err }); + this.handleError({ ...err, errMsg: 'Upload Network Error' }); reject(err); } }); @@ -215,12 +215,12 @@ class WxRequest { reject(finalData); } } else { - this.handleError({ errMsg: `HTTP Error: ${statusCode}`, ...res }); + this.handleError({ ...res, errMsg: `HTTP Error: ${statusCode}` }); reject(res); } }, fail: (err) => { - this.handleError({ errMsg: 'Upload Network Error', ...err }); + this.handleError({ ...err, errMsg: 'Upload Network Error' }); reject(err); } });