Files
wx_pbzc/pages/records/records.js
T
lc 75bad477b1 feat(records): 训练趋势图表 + 列表时长改 MM:SS
- 新增 trend-chart 柱状图组件(CSS,近7天/近6月切换)
- records 页加训练趋势卡片(统计卡片后)
- 训练记录列表时长从秒数改为 MM:SS 显示

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-28 11:19:07 +08:00

250 lines
7.3 KiB
JavaScript

const storage = require('../../utils/storage')
const util = require('../../utils/util')
const themeMod = require('../../utils/theme')
const iconsMod = require('../../utils/icons')
const config = require('../../config')
Page({
data: {
theme: { primary: '#FF6B35', primaryLight: '#FF8C5A', primaryBg: '#FFF3ED', primaryRgb: '255,107,53' },
themeStyle: themeMod.BASE_VARS,
loading: true,
totalDuration: 0,
totalDurationText: '',
totalSessions: 0,
maxDuration: 0,
maxDurationText: '',
currentYear: new Date().getFullYear(),
currentMonth: new Date().getMonth() + 1,
monthRecords: [],
historyList: [],
listMode: 'month',
trendMode: 'week',
trendData: [],
showDayDetail: false,
dayDetail: {},
icons: iconsMod.build()
},
onLoad() {
themeMod.applyThemeToPage(this)
this._firstShow = true
},
onShow() {
themeMod.applyThemeToPage(this)
try {
const tb = this.getTabBar()
if (tb) tb.setData({ selected: 1 })
} catch (e) {}
if (this._firstShow) {
this._firstShow = false
this.refresh()
return
}
this.refresh()
},
onPullDownRefresh() {
this.refresh()
wx.stopPullDownRefresh()
},
refresh() {
storage.validateStreak()
const stats = storage.getTotalStats()
const monthKey = `${this.data.currentYear}-${String(this.data.currentMonth).padStart(2, '0')}`
const records = (storage.getRecordsByMonth(monthKey) || []).map(r => ({ ...r, durationText: util.formatTime(r.duration) }))
// Each month is already sorted newest-first in storage. To get top 30 most recent,
// walk months in reverse chrono order and concat — O(months*30) instead of O(n log n).
const byMonth = storage.getRecords()
const monthKeys = Object.keys(byMonth).sort().reverse()
const historyList = []
for (const mk of monthKeys) {
for (const r of byMonth[mk]) {
historyList.push({ ...r, durationText: util.formatTime(r.duration) })
if (historyList.length >= 30) break
}
if (historyList.length >= 30) break
}
const theme = themeMod.getCurrentTheme()
this.setData({
theme,
icons: iconsMod.build(theme),
totalDuration: stats.totalDuration,
totalDurationText: util.formatDuration(stats.totalDuration),
totalSessions: stats.totalSessions,
maxDuration: stats.maxDuration,
maxDurationText: util.formatDuration(stats.maxDuration),
monthRecords: records,
historyList,
listMode: this.data.listMode || 'month',
displayList: (this.data.listMode || 'month') === 'month' ? records : historyList,
trendData: this._computeTrend(this.data.trendMode || 'week'),
loading: false
})
},
onPrevMonth() {
let { currentYear, currentMonth } = this.data
if (currentMonth === 1) {
currentYear--
currentMonth = 12
} else {
currentMonth--
}
this.setData({ currentYear, currentMonth })
this.refresh()
},
onNextMonth() {
let { currentYear, currentMonth } = this.data
if (currentMonth === 12) {
currentYear++
currentMonth = 1
} else {
currentMonth++
}
this.setData({ currentYear, currentMonth })
this.refresh()
},
onToggleListMode(e) {
const mode = e.currentTarget.dataset.mode
if (!mode || mode === this.data.listMode) return
this.setData({ listMode: mode })
this.refresh()
},
onToggleTrendMode(e) {
const mode = e.currentTarget.dataset.mode
if (!mode || mode === this.data.trendMode) return
this.setData({ trendMode: mode, trendData: this._computeTrend(mode) })
},
/**
* 训练趋势数据:
* week - 近 7 天每天的训练时长(秒)
* month - 近 6 个月每月的训练时长(秒)
* 返回 [{ label, value, valueText, highlight }] 供 trend-chart 渲染。
*/
_computeTrend(mode) {
const allRecords = Object.values(storage.getRecords()).flat()
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) {
const { year, month, day } = e.detail
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`
const allRecords = Object.values(storage.getRecords()).flat()
// record.date 是 "YYYY-MM-DD HH:MM:SS"(formatDate 生成),dateStr 只有
// 日期部分,直接 === 永远不匹配,必须用 dateOnly 截取后再比。
const sessions = allRecords.filter(r => util.dateOnly(r.date) === dateStr)
const totalDur = sessions.reduce((sum, r) => sum + r.duration, 0)
const calories = Math.round(totalDur * 0.068)
this.setData({
showDayDetail: true,
dayDetail: {
date: dateStr,
sessions: sessions.length,
duration: totalDur,
durationText: util.formatDuration(totalDur),
calories
}
})
},
onCloseDayDetail() {
this.setData({ showDayDetail: false })
},
// Swallow touchmove on the popup so the background page doesn't scroll.
onNoop() { /* swallow */ },
onLongPressDelete(e) {
this.onDeleteRecord(e)
},
onDeleteRecord(e) {
const id = e.currentTarget.dataset.id
if (!id && id !== 0) {
wx.showToast({ title: '记录数据异常', icon: 'none', duration: 1200 })
return
}
wx.showModal({
title: '删除记录',
content: '确定要删除这条训练记录吗?',
success: (res) => {
if (res.confirm) {
storage.deleteRecord(id)
this.refresh()
wx.showToast({ title: '已删除', icon: 'none', duration: 1200 })
}
}
})
},
/**
* 分享给朋友 — 训练记录页。把累计训练时长带进标题,
* 让分享卡片有点击欲(看到对方"已经练了 X 小时"会想点开)。
*/
onShareAppMessage() {
const stats = storage.getTotalStats()
const minutes = Math.round((stats.totalDuration || 0) / 60)
const s = config.share.records
return {
title: s.titleTemplate.replace('{minutes}', minutes),
path: s.path
}
},
/**
* 分享到朋友圈。
*/
onShareTimeline() {
return {
title: config.share.timelineTitle
}
}
})