183 lines
7.1 KiB
JavaScript
183 lines
7.1 KiB
JavaScript
const app = getApp()
|
|
|
|
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')
|
|
|
|
const ACHIEVEMENT_ICON_MAP = {
|
|
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'),
|
|
}
|
|
|
|
const STATUS_ICON_MAP = {
|
|
1: staticIcon('/icons/achievements/state/achievement-unlocked.svg'),
|
|
0: staticIcon('/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: 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') },
|
|
}
|
|
|
|
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: '',
|
|
heroBadgeIcon: HERO_BADGE_ICON,
|
|
},
|
|
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: 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],
|
|
}))
|
|
},
|
|
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 || '' },
|
|
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 || '' },
|
|
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 }),
|
|
})
|
|
},
|
|
})
|