diff --git a/app.js b/app.js index 438c558..2da3956 100644 --- a/app.js +++ b/app.js @@ -57,6 +57,18 @@ App({ }); }, + // Force refresh login (e.g. on 401) + forceRefreshLogin() { + // Reset Promise + this.loginPromise = new Promise((resolve, reject) => { + this._resolveLogin = resolve; + this._rejectLogin = reject; + }); + wx.removeStorageSync('token'); + this.doLogin(); + return this.loginPromise; + }, + // Method for other pages/utils to wait for login ensureLogin() { // If token exists, resolve immediately diff --git a/app.json b/app.json index f259092..2f5a002 100644 --- a/app.json +++ b/app.json @@ -14,7 +14,8 @@ "pages/wiki/identify/index", "pages/profile/identify-history/index", "pages/profile/badges/index", - "pages/profile/badges/level-detail/index" + "pages/profile/badges/level-detail/index", + "pages/profile/badges/badge-wall/index" ], "window": { "backgroundTextStyle": "light", diff --git a/pages/profile/badges/badge-wall/index.js b/pages/profile/badges/badge-wall/index.js new file mode 100644 index 0000000..f96832c --- /dev/null +++ b/pages/profile/badges/badge-wall/index.js @@ -0,0 +1,87 @@ +import request from '../../../../utils/request'; + +Page({ + data: { + dimensions: [], + achievedMap: {}, + isLoading: true, + selectedBadge: null, + showDetail: false, + activeTab: 0 + }, + + onLoad() { + this.fetchData(); + }, + + switchTab(e) { + const index = e.currentTarget.dataset.index; + if (index !== undefined) { + this.setData({ activeTab: index }); + } + }, + + async fetchData() { + this.setData({ isLoading: true }); + wx.showLoading({ title: '加载中...' }); + try { + // Fetch Config Tree + const treeRes = await request.get('/config/badge/tree'); + const list = Array.isArray(treeRes) ? treeRes : (treeRes.data || []); + + // DEBUG: Force Unlock All + let achievedMap = {}; + list.forEach(dim => { + if (dim.groups) { + dim.groups.forEach(grp => { + if (grp.badges) { + grp.badges.forEach(b => { + achievedMap[b.id] = true; + }); + } + }); + } + }); + + // Original logic commented out for debug + /* + try { + const profile = await request.get('/profile/detail'); + if (profile && profile.achievedBadges) { + profile.achievedBadges.forEach(b => { + const id = typeof b === 'string' ? b : b.id; + achievedMap[id] = true; + }); + } + } catch (e) { + // Silent fail + } + */ + + this.setData({ + dimensions: list, + achievedMap + }); + } catch (e) { + console.error('Fetch badge tree failed', e); + wx.showToast({ title: '加载失败', icon: 'none' }); + } finally { + this.setData({ isLoading: false }); + wx.hideLoading(); + } + }, + + onBadgeTap(e) { + const badge = e.currentTarget.dataset.badge; + this.setData({ + selectedBadge: badge, + showDetail: true + }); + }, + + closeDetail() { + this.setData({ showDetail: false }); + }, + + noop() { } +}); diff --git a/pages/profile/badges/badge-wall/index.json b/pages/profile/badges/badge-wall/index.json new file mode 100644 index 0000000..cec49f6 --- /dev/null +++ b/pages/profile/badges/badge-wall/index.json @@ -0,0 +1,8 @@ +{ + "navigationBarTitleText": "我的成就与徽章", + "usingComponents": { + "t-icon": "tdesign-miniprogram/icon/icon", + "t-image": "tdesign-miniprogram/image/image", + "t-loading": "tdesign-miniprogram/loading/loading" + } +} \ No newline at end of file diff --git a/pages/profile/badges/badge-wall/index.wxml b/pages/profile/badges/badge-wall/index.wxml new file mode 100644 index 0000000..704860e --- /dev/null +++ b/pages/profile/badges/badge-wall/index.wxml @@ -0,0 +1,96 @@ + + + + + + + + {{item.label}} + + + + + + + + + + + + + + + + {{group.groupLabel}} + + 完成 {{0}}/3 + + + + + + + + + + + + + + + + + + + {{badge.tier === 1 ? '铜' : (badge.tier === 2 ? '银' : '金')}} + + + + + + + + + + + + + + + + + + + + + + + + + {{selectedBadge.name}} + + + {{achievedMap[selectedBadge.id] ? '已获得' : '未解锁'}} + + + + {{selectedBadge.description}} + + + + 奖励 + + ☀️ + +{{selectedBadge.rewardSunlight}} 阳光值 + + + + + 解锁条件:{{selectedBadge.threshold}} {{selectedBadge.targetAction === 'ACT_ALIVE_DAYS' ? '天存活' : (selectedBadge.targetAction === 'ACT_WATER' ? '次浇水' : (selectedBadge.targetAction === 'ACT_FERTILIZE' ? '次施肥' : (selectedBadge.targetAction === 'ACT_PRUNE' ? '次修剪' : (selectedBadge.targetAction === 'ACT_REPOT' ? '次换盆' : (selectedBadge.targetAction === 'ACT_PHOTO' ? '张照片' : (selectedBadge.targetAction === 'ACT_NIGHT_CARE' ? '次深夜养护' : '次操作'))))))}} + + + + + diff --git a/pages/profile/badges/badge-wall/index.wxss b/pages/profile/badges/badge-wall/index.wxss new file mode 100644 index 0000000..7bd9e5e --- /dev/null +++ b/pages/profile/badges/badge-wall/index.wxss @@ -0,0 +1,378 @@ +/* Badge Wall Styles */ +page { + background: #F5F7FA; + height: 100vh; + display: flex; + flex-direction: column; +} + +.badge-wall-page { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; +} + +/* Tabs */ +.tabs-container { + background: white; + padding: 0 10rpx; + box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.03); + z-index: 10; + flex-shrink: 0; +} + +.dimension-tabs { + white-space: nowrap; + display: flex; + height: 96rpx; +} + +.tab-item { + display: inline-flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 0 32rpx; + position: relative; + height: 100%; + min-width: 140rpx; +} + +.tab-label { + font-size: 28rpx; + color: #90A4AE; + font-weight: 500; + transition: all 0.3s; +} + +.tab-item.active .tab-label { + color: #558B2F; + font-weight: 700; + font-size: 32rpx; + transform: scale(1.05); +} + +.tab-indicator { + position: absolute; + bottom: 0; + width: 48rpx; + height: 6rpx; + background: #558B2F; + border-radius: 6rpx 6rpx 0 0; +} + +/* Content Scroll */ +.wall-scroll { + flex: 1; + height: 0; +} + +.wall-container { + padding: 30rpx 40rpx; + display: flex; + flex-direction: column; + gap: 32rpx; +} + +/* Track Card */ +.achievement-track-card { + background: white; + border-radius: 32rpx; + padding: 40rpx 32rpx; + box-shadow: 0 8rpx 24rpx rgba(149, 157, 165, 0.08); /* Generic soft shadow */ + position: relative; + overflow: visible; + margin-bottom: 20rpx; +} + +.track-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 48rpx; + padding: 0 12rpx; +} + +.track-title-row { + display: flex; + align-items: center; + gap: 12rpx; +} + +.track-title { + font-size: 32rpx; + font-weight: 700; + color: #263238; +} + +.track-sub { + font-size: 24rpx; + color: #CFD8DC; + font-weight: 500; +} + +.track-body { + position: relative; + padding: 0 20rpx; +} + +.track-line-bg { + position: absolute; + top: 58rpx; /* Center of 120rpx icon approx */ + left: 40rpx; + right: 40rpx; + height: 4rpx; + background: #E0E0E0; + border-radius: 4rpx; + z-index: 0; +} + +/* Track Badges Row */ +.track-badges-row { + display: flex; + justify-content: space-between; + position: relative; + z-index: 1; +} + +.track-node { + display: flex; + flex-direction: column; + align-items: center; + gap: 16rpx; + position: relative; + width: 140rpx; /* Tappable area */ +} + +.node-icon-wrapper { + width: 120rpx; + height: 120rpx; + background: #FAFAFA; + border-radius: 50%; + border: 6rpx solid white; + box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.06); + display: flex; + align-items: center; + justify-content: center; + position: relative; + transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +.track-node:active .node-icon-wrapper { + transform: scale(0.92); +} + +.track-node.unlocked .node-icon-wrapper { + background: #FFF; + box-shadow: 0 8rpx 20rpx rgba(0,0,0,0.1); +} + +/* Unlocked Border Colors */ +/* Bronze */ +.track-node:nth-child(1).unlocked .node-icon-wrapper { border-color: #D7CCC8; } +/* Silver */ +.track-node:nth-child(2).unlocked .node-icon-wrapper { border-color: #E0E0E0; } +/* Gold */ +.track-node:nth-child(3).unlocked .node-icon-wrapper { border-color: #FFECB3; box-shadow: 0 0 24rpx rgba(255, 213, 79, 0.4); } + +.node-img { + width: 100%; + height: 100%; + border-radius: 50%; + transition: transform 0.3s; +} + +/* Add hover effect for image instead of wrapper */ +.track-node:active .node-img { + transform: scale(0.9); +} + +.lock-mask { + position: absolute; + top: 0; + left: 0; + width: 90%; + height: 90%; + margin: 5%; + background: rgba(0,0,0,0.4); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + backdrop-filter: blur(2px); +} + +.node-info { + display: flex; + flex-direction: column; + align-items: center; +} + +.node-tier-tag { + font-size: 20rpx; + color: #90A4AE; + background: #ECEFF1; + padding: 6rpx 18rpx; + border-radius: 20rpx; + font-weight: 700; + letter-spacing: 1rpx; +} + +.node-tier-tag.bronze { background: #EFEBE9; color: #8D6E63; } +.node-tier-tag.silver { background: #FAFAFA; color: #757575; border: 1px solid #EEEEEE; } +.node-tier-tag.gold { + background: linear-gradient(135deg, #FFF8E1, #FFECB3); + color: #F57F17; + box-shadow: 0 2rpx 8rpx rgba(255, 193, 7, 0.2); +} + +/* Detail Popup Styles (Refined) */ +.badge-detail-mask { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0,0,0,0.6); + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + pointer-events: none; + transition: opacity 0.3s ease; + backdrop-filter: blur(8rpx); +} + +.badge-detail-mask.show { + opacity: 1; + pointer-events: auto; +} + +.badge-detail-card { + width: 80%; + background: white; + border-radius: 40rpx; + padding: 56rpx 40rpx; + display: flex; + flex-direction: column; + align-items: center; + position: relative; + transform: scale(0.95); + transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); + box-shadow: 0 32rpx 64rpx rgba(0,0,0,0.2); +} + +.badge-detail-mask.show .badge-detail-card { + transform: scale(1); +} + +.detail-close { + position: absolute; + top: 24rpx; + right: 24rpx; + padding: 16rpx; + opacity: 0.6; +} + +.detail-icon-box { + width: 200rpx; + height: 200rpx; + margin-bottom: 32rpx; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} + +.detail-icon-box.achieved .detail-img { + width: 100%; + height: 100%; + filter: drop-shadow(0 16rpx 32rpx rgba(255, 213, 79, 0.5)); +} + +.detail-icon-box.locked .detail-img { + width: 80%; + height: 80%; + opacity: 0.4; + filter: grayscale(100%); +} + +.detail-name { + font-size: 40rpx; + font-weight: 800; + color: #263238; + margin-bottom: 12rpx; + text-align: center; +} + +.detail-status-tag { + padding: 8rpx 24rpx; + border-radius: 24rpx; + font-size: 24rpx; + margin-bottom: 40rpx; + font-weight: 600; +} + +.detail-status-tag.success { + background: #E8F5E9; + color: #2E7D32; +} + +.detail-status-tag.pending { + background: #ECEFF1; + color: #78909C; +} + +.detail-desc-box { + background: #F5F7F9; + padding: 32rpx; + border-radius: 24rpx; + width: 100%; + text-align: center; + margin-bottom: 32rpx; +} + +.detail-desc { + font-size: 30rpx; + color: #455A64; + line-height: 1.6; +} + +.detail-reward-box { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 0 24rpx; + margin-bottom: 32rpx; + background: linear-gradient(135deg, #FFF8E1, #FFECB3); + border-radius: 20rpx; + height: 96rpx; + box-shadow: 0 4rpx 12rpx rgba(255, 193, 7, 0.2); +} + +.reward-label { + font-size: 28rpx; + color: #E65100; + font-weight: 700; +} + +.reward-val { + font-size: 36rpx; + font-weight: 800; + color: #E65100; + display: flex; + align-items: center; + gap: 8rpx; +} + +.detail-condition { + width: 100%; + text-align: center; + font-size: 24rpx; + color: #90A4AE; + border-top: 2rpx dashed #CFD8DC; + padding-top: 24rpx; +} diff --git a/pages/profile/badges/index.js b/pages/profile/badges/index.js index 5597fd4..88c273f 100644 --- a/pages/profile/badges/index.js +++ b/pages/profile/badges/index.js @@ -102,4 +102,10 @@ Page({ url: `/pages/profile/badges/level-detail/index?sunlight=${this.data.currentExp}` }); }, + + openBadgeWall() { + wx.navigateTo({ + url: '/pages/profile/badges/badge-wall/index' + }); + }, }); diff --git a/pages/profile/badges/index.wxml b/pages/profile/badges/index.wxml index 84e0044..6b075c8 100644 --- a/pages/profile/badges/index.wxml +++ b/pages/profile/badges/index.wxml @@ -24,17 +24,22 @@ 点击查看等级详情 > - 所有徽章 ({{badges.length}}) + 我的成就 - - - - - + + + + + - {{item.name}} - {{item.desc}} - {{item.progress}} + + 成就徽章墙 + 查看所有成就与收集进度 + + + + 去查看 + diff --git a/pages/profile/badges/index.wxss b/pages/profile/badges/index.wxss index c3ec71f..cf6dc14 100644 --- a/pages/profile/badges/index.wxss +++ b/pages/profile/badges/index.wxss @@ -218,56 +218,74 @@ page { 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; +.badge-wall-entry { + margin: 10rpx 0 40rpx; + background: linear-gradient(120deg, #1976D2, #64B5F6); + border-radius: 32rpx; + padding: 32rpx 40rpx; display: flex; - flex-direction: column; + justify-content: space-between; align-items: center; - text-align: center; - box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.02); + position: relative; + overflow: hidden; + box-shadow: 0 16rpx 32rpx rgba(25, 118, 210, 0.25); } -.badge-item.locked { - background: #F8F9FA; - border: 2rpx dashed #E5E7EB; - box-shadow: none; +.entry-bg-bloom { + position: absolute; + right: -40rpx; + top: -60rpx; + width: 240rpx; + height: 240rpx; + background: radial-gradient(circle, rgba(255,255,255,0.15) 0%, transparent 70%); + border-radius: 50%; + z-index: 1; } -.badge-icon-circle { - width: 88rpx; - height: 88rpx; - border-radius: 30rpx; +.wall-entry-left { + display: flex; + align-items: center; + gap: 24rpx; + z-index: 2; +} + +.entry-icon-box { + width: 96rpx; + height: 96rpx; + background: rgba(255,255,255,0.2); + border-radius: 24rpx; display: flex; align-items: center; justify-content: center; - margin-bottom: 20rpx; + border: 2rpx solid rgba(255,255,255,0.1); } -.badge-name { - font-size: 26rpx; +.wall-entry-text { + display: flex; + flex-direction: column; + gap: 6rpx; +} + +.entry-title { + color: white; + font-size: 32rpx; font-weight: 700; - color: #374151; - margin-bottom: 6rpx; } -.badge-desc { - font-size: 20rpx; - color: #9CA3AF; +.entry-desc { + color: rgba(255,255,255,0.8); + font-size: 24rpx; } -.badge-progress { - margin-top: 12rpx; - font-size: 20rpx; - background: #F3F4F6; - padding: 4rpx 12rpx; - border-radius: 12rpx; - color: #6B7280; +.wall-entry-right { + display: flex; + align-items: center; + gap: 8rpx; + z-index: 2; +} + +.entry-action { + color: rgba(255,255,255,0.9); + font-size: 26rpx; + font-weight: 600; } diff --git a/pages/profile/index.js b/pages/profile/index.js index d9463cd..1dd2e9c 100644 --- a/pages/profile/index.js +++ b/pages/profile/index.js @@ -233,14 +233,16 @@ Page({ 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' }); + openDoc(e) { + if (wx.openPrivacyContract) { + wx.openPrivacyContract({ + fail: () => { + wx.showToast({ title: '无法打开协议', icon: 'none' }); + } + }); + } else { + wx.showToast({ title: '当前微信版本不支持查看', icon: 'none' }); + } }, // ======== Profile Editor Popup ======== diff --git a/pages/profile/index.wxml b/pages/profile/index.wxml index ccc9578..032fe05 100644 --- a/pages/profile/index.wxml +++ b/pages/profile/index.wxml @@ -81,6 +81,13 @@ 一款专注于家庭植物养护的小程序。帮助你记录植物成长、制定养护计划、识别未知植物,与花友们分享养花心得。 + + + + 用户协议 + + + © 2026 Sundynix · All Rights Reserved @@ -117,7 +124,7 @@ {{taskDoneCount}} - 养护 + 养护次数 @@ -167,7 +174,7 @@ - 成就徽章 + 等级徽章 diff --git a/pages/profile/index.wxss b/pages/profile/index.wxss index d8d3069..b6b6a2a 100644 --- a/pages/profile/index.wxss +++ b/pages/profile/index.wxss @@ -642,3 +642,38 @@ button.edit-row.avatar-btn::after { border: none; display: none; } + +.about-menu-list { + margin: 40rpx 0; + background: white; + border-radius: 24rpx; + padding: 0 32rpx; + box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.02); +} + +.about-menu-item { + display: flex; + align-items: center; + justify-content: space-between; + height: 112rpx; + border-bottom: 2rpx solid #F9FAFB; + font-size: 30rpx; + color: #374151; + font-weight: 500; +} + +.about-menu-item:last-child { + border-bottom: none; +} + +.about-footer { + display: flex; + justify-content: center; + padding: 40rpx 0; + margin-top: 40rpx; +} + +.about-copyright { + font-size: 22rpx; + color: #B0BEC5; +} diff --git a/project.private.config.json b/project.private.config.json index 1a50a2c..e1ffeca 100644 --- a/project.private.config.json +++ b/project.private.config.json @@ -3,7 +3,7 @@ "projectname": "plant-mp", "condition": {}, "setting": { - "urlCheck": true, + "urlCheck": false, "coverView": true, "lazyloadPlaceholderEnable": false, "skylineRenderEnable": false, diff --git a/utils/request.js b/utils/request.js index ce82c21..7c84974 100644 --- a/utils/request.js +++ b/utils/request.js @@ -59,6 +59,24 @@ class WxRequest { const processedResponse = this.interceptors.response(res); const { statusCode, data } = processedResponse; + // Auto-refresh token if 401 (One-time retry) + if ((statusCode === 401 || data.code === 401) && !options.skipToken && !options._retry) { + const app = getApp(); + if (app && app.forceRefreshLogin) { + console.log('401 detected, refreshing token...'); + app.forceRefreshLogin().then(() => { + // Retry Original Request + this.request({ ...options, _retry: true }) + .then(resolve) + .catch(reject); + }).catch(err => { + console.error('Token refresh failed', err); + reject(data); + }); + return; + } + } + // 1. Check HTTP Status Code if (statusCode >= 200 && statusCode < 300) { // 2. Check Business Logic Code (Assuming 200 is success based on common Go patterns, @@ -252,8 +270,8 @@ class WxRequest { // Initialize with default instance const request = new WxRequest({ - //baseUrl: 'http://192.168.0.184:8889', - baseUrl: 'https://go.sundynix.cn/api', + baseUrl: 'http://192.168.0.184:8889', + //baseUrl: 'https://go.sundynix.cn/api', header: { 'Content-Type': 'application/json' }