const app = getApp() const ACHIEVEMENT_ICON_MAP = { MASTERED_10: '/assets/icons/achievements/core/mastered-10.svg', MASTERED_50: '/assets/icons/achievements/core/mastered-50.svg', MASTERED_100: '/assets/icons/achievements/core/mastery-advanced.svg', MASTERED_500: '/assets/icons/achievements/core/mastery-advanced.svg', MASTERED_1000: '/assets/icons/achievements/core/mastery-advanced.svg', MASTERED_2000: '/assets/icons/achievements/core/mastery-2000.svg', MASTERED_5000: '/assets/icons/achievements/core/mastery-5000.svg', MASTERED_10000: '/assets/icons/achievements/core/mastery-10000.svg', TOTAL_100: '/assets/icons/achievements/core/total-100.svg', CHECKIN_7: '/assets/icons/achievements/core/checkin-30.svg', CHECKIN_30: '/assets/icons/achievements/core/checkin-30.svg', CHECKIN_100: '/assets/icons/achievements/core/streak.svg', CHECKIN_180: '/assets/icons/achievements/core/streak.svg', CHECKIN_360: '/assets/icons/achievements/core/streak.svg', CHECKIN_STREAK_7: '/assets/icons/achievements/core/streak.svg', CHECKIN_STREAK_30: '/assets/icons/achievements/core/streak.svg', CHECKIN_STREAK_100: '/assets/icons/achievements/core/streak.svg', CHECKIN_STREAK_180: '/assets/icons/achievements/core/streak.svg', CHECKIN_STREAK_360: '/assets/icons/achievements/core/streak.svg', ACCURACY_80: '/assets/icons/achievements/core/accuracy.svg', ACCURACY_90: '/assets/icons/achievements/core/accuracy.svg', DAILY_REVIEW_20: '/assets/icons/achievements/core/review-volume.svg', DAILY_REVIEW_50: '/assets/icons/achievements/core/review-volume.svg', } const STATUS_ICON_MAP = { 1: '/assets/icons/achievements/state/achievement-unlocked.svg', 0: '/assets/icons/achievements/state/achievement-locked.svg', } const GROUP_ORDER = { mastery: 1, checkin: 2, streak: 3, accuracy: 4, review: 5, other: 9, } const GROUP_META = { mastery: { title: '掌握进阶', subtitle: '从入门到精通', icon: '/assets/icons/achievements/core/mastery-advanced.svg' }, checkin: { title: '打卡里程', subtitle: '记录坚持的脚步', icon: '/assets/icons/achievements/core/checkin-30.svg' }, streak: { title: '连续坚持', subtitle: '不断线的努力', icon: '/assets/icons/achievements/core/streak.svg' }, accuracy: { title: '准确率', subtitle: '复习质量提升', icon: '/assets/icons/achievements/core/accuracy.svg' }, review: { title: '复习数量', subtitle: '积累复习量', icon: '/assets/icons/achievements/core/review-volume.svg' }, other: { title: '其他', subtitle: '更多成就', icon: '/assets/icons/achievements/core/total-100.svg' }, } const GROUP_PROGRESS = { mastery: [10, 50, 100, 500, 1000, 2000, 5000, 10000], checkin: [7, 30, 100, 180, 360], streak: [7, 30, 100, 180, 360], accuracy: [80, 90], review: [20, 50], } function getNextTarget(items, values) { const unlockedMax = items.filter((item) => item.is_unlocked).reduce((max, item) => Math.max(max, item.target_value || 0), 0) const nextTarget = values.find((value) => value > unlockedMax) return nextTarget || null } function formatProgressText(key, items) { const values = GROUP_PROGRESS[key] || [] const nextTarget = getNextTarget(items, values) const best = items.reduce((max, item) => Math.max(max, item.progress_value || 0), 0) if (nextTarget) { return `距离下一档还差 ${Math.max(0, nextTarget - best)} 个` } if (items.some((item) => item.is_unlocked)) { return '该分组已全部解锁' } return '继续坚持,解锁第一档' } Page({ data: { total: 0, unlocked: 0, sections: [], loading: false, error: '', }, onShow() { this.loadData() }, getGroup(code) { if (code.startsWith('MASTERED_')) return 'mastery' if (code.startsWith('CHECKIN_STREAK_')) return 'streak' if (code.startsWith('CHECKIN_')) return 'checkin' if (code.startsWith('ACCURACY_')) return 'accuracy' if (code.startsWith('DAILY_REVIEW_')) return 'review' return 'other' }, decorateList(list) { return (list || []).map((item) => ({ ...item, group: this.getGroup(item.achievement_code), icon: ACHIEVEMENT_ICON_MAP[item.achievement_code] || GROUP_META[this.getGroup(item.achievement_code)].icon, stateIcon: STATUS_ICON_MAP[item.is_unlocked] || STATUS_ICON_MAP[0], })) }, buildSections(list) { const buckets = { unlocked: {}, locked: {} } list.forEach((item) => { const state = item.is_unlocked ? 'unlocked' : 'locked' const key = item.group || 'other' if (!buckets[state][key]) buckets[state][key] = [] buckets[state][key].push(item) }) const buildStateSections = (state) => Object.keys(buckets[state]) .sort((a, b) => (GROUP_ORDER[a] || 99) - (GROUP_ORDER[b] || 99)) .map((key) => { const items = buckets[state][key].sort((a, b) => a.target_value - b.target_value) return { key: `${state}-${key}`, state, meta: GROUP_META[key] || GROUP_META.other, progressText: formatProgressText(key, list.filter((item) => item.group === key)), items, } }) return [ { key: 'unlocked', title: '已解锁', sections: buildStateSections('unlocked') }, { key: 'locked', title: '未解锁', sections: buildStateSections('locked') }, ].filter((group) => group.sections.length) }, loadData() { this.setData({ loading: true, error: '' }) wx.request({ url: `${app.globalData.baseUrl}/api/v1/achievements`, header: { 'X-OpenID': app.globalData.openid || '' }, success: (res) => { const list = this.decorateList(res.data?.data || []) const sections = this.buildSections(list) this.setData({ sections, total: list.length, unlocked: list.filter((item) => item.is_unlocked).length, }) }, fail: () => this.setData({ error: '成就数据加载失败' }), complete: () => this.setData({ loading: false }), }) }, refreshAchievements() { this.setData({ loading: true, error: '' }) wx.request({ url: `${app.globalData.baseUrl}/api/v1/achievements/refresh`, method: 'POST', header: { 'X-OpenID': app.globalData.openid || '' }, success: (res) => { const payload = res.data?.data || {} const list = this.decorateList(payload.achievements || []) const sections = this.buildSections(list) this.setData({ sections, total: list.length, unlocked: list.filter((item) => item.is_unlocked).length, }) if ((payload.just_unlocked_count || 0) > 0) { wx.showToast({ title: `新解锁 ${payload.just_unlocked_count} 项成就`, icon: 'success' }) } }, fail: () => this.setData({ error: '成就刷新失败' }), complete: () => this.setData({ loading: false }), }) }, })