From ca1793b4c075ea966c9714b319a176f99f9f64d9 Mon Sep 17 00:00:00 2001 From: cnliucheng Date: Tue, 4 Aug 2026 15:46:18 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=B8=AA=E4=BA=BA=E6=9C=80=E4=BD=B3?= =?UTF-8?q?=E8=B6=8B=E5=8A=BF=E6=94=B9=E4=B8=BA=E6=8A=98=E7=BA=BF=E5=9B=BE?= =?UTF-8?q?=20+=20=E5=BA=95=E9=83=A8=E6=9C=80=E4=BD=B3=E8=B6=8B=E5=8A=BF?= =?UTF-8?q?=E6=97=A5=E6=9C=9F=20+=20=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 趋势组件 trend-chart 新增 chartType(line/bar),line 模式用内联 SVG 渲染折线+渐变面积(无 canvas) - 记录页个人最佳趋势改用折线图,峰值信息移到图表下方对称高亮块显示最佳趋势产生日期 - 修复1: linePoints 漏存绝对坐标 y 导致运行时 toFixed 崩溃 - 修复2: 折线模式漏渲染数据点数值(valueText),现圆点上方显示且 0 值不显示 --- .workbuddy/memory/2026-08-04.md | 29 +++++++++ .workbuddy/memory/MEMORY.md | 1 + components/trend-chart/trend-chart.js | 81 +++++++++++++++++++++---- components/trend-chart/trend-chart.wxml | 34 ++++++++--- components/trend-chart/trend-chart.wxss | 77 +++++++++++++++++++++++ pages/records/records.js | 17 +++++- pages/records/records.wxml | 8 ++- pages/records/records.wxss | 6 -- 8 files changed, 222 insertions(+), 31 deletions(-) diff --git a/.workbuddy/memory/2026-08-04.md b/.workbuddy/memory/2026-08-04.md index b494c61..8ec5c51 100644 --- a/.workbuddy/memory/2026-08-04.md +++ b/.workbuddy/memory/2026-08-04.md @@ -150,3 +150,32 @@ - 清残留:删 preview-buttons.html + rmdir cloudfunctions/shared(空死目录,重构遗留);.claude/.codegraph 已被 packOptions.ignore 的 `.!*` 忽略,不动。 - 未做:全局错误上报(wx.onError)、自定义隐私弹窗(用户仅确认做这三个清理项)。 - 改动未提交(git status 显示 M/D 文件)。clean-console.js 为一性工具,留 .workbuddy 不进包。 + +## 上线前优化续:组件按需注入 + records 补声明 + 提交推送 +- 微信开发者工具「代码质量」扫描显示「组件:启用组件按需注入 — 未通过」。 +- 根因:project.config.json 缺 `lazyCodeLoading` 配置项。已加 `"lazyCodeLoading": "requiredComponents"`(只加载页面 usingComponents 声明且模板实际用到的组件)。 +- 核实:7 组件均静态写死在 WXML,无动态 ``** 渲染折线+半透明面积(无 canvas,和组件 no-canvas 理念一致);圆点与横轴标签用 WXML 绝对定位(按 xPercent/yPercent),保证清晰且能吃主题色(因 SVG 在 image 内读不到 CSS 变量,颜色由页面传 hex)。 +- 几何:viewBox=640×H(H=peak?170:200),`mode=widthFix` 让 SVG 宽高比与容器 rpx 一致→stroke 均匀不拉伸;x 按 (i+0.5)*640/N 均分,y 用 topPad/bottomPad 留头尾余量;零值点落到接近底部。 +- 数据层不动(getBestTrendData 仍返回 label=周一/7月 等周期标签,符合"就像最高训练日"的粒度)。 +- records 页:best 趋势 ``;原在 header 的 `best-trend-best` 文本移到图表下方,复用 `.trend-highlight` 对称块(icons.crownFill + bestTrendSummary.peakTitle + peakText);新增 `_buildBestSummary` 把 peakTitle 覆盖为「个人最佳日/月」(通用 getTrendSummary 原给"最高训练日",不对);bestLineColor=theme.primary(peak 实际用主题主色,非金)。 +- 验证:node --check 通过;tests/trend-state.test.js 5/5 通过(数据层未变);SVG 字符串模拟确认无 NaN、含 stroke/fill-opacity/width/height。删了 records.wxss 里已不用的 .best-trend-best 样式。 +- 改动未提交(git status 有 M)。待用户审阅后可提交(soao 国内远程稳定;origin 仍待网络恢复推)。 + +## BUG 修复:折线图运行时崩溃(toFixed of undefined) +- 现象:记录页切换 trend 模式报 `Cannot read property 'toFixed' of undefined`(trend-chart.js:90, _buildLineSvg 内 `p.y.toFixed`)。 +- 根因:`_compute` 生成 linePoints 时只存了 `x`(绝对)与 `yPercent`(百分比),**漏存绝对坐标 `y`**;`_buildLineSvg` 画 polyline/area 用的是 viewBox 绝对坐标 `p.x`/`p.y`,`p.y` 为 undefined → 崩。WXML 圆点/标签用 xPercent/yPercent 定位(无误)。 +- 修复:linePoints 返回对象补 `y`(与 x 同为 viewBox 坐标);`_buildLineSvg` 加防御 `valid = points.filter(p => p && typeof p.x==='number' && typeof p.y==='number')`,first/last/area/polyline 全改走 `valid`,空则返 ''。 +- 验证:node --check 通过;用真实 getBestTrendData 输出实跑 _compute(week/month)均生成 lineSvg 不再崩;稀疏数组也安全。临时 repro 脚本已删。改动未提交。 + +## BUG 修复:折线图数据点未显示数值 +- 现象:折线模式每个数据点旁没有数值文字(柱状图有,折线没有)。 +- 根因:line 模式 WXML 只渲染了圆点(`trend-line-dot`)与横轴 label,**漏渲染 valueText**;且 linePoints 返回对象未透传 `value` 字段,无法做 `value>0` 判断。 +- 修复:trend-chart.js 的 linePoints 返回补 `value: b.value`(bars 项第57行本就有 value);WXML 圆点内加 `{{item.valueText}}`(仿柱状图,0 值不显示);WXSS 加 `.trend-line-value`(绝对定位在 dot 上方居中,16rpx,高亮点用主色加粗),并把 `.trend-chart.line` 的 padding-top 16rpx→30rpx 给最高点数值留缓冲防裁切。 +- 验证:node --check 通过;bars.value 字段确认存在。改动未提交。 diff --git a/.workbuddy/memory/MEMORY.md b/.workbuddy/memory/MEMORY.md index ae2d6ce..d453c10 100644 --- a/.workbuddy/memory/MEMORY.md +++ b/.workbuddy/memory/MEMORY.md @@ -27,3 +27,4 @@ - 页面叫法:index=训练首页,timer=训练页,leaderboard=排行榜,records=记录,settings=设置。 - 美化按"见效快优先"分批,每轮 `node --check` + grep 残留再交付。 - **git push 须 `dangerouslyDisableSandbox:true`**(沙箱隔离致 github SSL 超时);无 SSH,仅 HTTPS remote。 +- **GitHub 链路不稳**:国内环境到 `github.com:443` 常 SSL 重置/HTTP2 framing 失败(非代码问题),`git.soao.net`(soao) 国内远程稳定。策略:soao 为主远程、实时同步;`origin`(GitHub) 失败则延后推或本地代理(`127.0.0.1:7890` 等)临时 `git -C config http.proxy` 推完 `config --unset` 撤销。代码已 commit 则零丢失风险。 diff --git a/components/trend-chart/trend-chart.js b/components/trend-chart/trend-chart.js index a55e3ce..28314d6 100644 --- a/components/trend-chart/trend-chart.js +++ b/components/trend-chart/trend-chart.js @@ -1,38 +1,53 @@ const { getBarPercent } = require('../../utils/util') /** - * Trend bar chart - pure CSS (no canvas), matches the calendar-heatmap's - * rendering approach for consistency. + * Trend chart — pure CSS bar mode + optional SVG line mode (no canvas), + * matching the calendar-heatmap's rendering approach for consistency. * * chartData: [{ label, value, valueText, highlight }] * - label: bottom caption (e.g. "一" / "7月") - * - value: numeric magnitude (seconds); drives bar height + * - value: numeric magnitude (seconds); drives bar height / line y * - valueText: formatted string shown above the bar (e.g. "1分30秒") * - highlight: truthy -> theme glow + bold label (today / this month) * - * Bar height is strictly proportional to value/max. Zero values render a - * faint stub so empty dates remain visible without distorting the data. + * chartType: 'bar' (default) | 'line' + * - bar: CSS columns, height strictly proportional to value/max. + * - line: inline SVG (data URI) polyline + translucent area, rendered in an + * ; dots + axis labels are real WXML elements positioned by + * percentage so they stay crisp and theme-colored (SVG inside + * cannot read page CSS variables). The viewBox aspect is matched to + * the container's rpx aspect (widthFix) so the stroke stays uniform + * — no non-uniform horizontal/vertical stretch. + * + * lineColor: hex used for the SVG stroke + area fill. Must be passed by the + * page because SVG inside cannot read page CSS variables. */ +const VB_W = 640 + Component({ properties: { chartData: { type: Array, value: [] }, - variant: { type: String, value: 'total' } + variant: { type: String, value: 'total' }, + chartType: { type: String, value: 'bar' }, + lineColor: { type: String, value: '#FF6B35' } }, data: { - bars: [] + bars: [], + linePoints: [], + lineSvg: '' }, observers: { - 'chartData'(chartData) { - this._compute(chartData) + 'chartData, chartType, lineColor'(chartData, chartType, lineColor) { + this._compute(chartData, chartType, lineColor) } }, lifetimes: { ready() { - this._compute(this.properties.chartData) + this._compute(this.properties.chartData, this.properties.chartType, this.properties.lineColor) } }, methods: { - _compute(chartData) { + _compute(chartData, chartType, lineColor) { const arr = Array.isArray(chartData) ? chartData : [] const max = arr.reduce((m, d) => Math.max(m, Number(d && d.value) || 0), 0) const bars = arr.map((d) => { @@ -45,7 +60,49 @@ Component({ percent: getBarPercent(v, max) } }) - this.setData({ bars }) + + const N = arr.length + let linePoints = [] + let lineSvg = '' + if (N > 0 && chartType === 'line') { + const lineH = this.properties.variant === 'peak' ? 170 : 200 + const topPad = 16 + const bottomPad = 12 + const plot = lineH - topPad - bottomPad + linePoints = bars.map((b, i) => { + const x = (i + 0.5) * (VB_W / N) + const y = topPad + (1 - b.percent / 100) * plot + return { + x, + y, + xPercent: (x / VB_W) * 100, + yPercent: (y / lineH) * 100, + label: b.label, + valueText: b.valueText, + value: b.value, + highlight: b.highlight + } + }) + lineSvg = this._buildLineSvg(linePoints, VB_W, lineH, lineColor || '#FF6B35') + } + this.setData({ bars, linePoints, lineSvg }) + }, + + _buildLineSvg(points, W, H, color) { + const valid = (points || []).filter(p => p && typeof p.x === 'number' && typeof p.y === 'number') + if (valid.length === 0) return '' + const pts = valid.map(p => `${p.x.toFixed(2)},${p.y.toFixed(2)}`).join(' ') + const first = valid[0] + const last = valid[valid.length - 1] + let area = `M ${first.x.toFixed(2)},${H} ` + valid.forEach((p) => { area += `L ${p.x.toFixed(2)},${p.y.toFixed(2)} ` }) + area += `L ${last.x.toFixed(2)},${H} Z` + const svg = + `` + + `` + + `` + + `` + return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg) } } }) diff --git a/components/trend-chart/trend-chart.wxml b/components/trend-chart/trend-chart.wxml index d70ff78..3ba2a7f 100644 --- a/components/trend-chart/trend-chart.wxml +++ b/components/trend-chart/trend-chart.wxml @@ -1,12 +1,28 @@ - - - - - {{item.valueText}} + + + + + + + {{item.valueText}} + - - - {{item.label}} - + + {{item.label}} + + + + + + + + + {{item.valueText}} + + + + {{item.label}} + + diff --git a/components/trend-chart/trend-chart.wxss b/components/trend-chart/trend-chart.wxss index 8fcb665..788e173 100644 --- a/components/trend-chart/trend-chart.wxss +++ b/components/trend-chart/trend-chart.wxss @@ -92,3 +92,80 @@ color: var(--primary); font-weight: 600; } + +/* ---- Line mode ---- */ +.trend-chart.line { + /* extra top room so the value caption above the highest dot isn't clipped */ + padding-top: 30rpx; +} + +.trend-line { + position: relative; + width: 100%; +} + +.trend-line-img { + display: block; + width: 100%; +} + +.trend-line-dot { + position: absolute; + width: 16rpx; + height: 16rpx; + margin-left: -8rpx; + margin-top: -8rpx; + border-radius: 50%; + background: var(--primary); + border: 3rpx solid var(--card-bg, #FFFFFF); + box-sizing: border-box; +} + +.trend-chart.peak .trend-line-dot { + background: var(--primary); +} + +.trend-line-dot.highlight { + width: 20rpx; + height: 20rpx; + margin-left: -10rpx; + margin-top: -10rpx; + box-shadow: 0 0 14rpx rgba(var(--primary-rgb), 0.6); +} + +/* value caption floats above the dot so it never eats into the line */ +.trend-line-value { + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + margin-bottom: 6rpx; + font-size: 16rpx; + color: var(--text-secondary); + line-height: 1; + white-space: nowrap; +} + +.trend-line-dot.highlight .trend-line-value { + color: var(--primary); + font-weight: 600; +} + +.trend-line-labels { + position: relative; + height: 28rpx; + margin-top: 10rpx; +} + +.trend-line-label { + position: absolute; + transform: translateX(-50%); + font-size: 20rpx; + color: var(--text-secondary); + white-space: nowrap; +} + +.trend-line-label.highlight { + color: var(--primary); + font-weight: 600; +} diff --git a/pages/records/records.js b/pages/records/records.js index 9dd28bd..bb440ba 100644 --- a/pages/records/records.js +++ b/pages/records/records.js @@ -35,6 +35,7 @@ Page({ trendSummary: {}, bestTrendData: [], bestTrendSummary: {}, + bestLineColor: '#FF6B35', showDayDetail: false, dayDetail: {}, icons: iconsMod.build() @@ -138,7 +139,8 @@ Page({ trendData, trendSummary: { ...util.getTrendSummary(trendData, trendMode), streakCount: Number(streak.count) || 0 }, bestTrendData, - bestTrendSummary: util.getTrendSummary(bestTrendData, trendMode), + bestTrendSummary: this._buildBestSummary(bestTrendData, trendMode), + bestLineColor: theme.primary, loading: false }) }, @@ -199,7 +201,8 @@ Page({ trendData, trendSummary: { ...util.getTrendSummary(trendData, mode), streakCount: Number(storage.getStreak().count) || 0 }, bestTrendData, - bestTrendSummary: util.getTrendSummary(bestTrendData, mode) + bestTrendSummary: this._buildBestSummary(bestTrendData, mode), + bestLineColor: themeMod.getCurrentTheme().primary }) }, @@ -217,6 +220,16 @@ Page({ return util.getBestTrendData(storage.getRecords(), mode) }, + /** + * 个人最佳趋势的峰值摘要:复用通用 getTrendSummary,但把 peakTitle 从 + * "最高训练日/月" 覆盖成 "个人最佳日/月",贴合"个人最佳趋势"区块语义。 + */ + _buildBestSummary(bestTrendData, mode) { + const s = util.getTrendSummary(bestTrendData, mode) + s.peakTitle = mode === 'month' ? '个人最佳月' : '个人最佳日' + return s + }, + onDayTap(e) { const { year, month, day } = e.detail const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}` diff --git a/pages/records/records.wxml b/pages/records/records.wxml index da05d91..4a0f129 100644 --- a/pages/records/records.wxml +++ b/pages/records/records.wxml @@ -125,9 +125,13 @@ 个人最佳趋势 - {{bestTrendSummary.peakText}} - + + + + {{bestTrendSummary.peakTitle}} + {{bestTrendSummary.peakText}} + diff --git a/pages/records/records.wxss b/pages/records/records.wxss index 1c7c7fa..ef011e7 100644 --- a/pages/records/records.wxss +++ b/pages/records/records.wxss @@ -360,12 +360,6 @@ font-weight: 700; } -.best-trend-best { - color: var(--primary); - font-size: 20rpx; - font-weight: 600; -} - /* ---- history ---- */ .history-section { padding: 0;