kydc/achievements/achievements.js

173 lines
6.6 KiB
JavaScript
Raw Normal View History

2026-04-19 11:56:49 +08:00
const app = getApp()
2026-04-23 11:34:54 +08:00
const ACHIEVEMENT_ICON_MAP = {
2026-04-30 11:15:39 +08:00
MASTERED_100: '/assets/icons/achievements/core/100.svg',
MASTERED_1000: '/assets/icons/achievements/core/1000.svg',
MASTERED_3000: '/assets/icons/achievements/core/3000.svg',
MASTERED_5000: '/assets/icons/achievements/core/5000.svg',
MASTERED_7000: '/assets/icons/achievements/core/7000.svg',
MASTERED_10000: '/assets/icons/achievements/core/10000.svg',
2026-04-23 11:34:54 +08:00
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 = {
2026-04-30 11:15:39 +08:00
mastery: [100, 1000, 3000, 5000, 7000, 10000],
2026-04-23 11:34:54 +08:00
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 '继续坚持,解锁第一档'
}
2026-04-19 11:56:49 +08:00
Page({
data: {
total: 0,
unlocked: 0,
2026-04-23 11:34:54 +08:00
sections: [],
2026-04-19 11:56:49 +08:00
loading: false,
error: '',
},
onShow() {
this.loadData()
},
2026-04-23 11:34:54 +08:00
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)
},
2026-04-19 11:56:49 +08:00
loadData() {
this.setData({ loading: true, error: '' })
wx.request({
url: `${app.globalData.baseUrl}/api/v1/achievements`,
header: { 'X-OpenID': app.globalData.openid || '' },
success: (res) => {
2026-04-23 11:34:54 +08:00
const list = this.decorateList(res.data?.data || [])
const sections = this.buildSections(list)
2026-04-19 11:56:49 +08:00
this.setData({
2026-04-23 11:34:54 +08:00
sections,
2026-04-19 11:56:49 +08:00
total: list.length,
unlocked: list.filter((item) => item.is_unlocked).length,
})
},
fail: () => this.setData({ error: '成就数据加载失败' }),
complete: () => this.setData({ loading: false }),
})
},
2026-04-23 11:34:54 +08:00
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 }),
})
},
2026-04-19 11:56:49 +08:00
})