diff --git a/pages/records/records.js b/pages/records/records.js
index 7a69622..839f28a 100644
--- a/pages/records/records.js
+++ b/pages/records/records.js
@@ -3,7 +3,12 @@ const util = require('../../utils/util')
const themeMod = require('../../utils/theme')
const iconsMod = require('../../utils/icons')
const config = require('../../config')
-const { getBadgeProgress, getBadgeCelebration } = require('../../utils/badge-state')
+const {
+ getBadgePositions,
+ getBadgeSegments,
+ getBadgeDisplayLabel,
+ getBadgeCelebration
+} = require('../../utils/badge-state')
Page({
data: {
@@ -17,7 +22,7 @@ Page({
maxDurationText: '',
totalDays: 0,
trainingBadges: [],
- badgeProgress: 0,
+ badgeSegments: [],
nextBadgeHint: '',
currentYear: new Date().getFullYear(),
currentMonth: new Date().getMonth() + 1,
@@ -75,20 +80,17 @@ Page({
}
// 累计训练天数勋章(里程碑进度条):统计口径=累计去重训练日(可中断)。
- // 节点等距排列(间距一致、视觉整齐),填充在区间内线性插值,
- // 解锁时填充正好抵达该节点,满级时填充满格。
+ // 节点等距排列;每个连接段独立填充,绿色铺满即解锁下一枚勋章。
const totalDays = stats.totalDays
const badgeDefs = config.trainingDayBadges
- const n = badgeDefs.length
- const TRACK_PAD = 4 // 首尾留白,保证首尾节点/标签不溢出
- const segWidth = n > 1 ? (100 - TRACK_PAD * 2) / (n - 1) : 0
- const posOf = (i) => TRACK_PAD + i * segWidth
+ const badgePositions = getBadgePositions(badgeDefs)
const trainingBadges = badgeDefs.map((b, i) => ({
...b,
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),
totalDays,
trainingBadges,
- badgeProgress,
+ badgeSegments,
nextBadgeHint,
monthRecords: records,
historyList,
diff --git a/pages/records/records.wxml b/pages/records/records.wxml
index ea87771..3c42ab3 100644
--- a/pages/records/records.wxml
+++ b/pages/records/records.wxml
@@ -59,7 +59,10 @@
{{nextBadgeHint}}
-
+
+
+
{{item.unlocked ? item.name : item.days}}
+ style="left: {{item.pos}}%;">{{item.label}}
diff --git a/pages/records/records.wxss b/pages/records/records.wxss
index 3f32457..ad16d5a 100644
--- a/pages/records/records.wxss
+++ b/pages/records/records.wxss
@@ -101,11 +101,14 @@
border-radius: 2rpx;
}
-.badge-fill {
+.badge-segment {
position: absolute;
- left: 0;
top: 0;
height: 100%;
+}
+
+.badge-segment-fill {
+ height: 100%;
background: var(--primary);
border-radius: 2rpx;
transition: width 0.6s ease;
diff --git a/tests/badge-state.test.js b/tests/badge-state.test.js
index 5f517fc..ca1e18f 100644
--- a/tests/badge-state.test.js
+++ b/tests/badge-state.test.js
@@ -1,9 +1,23 @@
const test = require('node:test')
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', () => {
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)
})
+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', () => {
const afterDeletion = getBadgeCelebration([3], [])
assert.deepEqual(afterDeletion, { newly: [], seen: [3] })
diff --git a/utils/badge-state.js b/utils/badge-state.js
index 3c88a46..8337195 100644
--- a/utils/badge-state.js
+++ b/utils/badge-state.js
@@ -1,5 +1,15 @@
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 defs = Array.isArray(badges) ? badges : []
if (defs.length === 0) return 0
@@ -8,12 +18,39 @@ const getBadgeProgress = (badges, totalDays) => {
if (days < defs[0].days) return 0
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
while (index < defs.length - 1 && days >= defs[index + 1].days) index++
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) => {
@@ -25,4 +62,10 @@ const getBadgeCelebration = (previous, unlockedDays) => {
return { newly, seen: Array.from(new Set(previous.concat(unlocked))) }
}
-module.exports = { getBadgeProgress, getBadgeCelebration }
+module.exports = {
+ getBadgeProgress,
+ getBadgePositions,
+ getBadgeSegments,
+ getBadgeDisplayLabel,
+ getBadgeCelebration
+}