优化勋章分段进度展示
This commit is contained in:
+13
-11
@@ -3,7 +3,12 @@ const util = require('../../utils/util')
|
|||||||
const themeMod = require('../../utils/theme')
|
const themeMod = require('../../utils/theme')
|
||||||
const iconsMod = require('../../utils/icons')
|
const iconsMod = require('../../utils/icons')
|
||||||
const config = require('../../config')
|
const config = require('../../config')
|
||||||
const { getBadgeProgress, getBadgeCelebration } = require('../../utils/badge-state')
|
const {
|
||||||
|
getBadgePositions,
|
||||||
|
getBadgeSegments,
|
||||||
|
getBadgeDisplayLabel,
|
||||||
|
getBadgeCelebration
|
||||||
|
} = require('../../utils/badge-state')
|
||||||
|
|
||||||
Page({
|
Page({
|
||||||
data: {
|
data: {
|
||||||
@@ -17,7 +22,7 @@ Page({
|
|||||||
maxDurationText: '',
|
maxDurationText: '',
|
||||||
totalDays: 0,
|
totalDays: 0,
|
||||||
trainingBadges: [],
|
trainingBadges: [],
|
||||||
badgeProgress: 0,
|
badgeSegments: [],
|
||||||
nextBadgeHint: '',
|
nextBadgeHint: '',
|
||||||
currentYear: new Date().getFullYear(),
|
currentYear: new Date().getFullYear(),
|
||||||
currentMonth: new Date().getMonth() + 1,
|
currentMonth: new Date().getMonth() + 1,
|
||||||
@@ -75,20 +80,17 @@ Page({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 累计训练天数勋章(里程碑进度条):统计口径=累计去重训练日(可中断)。
|
// 累计训练天数勋章(里程碑进度条):统计口径=累计去重训练日(可中断)。
|
||||||
// 节点等距排列(间距一致、视觉整齐),填充在区间内线性插值,
|
// 节点等距排列;每个连接段独立填充,绿色铺满即解锁下一枚勋章。
|
||||||
// 解锁时填充正好抵达该节点,满级时填充满格。
|
|
||||||
const totalDays = stats.totalDays
|
const totalDays = stats.totalDays
|
||||||
const badgeDefs = config.trainingDayBadges
|
const badgeDefs = config.trainingDayBadges
|
||||||
const n = badgeDefs.length
|
const badgePositions = getBadgePositions(badgeDefs)
|
||||||
const TRACK_PAD = 4 // 首尾留白,保证首尾节点/标签不溢出
|
|
||||||
const segWidth = n > 1 ? (100 - TRACK_PAD * 2) / (n - 1) : 0
|
|
||||||
const posOf = (i) => TRACK_PAD + i * segWidth
|
|
||||||
const trainingBadges = badgeDefs.map((b, i) => ({
|
const trainingBadges = badgeDefs.map((b, i) => ({
|
||||||
...b,
|
...b,
|
||||||
unlocked: totalDays >= b.days,
|
unlocked: totalDays >= b.days,
|
||||||
pos: Math.round(posOf(i) * 10) / 10
|
pos: badgePositions[i],
|
||||||
|
label: getBadgeDisplayLabel(b, totalDays >= b.days)
|
||||||
}))
|
}))
|
||||||
const badgeProgress = getBadgeProgress(badgeDefs, totalDays)
|
const badgeSegments = getBadgeSegments(badgeDefs, totalDays)
|
||||||
|
|
||||||
// 单枚解锁庆祝反馈:对比持久化"已见解锁集合",只庆祝新增勋章。
|
// 单枚解锁庆祝反馈:对比持久化"已见解锁集合",只庆祝新增勋章。
|
||||||
// 首次运行(无存储)仅初始化集合,避免老用户一打开就被历史勋章刷屏。
|
// 首次运行(无存储)仅初始化集合,避免老用户一打开就被历史勋章刷屏。
|
||||||
@@ -111,7 +113,7 @@ Page({
|
|||||||
maxDurationText: util.formatDuration(stats.maxDuration),
|
maxDurationText: util.formatDuration(stats.maxDuration),
|
||||||
totalDays,
|
totalDays,
|
||||||
trainingBadges,
|
trainingBadges,
|
||||||
badgeProgress,
|
badgeSegments,
|
||||||
nextBadgeHint,
|
nextBadgeHint,
|
||||||
monthRecords: records,
|
monthRecords: records,
|
||||||
historyList,
|
historyList,
|
||||||
|
|||||||
@@ -59,7 +59,10 @@
|
|||||||
<text class="badge-status-next" wx:if="{{nextBadgeHint}}">{{nextBadgeHint}}</text>
|
<text class="badge-status-next" wx:if="{{nextBadgeHint}}">{{nextBadgeHint}}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="badge-track">
|
<view class="badge-track">
|
||||||
<view class="badge-fill" style="width: {{badgeProgress}}%;"></view>
|
<view class="badge-segment" wx:for="{{badgeSegments}}" wx:key="index"
|
||||||
|
style="left: {{item.left}}%; width: {{item.width}}%;">
|
||||||
|
<view class="badge-segment-fill" style="width: {{item.progress}}%;"></view>
|
||||||
|
</view>
|
||||||
<view class="badge-node {{item.unlocked ? 'unlocked' : ''}} {{item.justUnlocked ? 'celebrate' : ''}}"
|
<view class="badge-node {{item.unlocked ? 'unlocked' : ''}} {{item.justUnlocked ? 'celebrate' : ''}}"
|
||||||
wx:for="{{trainingBadges}}" wx:key="days"
|
wx:for="{{trainingBadges}}" wx:key="days"
|
||||||
style="left: {{item.pos}}%;"
|
style="left: {{item.pos}}%;"
|
||||||
@@ -72,7 +75,7 @@
|
|||||||
<view class="badge-labels">
|
<view class="badge-labels">
|
||||||
<text class="badge-label {{item.unlocked ? 'unlocked' : ''}}"
|
<text class="badge-label {{item.unlocked ? 'unlocked' : ''}}"
|
||||||
wx:for="{{trainingBadges}}" wx:key="days"
|
wx:for="{{trainingBadges}}" wx:key="days"
|
||||||
style="left: {{item.pos}}%;">{{item.unlocked ? item.name : item.days}}</text>
|
style="left: {{item.pos}}%;">{{item.label}}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</ui-card>
|
</ui-card>
|
||||||
|
|||||||
@@ -101,11 +101,14 @@
|
|||||||
border-radius: 2rpx;
|
border-radius: 2rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-fill {
|
.badge-segment {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
|
||||||
top: 0;
|
top: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-segment-fill {
|
||||||
|
height: 100%;
|
||||||
background: var(--primary);
|
background: var(--primary);
|
||||||
border-radius: 2rpx;
|
border-radius: 2rpx;
|
||||||
transition: width 0.6s ease;
|
transition: width 0.6s ease;
|
||||||
|
|||||||
@@ -1,9 +1,23 @@
|
|||||||
const test = require('node:test')
|
const test = require('node:test')
|
||||||
const assert = require('node:assert/strict')
|
const assert = require('node:assert/strict')
|
||||||
|
|
||||||
const { getBadgeProgress, getBadgeCelebration } = require('../utils/badge-state')
|
const {
|
||||||
|
getBadgeProgress,
|
||||||
|
getBadgePositions,
|
||||||
|
getBadgeSegments,
|
||||||
|
getBadgeDisplayLabel,
|
||||||
|
getBadgeCelebration
|
||||||
|
} = require('../utils/badge-state')
|
||||||
|
|
||||||
const badges = [{ days: 3 }, { days: 7 }, { days: 14 }]
|
const badges = [
|
||||||
|
{ days: 3, name: '抖登' },
|
||||||
|
{ days: 7, name: '中登' },
|
||||||
|
{ days: 14, name: '老登' },
|
||||||
|
{ days: 30, name: '神登' },
|
||||||
|
{ days: 60, name: '上古神登' },
|
||||||
|
{ days: 80, name: '洪荒神登' },
|
||||||
|
{ days: 100, name: '究极登祖' }
|
||||||
|
]
|
||||||
|
|
||||||
test('badge progress remains zero before the first milestone', () => {
|
test('badge progress remains zero before the first milestone', () => {
|
||||||
assert.equal(getBadgeProgress(badges, 1), 0)
|
assert.equal(getBadgeProgress(badges, 1), 0)
|
||||||
@@ -11,6 +25,30 @@ test('badge progress remains zero before the first milestone', () => {
|
|||||||
assert.equal(getBadgeProgress(badges, 3), 4)
|
assert.equal(getBadgeProgress(badges, 3), 4)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('milestones use equal spacing and overall fill stays aligned', () => {
|
||||||
|
assert.deepEqual(getBadgePositions(badges), [4, 19.3, 34.7, 50, 65.3, 80.7, 96])
|
||||||
|
assert.equal(getBadgeProgress(badges, 7), 19.3)
|
||||||
|
assert.equal(getBadgeProgress(badges, 60), 65.3)
|
||||||
|
assert.equal(getBadgeProgress(badges, 100), 100)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('only the active badge connection is partially green', () => {
|
||||||
|
assert.deepEqual(getBadgeSegments(badges, 16), [
|
||||||
|
{ left: 4, width: 15.3, progress: 100 },
|
||||||
|
{ left: 19.3, width: 15.4, progress: 100 },
|
||||||
|
{ left: 34.7, width: 15.3, progress: 12.5 },
|
||||||
|
{ left: 50, width: 15.3, progress: 0 },
|
||||||
|
{ left: 65.3, width: 15.4, progress: 0 },
|
||||||
|
{ left: 80.7, width: 15.3, progress: 0 }
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('equal spacing keeps full unlocked badge names readable', () => {
|
||||||
|
assert.equal(getBadgeDisplayLabel(badges[3], true), '神登')
|
||||||
|
assert.equal(getBadgeDisplayLabel(badges[4], true), '上古神登')
|
||||||
|
assert.equal(getBadgeDisplayLabel(badges[6], false), '100天')
|
||||||
|
})
|
||||||
|
|
||||||
test('badge celebration keeps prior milestones seen after records are deleted', () => {
|
test('badge celebration keeps prior milestones seen after records are deleted', () => {
|
||||||
const afterDeletion = getBadgeCelebration([3], [])
|
const afterDeletion = getBadgeCelebration([3], [])
|
||||||
assert.deepEqual(afterDeletion, { newly: [], seen: [3] })
|
assert.deepEqual(afterDeletion, { newly: [], seen: [3] })
|
||||||
|
|||||||
+46
-3
@@ -1,5 +1,15 @@
|
|||||||
const TRACK_PAD = 4
|
const TRACK_PAD = 4
|
||||||
|
|
||||||
|
const roundPosition = (value) => Math.round(value * 10) / 10
|
||||||
|
|
||||||
|
const getBadgePositions = (badges) => {
|
||||||
|
const defs = Array.isArray(badges) ? badges : []
|
||||||
|
if (defs.length === 0) return []
|
||||||
|
if (defs.length === 1) return [TRACK_PAD]
|
||||||
|
const segmentWidth = (100 - TRACK_PAD * 2) / (defs.length - 1)
|
||||||
|
return defs.map((_, index) => roundPosition(TRACK_PAD + index * segmentWidth))
|
||||||
|
}
|
||||||
|
|
||||||
const getBadgeProgress = (badges, totalDays) => {
|
const getBadgeProgress = (badges, totalDays) => {
|
||||||
const defs = Array.isArray(badges) ? badges : []
|
const defs = Array.isArray(badges) ? badges : []
|
||||||
if (defs.length === 0) return 0
|
if (defs.length === 0) return 0
|
||||||
@@ -8,12 +18,39 @@ const getBadgeProgress = (badges, totalDays) => {
|
|||||||
if (days < defs[0].days) return 0
|
if (days < defs[0].days) return 0
|
||||||
if (defs.length === 1 || days >= defs[defs.length - 1].days) return 100
|
if (defs.length === 1 || days >= defs[defs.length - 1].days) return 100
|
||||||
|
|
||||||
const segmentWidth = (100 - TRACK_PAD * 2) / (defs.length - 1)
|
const positions = getBadgePositions(defs)
|
||||||
let index = 0
|
let index = 0
|
||||||
while (index < defs.length - 1 && days >= defs[index + 1].days) index++
|
while (index < defs.length - 1 && days >= defs[index + 1].days) index++
|
||||||
|
|
||||||
const ratio = (days - defs[index].days) / (defs[index + 1].days - defs[index].days)
|
const ratio = (days - defs[index].days) / (defs[index + 1].days - defs[index].days)
|
||||||
return Math.round((TRACK_PAD + index * segmentWidth + ratio * segmentWidth) * 10) / 10
|
return roundPosition(positions[index] + ratio * (positions[index + 1] - positions[index]))
|
||||||
|
}
|
||||||
|
|
||||||
|
const getBadgeSegments = (badges, totalDays) => {
|
||||||
|
const defs = Array.isArray(badges) ? badges : []
|
||||||
|
const positions = getBadgePositions(defs)
|
||||||
|
const days = Math.max(0, Number(totalDays) || 0)
|
||||||
|
|
||||||
|
return defs.slice(0, -1).map((badge, index) => {
|
||||||
|
const nextBadge = defs[index + 1]
|
||||||
|
const segmentDays = nextBadge.days - badge.days
|
||||||
|
const progress = days <= badge.days
|
||||||
|
? 0
|
||||||
|
: days >= nextBadge.days
|
||||||
|
? 100
|
||||||
|
: roundPosition((days - badge.days) / segmentDays * 100)
|
||||||
|
|
||||||
|
return {
|
||||||
|
left: positions[index],
|
||||||
|
width: roundPosition(positions[index + 1] - positions[index]),
|
||||||
|
progress
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getBadgeDisplayLabel = (badge, unlocked) => {
|
||||||
|
if (!badge) return ''
|
||||||
|
return unlocked && badge.name ? badge.name : `${badge.days}天`
|
||||||
}
|
}
|
||||||
|
|
||||||
const getBadgeCelebration = (previous, unlockedDays) => {
|
const getBadgeCelebration = (previous, unlockedDays) => {
|
||||||
@@ -25,4 +62,10 @@ const getBadgeCelebration = (previous, unlockedDays) => {
|
|||||||
return { newly, seen: Array.from(new Set(previous.concat(unlocked))) }
|
return { newly, seen: Array.from(new Set(previous.concat(unlocked))) }
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { getBadgeProgress, getBadgeCelebration }
|
module.exports = {
|
||||||
|
getBadgeProgress,
|
||||||
|
getBadgePositions,
|
||||||
|
getBadgeSegments,
|
||||||
|
getBadgeDisplayLabel,
|
||||||
|
getBadgeCelebration
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user