kydc/progress/achievements/achievements.js

183 lines
7.1 KiB
JavaScript
Raw Permalink Normal View History

2026-05-03 21:44:16 +08:00
const app = getApp()
2026-05-07 10:19:07 +08:00
function staticIcon(path) {
const base = app.globalData.staticBaseUrl || ''
const cleanBase = base.replace(/\/$/, '')
const cleanPath = path.replace(/^\/+/, '')
return `${cleanBase}/${cleanPath}`
}
const HERO_BADGE_ICON = staticIcon('/icons/achievements/state/achievement-unlocked.svg')
2026-05-03 21:44:16 +08:00
const ACHIEVEMENT_ICON_MAP = {
2026-05-07 10:19:07 +08:00
MASTERED_100: staticIcon('/icons/achievements/core/100.svg'),
MASTERED_1000: staticIcon('/icons/achievements/core/1000.svg'),
MASTERED_3000: staticIcon('/icons/achievements/core/3000.svg'),
MASTERED_5000: staticIcon('/icons/achievements/core/5000.svg'),
MASTERED_7000: staticIcon('/icons/achievements/core/7000.svg'),
MASTERED_10000: staticIcon('/icons/achievements/core/10000.svg'),
TOTAL_100: staticIcon('/icons/achievements/core/total-100.svg'),
CHECKIN_7: staticIcon('/icons/achievements/core/checkin-30.svg'),
CHECKIN_30: staticIcon('/icons/achievements/core/checkin-30.svg'),
CHECKIN_100: staticIcon('/icons/achievements/core/streak.svg'),
CHECKIN_180: staticIcon('/icons/achievements/core/streak.svg'),
CHECKIN_360: staticIcon('/icons/achievements/core/streak.svg'),
CHECKIN_STREAK_7: staticIcon('/icons/achievements/core/streak.svg'),
CHECKIN_STREAK_30: staticIcon('/icons/achievements/core/streak.svg'),
CHECKIN_STREAK_100: staticIcon('/icons/achievements/core/streak.svg'),
CHECKIN_STREAK_180: staticIcon('/icons/achievements/core/streak.svg'),
CHECKIN_STREAK_360: staticIcon('/icons/achievements/core/streak.svg'),
ACCURACY_80: staticIcon('/icons/achievements/core/accuracy.svg'),
ACCURACY_90: staticIcon('/icons/achievements/core/accuracy.svg'),
DAILY_REVIEW_20: staticIcon('/icons/achievements/core/review-volume.svg'),
DAILY_REVIEW_50: staticIcon('/icons/achievements/core/review-volume.svg'),
2026-05-03 21:44:16 +08:00
}
const STATUS_ICON_MAP = {
2026-05-07 10:19:07 +08:00
1: staticIcon('/icons/achievements/state/achievement-unlocked.svg'),
0: staticIcon('/icons/achievements/state/achievement-locked.svg'),
2026-05-03 21:44:16 +08:00
}
const GROUP_ORDER = {
mastery: 1,
checkin: 2,
streak: 3,
accuracy: 4,
review: 5,
other: 9,
}
const GROUP_META = {
2026-05-07 10:19:07 +08:00
mastery: { title: '掌握进阶', subtitle: '从入门到精通', icon: staticIcon('/icons/achievements/core/mastery-advanced.svg') },
checkin: { title: '打卡里程', subtitle: '记录坚持的脚步', icon: staticIcon('/icons/achievements/core/checkin-30.svg') },
streak: { title: '连续坚持', subtitle: '不断线的努力', icon: staticIcon('/icons/achievements/core/streak.svg') },
accuracy: { title: '准确率', subtitle: '复习质量提升', icon: staticIcon('/icons/achievements/core/accuracy.svg') },
review: { title: '复习数量', subtitle: '积累复习量', icon: staticIcon('/icons/achievements/core/review-volume.svg') },
other: { title: '其他', subtitle: '更多成就', icon: staticIcon('/icons/achievements/core/total-100.svg') },
2026-05-03 21:44:16 +08:00
}
const GROUP_PROGRESS = {
mastery: [100, 1000, 3000, 5000, 7000, 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: '',
2026-05-07 10:19:07 +08:00
heroBadgeIcon: HERO_BADGE_ICON,
2026-05-03 21:44:16 +08:00
},
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),
2026-05-07 10:19:07 +08:00
icon: item.icon_url || ACHIEVEMENT_ICON_MAP[item.achievement_code] || GROUP_META[this.getGroup(item.achievement_code)].icon,
stateIcon: item.state_icon_url || STATUS_ICON_MAP[item.is_unlocked] || STATUS_ICON_MAP[0],
2026-05-03 21:44:16 +08:00
}))
},
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-Auth-Token': app.globalData.authToken || '' },
2026-05-03 21:44:16 +08:00
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-Auth-Token': app.globalData.authToken || '' },
2026-05-03 21:44:16 +08:00
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 }),
})
},
})