From 420476183780db3cf02e4c953bb16553d45262b7 Mon Sep 17 00:00:00 2001 From: taiyi Date: Thu, 23 Apr 2026 11:34:54 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=88=91=E7=9A=84=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E5=A4=B4=E5=83=8F=E7=99=BB=E5=BD=95=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- achievements/achievements.js | 149 +++++++++++++++- achievements/achievements.wxml | 45 ++++- achievements/achievements.wxss | 2 +- entry-edit/entry-edit.js | 97 ++++++++--- entry-edit/entry-edit.wxml | 56 ++++-- entry-edit/entry-edit.wxss | 266 ++++++++++++++++++++++------- me/me.js | 143 +++++++++++++++- me/me.wxml | 26 ++- me/me.wxss | 2 +- progress-list/progress-list.js | 29 +++- progress-list/progress-list.wxml | 178 ++++++++++++++----- progress-list/progress-list.wxss | 2 +- review-config/review-config.js | 10 +- review-result/review-result.js | 8 +- review-result/review-result.wxml | 10 +- review-session/review-session.js | 18 +- review-session/review-session.wxml | 5 +- review-session/review-session.wxss | 2 +- vocab-manage/vocab-manage.js | 4 + vocab-manage/vocab-manage.wxml | 9 +- 20 files changed, 876 insertions(+), 185 deletions(-) diff --git a/achievements/achievements.js b/achievements/achievements.js index 98d2577..f24681a 100644 --- a/achievements/achievements.js +++ b/achievements/achievements.js @@ -1,25 +1,145 @@ 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, - list: [], + 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 = res.data?.data || [] + const list = this.decorateList(res.data?.data || []) + const sections = this.buildSections(list) this.setData({ - list, + sections, total: list.length, unlocked: list.filter((item) => item.is_unlocked).length, }) @@ -28,4 +148,27 @@ Page({ 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 }), + }) + }, }) diff --git a/achievements/achievements.wxml b/achievements/achievements.wxml index 02277ef..0efa6a9 100644 --- a/achievements/achievements.wxml +++ b/achievements/achievements.wxml @@ -12,7 +12,9 @@ {{unlocked}} 已点亮勋章 / {{total}} - 🏆 + + + @@ -25,17 +27,44 @@ {{error}} - + 暂无成就 继续复习和打卡后会逐渐解锁 - - - - 🏅 - {{item.achievement_name}} - {{item.progress_value}} / {{item.target_value}} + + + + + {{item.title}} + {{item.sections.length}} 个分组 + + + + + + + + {{item.meta.title}} + {{item.meta.subtitle}} + + + {{item.items.length}} 项 + + {{item.progressText}} + + + + + + + {{item.achievement_name}} + {{item.progress_value}} / {{item.target_value}} + + + + + diff --git a/achievements/achievements.wxss b/achievements/achievements.wxss index 6bb2344..06f5773 100644 --- a/achievements/achievements.wxss +++ b/achievements/achievements.wxss @@ -1 +1 @@ -page{background:#0c0e12;color:#f6f6fc}.page{min-height:100vh;background:#0c0e12;box-sizing:border-box}.topbar{display:flex;justify-content:space-between;align-items:center;padding:24rpx 32rpx;background:rgba(12,14,18,.8);backdrop-filter:blur(24px);border-bottom:1rpx solid rgba(255,255,255,.08);box-shadow:0 1rpx 20rpx rgba(0,0,0,.08)}.brand{display:flex;align-items:center;gap:18rpx}.avatar-wrap{width:64rpx;height:64rpx;border-radius:9999rpx;overflow:hidden;border:1rpx solid rgba(255,255,255,.1);flex-shrink:0}.avatar{width:100%;height:100%}.brand-text{font-size:38rpx;font-weight:800;color:#c19cff}.icon-btn{width:72rpx;height:72rpx;border-radius:9999rpx;background:rgba(255,255,255,.06);display:flex;align-items:center;justify-content:center}.content{height:calc(100vh - 120rpx);padding:48rpx 40rpx 32rpx;box-sizing:border-box}.heading{margin-bottom:28rpx}.title{display:block;font-size:52rpx;font-weight:800;line-height:1.1}.subtitle{display:block;margin-top:10rpx;color:#aaabb0;font-size:26rpx}.hero-card{position:relative;overflow:hidden;padding:32rpx;border-radius:32rpx;background:rgba(29,32,37,.6);backdrop-filter:blur(24rpx);border:1rpx solid rgba(255,255,255,.08);box-shadow:0 20rpx 40rpx rgba(0,0,0,.08);margin-bottom:24rpx}.hero-glow{position:absolute;right:-40rpx;top:-40rpx;width:160rpx;height:160rpx;border-radius:9999rpx;background:rgba(255,64,129,.12);filter:blur(30rpx)}.hero-row{display:flex;justify-content:space-between;align-items:center;position:relative;z-index:1}.hero-num{display:block;font-size:72rpx;font-weight:800;color:#fff}.hero-label{display:block;margin-top:8rpx;font-size:24rpx;color:#aaabb0}.hero-badge{width:112rpx;height:112rpx;border-radius:9999rpx;background:linear-gradient(135deg,#ff4081,#00e5ff);display:flex;align-items:center;justify-content:center;font-size:44rpx;box-shadow:0 0 20rpx rgba(255,64,129,.2)}.grid{display:grid;grid-template-columns:1fr 1fr;gap:18rpx}.item{position:relative;overflow:hidden;padding:28rpx;border-radius:28rpx;background:rgba(29,32,37,.56);backdrop-filter:blur(24rpx);border:1rpx solid rgba(255,255,255,.06);box-shadow:0 20rpx 40rpx rgba(0,0,0,.06);min-height:220rpx}.accent-tertiary{box-shadow:0 20rpx 40rpx rgba(255,64,129,.06)}.accent-secondary{box-shadow:0 20rpx 40rpx rgba(0,229,255,.06)}.accent-primary{box-shadow:0 20rpx 40rpx rgba(127,0,255,.06)}.item-icon{width:76rpx;height:76rpx;border-radius:9999rpx;background:rgba(255,255,255,.06);display:flex;align-items:center;justify-content:center;margin-bottom:24rpx;font-size:32rpx}.item-title{display:block;font-size:30rpx;font-weight:700;margin-bottom:8rpx}.item-sub{display:block;font-size:22rpx;color:#aaabb0} +page{background:#0c0e12;color:#f6f6fc}.page{min-height:100vh;background:#0c0e12;box-sizing:border-box}.topbar{display:flex;justify-content:space-between;align-items:center;padding:24rpx 32rpx;background:rgba(12,14,18,.8);backdrop-filter:blur(24px);border-bottom:1rpx solid rgba(255,255,255,.08);box-shadow:0 1rpx 20rpx rgba(0,0,0,.08)}.brand{display:flex;align-items:center;gap:18rpx}.avatar-wrap{width:64rpx;height:64rpx;border-radius:9999rpx;overflow:hidden;border:1rpx solid rgba(255,255,255,.1);flex-shrink:0}.avatar{width:100%;height:100%}.brand-text{font-size:38rpx;font-weight:800;color:#c19cff}.icon-btn{width:72rpx;height:72rpx;border-radius:9999rpx;background:rgba(255,255,255,.06);display:flex;align-items:center;justify-content:center}.content{height:calc(100vh - 120rpx);padding:48rpx 40rpx 32rpx;box-sizing:border-box}.heading{margin-bottom:28rpx}.title{display:block;font-size:52rpx;font-weight:800;line-height:1.1}.subtitle{display:block;margin-top:10rpx;color:#aaabb0;font-size:26rpx}.hero-card{position:relative;overflow:hidden;padding:32rpx;border-radius:32rpx;background:rgba(29,32,37,.6);backdrop-filter:blur(24rpx);border:1rpx solid rgba(255,255,255,.08);box-shadow:0 20rpx 40rpx rgba(0,0,0,.08);margin-bottom:24rpx}.hero-glow{position:absolute;right:-40rpx;top:-40rpx;width:160rpx;height:160rpx;border-radius:9999rpx;background:rgba(255,64,129,.12);filter:blur(30rpx)}.hero-row{display:flex;justify-content:space-between;align-items:center;position:relative;z-index:1}.hero-num{display:block;font-size:72rpx;font-weight:800;color:#fff}.hero-label{display:block;margin-top:8rpx;font-size:24rpx;color:#aaabb0}.hero-badge{width:112rpx;height:112rpx;border-radius:9999rpx;background:linear-gradient(135deg,#ff4081,#00e5ff);display:flex;align-items:center;justify-content:center;box-shadow:0 0 20rpx rgba(255,64,129,.2);overflow:hidden}.hero-badge-img{width:64rpx;height:64rpx}.state-card{margin-bottom:28rpx}.state-head{display:flex;justify-content:space-between;align-items:center;margin:0 6rpx 18rpx}.state-title{font-size:36rpx;font-weight:800}.state-subtitle{font-size:22rpx;color:#aaabb0}.section-card{margin-bottom:20rpx;padding:28rpx;border-radius:30rpx;background:rgba(29,32,37,.45);border:1rpx solid rgba(255,255,255,.07);backdrop-filter:blur(24rpx)}.section-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:14rpx}.section-title-wrap{display:flex;align-items:center;gap:16rpx}.section-icon{width:52rpx;height:52rpx;border-radius:9999rpx;background:rgba(255,255,255,.06);padding:8rpx;box-sizing:border-box}.section-title{display:block;font-size:32rpx;font-weight:800;line-height:1.2}.section-subtitle{display:block;margin-top:4rpx;font-size:22rpx;color:#aaabb0}.section-count{font-size:22rpx;color:#c19cff;background:rgba(193,156,255,.12);padding:8rpx 14rpx;border-radius:9999rpx}.section-progress{display:block;margin:0 0 18rpx 68rpx;font-size:22rpx;color:#8fe3ff}.grid{display:grid;grid-template-columns:1fr 1fr;gap:18rpx}.item{position:relative;overflow:hidden;padding:28rpx;border-radius:28rpx;background:rgba(29,32,37,.56);backdrop-filter:blur(24rpx);border:1rpx solid rgba(255,255,255,.06);box-shadow:0 20rpx 40rpx rgba(0,0,0,.06);min-height:220rpx}.accent-tertiary{box-shadow:0 20rpx 40rpx rgba(255,64,129,.06)}.accent-secondary{box-shadow:0 20rpx 40rpx rgba(0,229,255,.06)}.accent-primary{box-shadow:0 20rpx 40rpx rgba(127,0,255,.06)}.item-icon{width:76rpx;height:76rpx;border-radius:9999rpx;background:rgba(255,255,255,.06);display:flex;align-items:center;justify-content:center;margin-bottom:24rpx;overflow:hidden}.item-icon-img{width:44rpx;height:44rpx}.item-title{display:block;font-size:30rpx;font-weight:700;margin-bottom:8rpx}.item-sub{display:block;font-size:22rpx;color:#aaabb0} diff --git a/entry-edit/entry-edit.js b/entry-edit/entry-edit.js index 3094993..6b9f5d5 100644 --- a/entry-edit/entry-edit.js +++ b/entry-edit/entry-edit.js @@ -2,15 +2,61 @@ const app = getApp() Page({ data: { + entryId: null, entryType: 'word', enText: '', zhText: '', exampleText: '', loading: false, error: '', + isEditMode: false, }, - setType(e) { - this.setData({ entryType: e.currentTarget.dataset.type }) + onLoad(options) { + const entryId = options.id ? String(options.id) : null + this.setData({ entryId, isEditMode: !!entryId }) + if (entryId) this.loadEntry(entryId) + }, + loadEntry(entryId) { + if (!app.globalData.openid) { + this.setData({ error: '请先登录' }) + return + } + this.setData({ loading: true, error: '' }) + wx.request({ + url: `${app.globalData.baseUrl}/api/v1/entries/${entryId}`, + header: { 'X-OpenID': app.globalData.openid }, + success: (res) => { + const entry = res.data?.data + if (!entry) { + this.setData({ error: '词条不存在' }) + return + } + this.setData({ + entryType: entry.entry_type || 'word', + enText: entry.en_text || '', + zhText: entry.zh_text || '', + exampleText: entry.example_text || '', + }) + }, + fail: () => this.setData({ error: '词条加载失败' }), + complete: () => this.setData({ loading: false }), + }) + }, + switchType(e) { + const entryType = e.currentTarget.dataset.type + if (this.data.isEditMode && entryType !== this.data.entryType) { + wx.showModal({ + title: '切换类型', + content: '切换类型后,当前内容仍会保留,但字段含义会变成新的类型。是否继续?', + confirmText: '继续切换', + cancelText: '取消', + success: (res) => { + if (res.confirm) this.setData({ entryType }) + }, + }) + return + } + this.setData({ entryType }) }, onEnInput(e) { this.setData({ enText: e.detail.value }) @@ -29,28 +75,41 @@ Page({ wx.showToast({ title: '请先登录', icon: 'none' }) return } + const method = this.data.isEditMode ? 'PUT' : 'POST' + const url = this.data.isEditMode + ? `${app.globalData.baseUrl}/api/v1/entries/${this.data.entryId}` + : `${app.globalData.baseUrl}/api/v1/entries` this.setData({ loading: true, error: '' }) wx.request({ - url: `${app.globalData.baseUrl}/api/v1/entries`, - method: 'POST', + url, + method, header: { 'Content-Type': 'application/json', 'X-OpenID': app.globalData.openid }, - data: { - entry_type: this.data.entryType, - en_text: this.data.enText, - zh_text: this.data.zhText, - example_text: this.data.exampleText, - source_type: 'manual', - upload_date: new Date().toISOString().slice(0, 10), - }, + data: this.data.isEditMode + ? { + entry_type: this.data.entryType, + en_text: this.data.enText, + zh_text: this.data.zhText, + example_text: this.data.exampleText, + } + : { + entry_type: this.data.entryType, + en_text: this.data.enText, + zh_text: this.data.zhText, + example_text: this.data.exampleText, + source_type: 'manual', + upload_date: new Date().toISOString().slice(0, 10), + }, success: () => { - wx.showToast({ title: '保存成功', icon: 'success' }) - this.setData({ - enText: '', - zhText: '', - exampleText: '', - }) + wx.showToast({ title: this.data.isEditMode ? '更新成功' : '保存成功', icon: 'success' }) + if (!this.data.isEditMode) { + this.setData({ + enText: '', + zhText: '', + exampleText: '', + }) + } }, - fail: () => this.setData({ error: '保存失败' }), + fail: () => this.setData({ error: this.data.isEditMode ? '更新失败' : '保存失败' }), complete: () => this.setData({ loading: false }), }) }, diff --git a/entry-edit/entry-edit.wxml b/entry-edit/entry-edit.wxml index a80c8a7..bee4636 100644 --- a/entry-edit/entry-edit.wxml +++ b/entry-edit/entry-edit.wxml @@ -1,39 +1,65 @@ - + + + - - + + {{isEditMode ? '编辑模式' : '新增模式'}} - - 添加新内容 - 构建你的专属知识库 + + + {{isEditMode ? '更新词条' : '创建词条'}} + {{isEditMode ? '编辑内容' : '添加新内容'}} + {{entryType === 'word' ? '记录单词、释义和例句' : '记录句子、翻译和语境说明'}} + + {{entryType === 'sentence' ? '句' : '词'}} - - 单词 - 句子 + + + 单词 + 句子 + + + + + 编辑提示 + 切换类型会改变字段含义,但不会自动清空你输入的内容。 - 英文内容 - + {{entryType === 'word' ? '单词' : '句子'}} + + + - 中文释义 - + {{entryType === 'word' ? '中文释义' : '中文翻译'}} + + + - + 例句 (可选)