修复趋势图表加载与比例展示

This commit is contained in:
2026-07-29 14:18:45 +08:00
parent 1372ec19b8
commit 19b0c4d4c8
5 changed files with 124 additions and 45 deletions
+5 -3
View File
@@ -1,3 +1,5 @@
const { getBarPercent } = require('../../utils/util')
/** /**
* Trend bar chart - pure CSS (no canvas), matches the calendar-heatmap's * Trend bar chart - pure CSS (no canvas), matches the calendar-heatmap's
* rendering approach for consistency. * rendering approach for consistency.
@@ -8,8 +10,8 @@
* - valueText: formatted string shown above the bar (e.g. "1分30秒") * - valueText: formatted string shown above the bar (e.g. "1分30秒")
* - highlight: truthy -> theme glow + bold label (today / this month) * - highlight: truthy -> theme glow + bold label (today / this month)
* *
* Bar height is value/max*100%, with an 8% floor for any non-zero value so * Bar height is strictly proportional to value/max. Zero values render a
* short sessions stay visible. Zero values render a faint stub. * faint stub so empty dates remain visible without distorting the data.
*/ */
Component({ Component({
properties: { properties: {
@@ -39,7 +41,7 @@ Component({
value: v, value: v,
valueText: (d && d.valueText) || '', valueText: (d && d.valueText) || '',
highlight: !!(d && d.highlight), highlight: !!(d && d.highlight),
percent: max > 0 ? Math.max((v / max) * 100, v > 0 ? 8 : 0) : 0 percent: getBarPercent(v, max)
} }
}) })
this.setData({ bars }) this.setData({ bars })
+2 -2
View File
@@ -39,11 +39,11 @@
/* value caption floats above the bar so it never eats into bar height */ /* value caption floats above the bar so it never eats into bar height */
.trend-bar-value { .trend-bar-value {
position: absolute; position: absolute;
top: -28rpx; top: -26rpx;
left: 0; left: 0;
right: 0; right: 0;
text-align: center; text-align: center;
font-size: 18rpx; font-size: 16rpx;
color: var(--text-secondary); color: var(--text-secondary);
line-height: 1; line-height: 1;
white-space: nowrap; white-space: nowrap;
+1 -40
View File
@@ -168,46 +168,7 @@ Page({
* 返回 [{ label, value, valueText, highlight }] 供 trend-chart 渲染。 * 返回 [{ label, value, valueText, highlight }] 供 trend-chart 渲染。
*/ */
_computeTrend(mode) { _computeTrend(mode) {
const allRecords = Object.values(storage.getRecords()).flat() return util.getTrendData(storage.getRecords(), mode)
const today = new Date()
const weekdays = ['日', '一', '二', '三', '四', '五', '六']
const pad = (n) => String(n).padStart(2, '0')
if (mode === 'month') {
const data = []
for (let i = 5; i >= 0; i--) {
const d = new Date(today.getFullYear(), today.getMonth() - i, 1)
const monthKey = `${d.getFullYear()}-${pad(d.getMonth() + 1)}`
const monthRecords = storage.getRecordsByMonth(monthKey) || []
const total = monthRecords.reduce((s, r) => s + (Number(r.duration) || 0), 0)
data.push({
label: `${d.getMonth() + 1}`,
value: total,
valueText: total > 0 ? util.formatDuration(total) : '',
highlight: i === 0
})
}
return data
}
// default: week (近 7 天)
const data = []
for (let i = 6; i >= 0; i--) {
const d = new Date(today)
d.setDate(d.getDate() - i)
const dateStr = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
const total = allRecords
.filter((r) => util.dateOnly(r.date) === dateStr)
.reduce((s, r) => s + (Number(r.duration) || 0), 0)
const isToday = i === 0
data.push({
label: isToday ? '今天' : weekdays[d.getDay()],
value: total,
valueText: total > 0 ? util.formatDuration(total) : '',
highlight: isToday
})
}
return data
}, },
onDayTap(e) { onDayTap(e) {
+47
View File
@@ -0,0 +1,47 @@
const test = require('node:test')
const assert = require('node:assert/strict')
const { getTrendData, getBarPercent } = require('../utils/util')
test('weekly trend combines multiple sessions from the same day', () => {
const records = {
'2026-07': [
{ date: '2026-07-29 08:00:00', duration: 30 },
{ date: '2026-07-29 20:00:00', duration: 45 },
{ date: '2026-07-23 08:00:00', duration: 60 }
]
}
assert.deepEqual(getTrendData(records, 'week', new Date(2026, 6, 29)), [
{ label: '四', value: 60, valueText: '01:00', highlight: false },
{ label: '五', value: 0, valueText: '', highlight: false },
{ label: '六', value: 0, valueText: '', highlight: false },
{ label: '日', value: 0, valueText: '', highlight: false },
{ label: '一', value: 0, valueText: '', highlight: false },
{ label: '二', value: 0, valueText: '', highlight: false },
{ label: '今天', value: 75, valueText: '01:15', highlight: true }
])
})
test('monthly trend spans a year boundary from a single record snapshot', () => {
const records = {
'2025-08': [{ date: '2025-08-31 08:00:00', duration: 60 }],
'2025-12': [{ date: '2025-12-01 08:00:00', duration: 120 }],
'2026-01': [{ date: '2026-01-02 08:00:00', duration: 3600 }]
}
assert.deepEqual(getTrendData(records, 'month', new Date(2026, 0, 15)), [
{ label: '8月', value: 60, valueText: '01:00', highlight: false },
{ label: '9月', value: 0, valueText: '', highlight: false },
{ label: '10月', value: 0, valueText: '', highlight: false },
{ label: '11月', value: 0, valueText: '', highlight: false },
{ label: '12月', value: 120, valueText: '02:00', highlight: false },
{ label: '1月', value: 3600, valueText: '60m', highlight: true }
])
})
test('bar height remains proportional for very short sessions', () => {
assert.equal(getBarPercent(0, 120), 0)
assert.ok(getBarPercent(1, 120) < 1)
assert.equal(getBarPercent(120, 120), 100)
})
+69
View File
@@ -54,6 +54,73 @@ const formatDate = (date) => {
/** Strip the time portion of a formatDate string → "YYYY-MM-DD". */ /** Strip the time portion of a formatDate string → "YYYY-MM-DD". */
const _dateOnly = (s) => (typeof s === 'string' && s.length >= 10) ? s.substring(0, 10) : '' const _dateOnly = (s) => (typeof s === 'string' && s.length >= 10) ? s.substring(0, 10) : ''
const _normalizeDuration = (value) => Math.max(0, Math.floor(Number(value) || 0))
const _formatTrendDuration = (seconds) => {
const duration = _normalizeDuration(seconds)
if (duration >= 3600) return `${Math.floor(duration / 60)}m`
return formatTime(duration)
}
/** Build daily or monthly duration totals from one training-record snapshot. */
const getTrendData = (recordGroups, mode, now = new Date()) => {
const dateTotals = {}
const groups = recordGroups && typeof recordGroups === 'object' ? recordGroups : {}
Object.values(groups).forEach((records) => {
if (!Array.isArray(records)) return
records.forEach((record) => {
const date = _dateOnly(record && record.date)
if (!date) return
dateTotals[date] = (dateTotals[date] || 0) + _normalizeDuration(record.duration)
})
})
const today = new Date(now)
const weekdays = ['日', '一', '二', '三', '四', '五', '六']
const pad = (n) => String(n).padStart(2, '0')
if (mode === 'month') {
const monthTotals = {}
Object.keys(dateTotals).forEach((date) => {
const month = date.substring(0, 7)
monthTotals[month] = (monthTotals[month] || 0) + dateTotals[date]
})
return Array.from({ length: 6 }, (_, index) => {
const offset = 5 - index
const date = new Date(today.getFullYear(), today.getMonth() - offset, 1)
const monthKey = `${date.getFullYear()}-${pad(date.getMonth() + 1)}`
const value = monthTotals[monthKey] || 0
return {
label: `${date.getMonth() + 1}`,
value,
valueText: value > 0 ? _formatTrendDuration(value) : '',
highlight: offset === 0
}
})
}
return Array.from({ length: 7 }, (_, index) => {
const offset = 6 - index
const date = new Date(today)
date.setDate(date.getDate() - offset)
const dateKey = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
const value = dateTotals[dateKey] || 0
return {
label: offset === 0 ? '今天' : weekdays[date.getDay()],
value,
valueText: value > 0 ? _formatTrendDuration(value) : '',
highlight: offset === 0
}
})
}
const getBarPercent = (value, max) => {
const amount = _normalizeDuration(value)
const maximum = _normalizeDuration(max)
return maximum > 0 ? amount / maximum * 100 : 0
}
/** /**
* Convert a local file path (typically `wxfile://...` from chooseAvatar) to a * Convert a local file path (typically `wxfile://...` from chooseAvatar) to a
* base64 data URI. Used to make temporary file URLs survive the WeChat * base64 data URI. Used to make temporary file URLs survive the WeChat
@@ -101,5 +168,7 @@ module.exports = {
getMonthCalendar, getMonthCalendar,
formatDate, formatDate,
dateOnly: _dateOnly, dateOnly: _dateOnly,
getTrendData,
getBarPercent,
fileToDataURI fileToDataURI
} }