feat: 修复登录逻辑

This commit is contained in:
Blizzard
2026-02-12 17:23:55 +08:00
parent 5553e2711a
commit daea00ca60
14 changed files with 732 additions and 59 deletions
+12
View File
@@ -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
+2 -1
View File
@@ -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",
+87
View File
@@ -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() { }
});
@@ -0,0 +1,8 @@
{
"navigationBarTitleText": "我的成就与徽章",
"usingComponents": {
"t-icon": "tdesign-miniprogram/icon/icon",
"t-image": "tdesign-miniprogram/image/image",
"t-loading": "tdesign-miniprogram/loading/loading"
}
}
@@ -0,0 +1,96 @@
<view class="badge-wall-page">
<!-- Dimension Tabs -->
<view class="tabs-container">
<scroll-view class="dimension-tabs" scroll-x enable-flex show-scrollbar="{{false}}">
<block wx:for="{{dimensions}}" wx:key="dimension">
<view class="tab-item {{activeTab === index ? 'active' : ''}}"
bindtap="switchTab" data-index="{{index}}">
<text class="tab-label">{{item.label}}</text>
<view class="tab-indicator" wx:if="{{activeTab === index}}"></view>
</view>
</block>
</scroll-view>
</view>
<!-- Content Area -->
<scroll-view scroll-y class="wall-scroll" enable-back-to-top show-scrollbar="{{false}}">
<view class="wall-container" wx:if="{{dimensions[activeTab]}}">
<block wx:for="{{dimensions[activeTab].groups}}" wx:key="groupId" wx:for-item="group">
<view class="achievement-track-card">
<view class="track-header">
<view class="track-title-row">
<t-icon name="caret-right-small" size="40rpx" color="#558B2F" />
<text class="track-title">{{group.groupLabel}}</text>
</view>
<text class="track-sub">完成 {{0}}/3</text> <!-- Placeholder count -->
</view>
<view class="track-body">
<!-- Background Line -->
<view class="track-line-bg"></view>
<view class="track-badges-row">
<block wx:for="{{group.badges}}" wx:key="id" wx:for-item="badge">
<view class="track-node {{achievedMap[badge.id] ? 'unlocked' : 'locked'}}"
bindtap="onBadgeTap" data-badge="{{badge}}">
<view class="node-icon-wrapper">
<image src="{{badge.icon.url}}" mode="aspectFit" class="node-img" />
<view class="lock-mask" wx:if="{{!achievedMap[badge.id]}}">
<t-icon name="lock-on" size="36rpx" color="rgba(255,255,255,0.8)" />
</view>
</view>
<view class="node-info">
<text class="node-tier-tag {{badge.tier===1?'bronze':(badge.tier===2?'silver':'gold')}}">{{badge.tier === 1 ? '铜' : (badge.tier === 2 ? '银' : '金')}}</text>
</view>
</view>
</block>
</view>
</view>
</view>
</block>
<view style="height: 60rpx;"></view>
</view>
</scroll-view>
<!-- Detailed Popup -->
<view class="badge-detail-mask {{showDetail ? 'show' : ''}}" bindtap="closeDetail">
<view class="badge-detail-card" catchtap="noop">
<view class="detail-close" bindtap="closeDetail">
<t-icon name="close" size="40rpx" color="#999" />
</view>
<block wx:if="{{selectedBadge}}">
<view class="detail-icon-box {{achievedMap[selectedBadge.id] ? 'achieved' : 'locked'}} tier-{{selectedBadge.tier}}">
<image src="{{selectedBadge.icon.url}}" mode="aspectFit" class="detail-img" />
</view>
<text class="detail-name">{{selectedBadge.name}}</text>
<view class="detail-status-tag {{achievedMap[selectedBadge.id] ? 'success' : 'pending'}}">
{{achievedMap[selectedBadge.id] ? '已获得' : '未解锁'}}
</view>
<view class="detail-desc-box">
<text class="detail-desc">{{selectedBadge.description}}</text>
</view>
<view class="detail-reward-box">
<text class="reward-label">奖励</text>
<view class="reward-val">
<text>☀️</text>
<text>+{{selectedBadge.rewardSunlight}} 阳光值</text>
</view>
</view>
<view class="detail-condition">
<text>解锁条件:{{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' ? '次深夜养护' : '次操作'))))))}}</text>
</view>
</block>
</view>
</view>
</view>
+378
View File
@@ -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;
}
+6
View File
@@ -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'
});
},
});
+14 -9
View File
@@ -24,17 +24,22 @@
<view class="click-hint">点击查看等级详情 ></view>
</view>
<view class="section-title-badges">所有徽章 ({{badges.length}})</view>
<view class="section-title-badges">我的成就</view>
<view class="badges-grid">
<view wx:for="{{badges}}" wx:key="id" class="badge-item {{item.unlocked ? 'unlocked' : 'locked'}}">
<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 class="badge-wall-entry" bindtap="openBadgeWall">
<view class="entry-bg-bloom"></view>
<view class="wall-entry-left">
<view class="entry-icon-box">
<t-icon name="achievement" size="56rpx" color="#FFD700" />
</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 class="wall-entry-text">
<text class="entry-title">成就徽章墙</text>
<text class="entry-desc">查看所有成就与收集进度</text>
</view>
</view>
<view class="wall-entry-right">
<text class="entry-action">去查看</text>
<t-icon name="chevron-right" size="40rpx" color="rgba(255,255,255,0.8)" />
</view>
</view>
+54 -36
View File
@@ -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;
}
+10 -8
View File
@@ -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 ========
+9 -2
View File
@@ -81,6 +81,13 @@
<view class="about-desc">
<text>一款专注于家庭植物养护的小程序。帮助你记录植物成长、制定养护计划、识别未知植物,与花友们分享养花心得。</text>
</view>
<view class="about-menu-list">
<view class="about-menu-item" bindtap="openDoc" data-type="terms">
<text>用户协议</text>
<t-icon name="chevron-right" size="36rpx" color="#CCC" />
</view>
</view>
<view class="about-footer">
<text class="about-copyright">© 2026 Sundynix · All Rights Reserved</text>
</view>
@@ -117,7 +124,7 @@
<view class="stat-divider"></view>
<view class="stat-item">
<text class="stat-num">{{taskDoneCount}}</text>
<text class="stat-label">养护</text>
<text class="stat-label">养护次数</text>
</view>
<view class="stat-divider"></view>
<view class="stat-item">
@@ -167,7 +174,7 @@
<view class="menu-icon-bg" style="background: #F3E5F5">
<t-icon name="award" size="36rpx" color="#9C27B0" />
</view>
<text class="menu-text">成就徽章</text>
<text class="menu-text">等级徽章</text>
</view>
<view class="menu-right-info">
<!-- <text class="menu-badge-text">已获 3 个</text> -->
+35
View File
@@ -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;
}
+1 -1
View File
@@ -3,7 +3,7 @@
"projectname": "plant-mp",
"condition": {},
"setting": {
"urlCheck": true,
"urlCheck": false,
"coverView": true,
"lazyloadPlaceholderEnable": false,
"skylineRenderEnable": false,
+20 -2
View File
@@ -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'
}