feat: 新增统计视图(ADR-012)
- 新增 /stats 路由与导航入口,与看板、今日视图平级 - 完成热力图:近一年 53 周,按每天完成数固定阈值分五档着色 - 汇总指标:今天 / 连续 / 本月 / 累计;连续天数允许「今天还没开始」 - 「时间花在哪」按标签分布(多标签任务每个标签各计一次) - 「一些细节」:完成周期中位数 / 最长连续 / 有记录天数 / 平均每天 - 统计只从 completedAt 推算,不新增计数表,数字随删除偏少但永不自相矛盾 热力图的布局约束记在 ADR-012 结论 9: - 月份标签绝对定位,避免 whitespace-nowrap 撑宽列而多出横向滚动条 - 富余宽度交给列间隙(justify-between),格子保持 11×11 正方 - 右侧补与星期标签列等宽的留白,使左右对称并给 ring 描边留出余量 新增 utils/activity.ts(纯函数)、views/StatsView.vue 及两个 spec、e2e/stats.spec.ts; 同步更新 README、docs/adr.md 与 AppNav 的三入口断言。
This commit is contained in:
+93
@@ -382,3 +382,96 @@ ADR-007 明确否决了「批量改属性做 `[{id, before}]` 快照式撤销」
|
||||
- 标签圆点带 `data-role="tag-dot"`:卡片上本来就有别的圆点元素(状态点、勾选框),
|
||||
用 `.rounded-full` 这类样式选择器会误伤,单测与 e2e 一律按 `data-role` 定位。
|
||||
- 导出 JSON 不含颜色,README 的已知限制已写明。
|
||||
|
||||
---
|
||||
|
||||
## ADR-012 · 统计视图:完成趋势与热力图
|
||||
|
||||
**背景**
|
||||
|
||||
README 的「已知限制」里长期挂着一条「统计只覆盖本周完成数等基础指标,没有趋势图或历史留存」。
|
||||
而 `completedAt` 从 T03 起就在数据里,除了算「本周完成」和判断归档候选之外没有任何出口 ——
|
||||
用户想知道「我这几周到底做了多少」,只能导出 JSON 自己数。
|
||||
|
||||
ADR-005 在引入路由时就写明:「若未来增加今日视图、统计等真正的独立页面,再引入路由」。
|
||||
统计视图正是那句话里的第二个页面。
|
||||
|
||||
**结论**
|
||||
|
||||
1. **独立路由 `/stats`**,与今日视图平级。今日视图往前看(今天该做什么),统计往回看(做过什么),
|
||||
两者的信息密度和使用时机都不同,塞在同一屏会互相稀释。按 ADR-005 的既有惯例懒加载。
|
||||
2. **数据只从 `completedAt` 推算**,不引入「每日完成计数」表。按本地日期分组,
|
||||
逐条走 `localDateOf` —— `completedAt` 存的是 UTC ISO,直接截前 10 位在东八区会把晚上完成的任务算到前一天。
|
||||
3. **热力图 53 周(一年)**,列为一周、行为周一到周日(周一为一周之始,与 `utils/stats.ts` 口径一致)。
|
||||
先用 26 周试过:只占卡片一半宽,右侧大片留白,且看不出季节性起伏。
|
||||
4. **强度固定阈值分档**(0 / 1 / 2 / 3 / 4+)。按最大值相对分档会让数据一变多就所有格子一起变深,
|
||||
反而看不出「哪天特别猛」;固定阈值可预测,也便于测试。
|
||||
5. **月份标签只打在该月第一周**(周一落在 1–7 号)。最初用「月份变了就打」,结果 4 月与 5 月
|
||||
这种相邻两周各打一个,而每列只有 14px 宽,两个标签直接叠在一起。每月一次后间隔必然 ≥ 4 列。
|
||||
6. **连续完成天数允许「今天还没开始」**:今天没有记录就从昨天起算。否则每天零点一过 streak 归零,
|
||||
等于在用户还没开始工作的时候先把成绩抹掉。
|
||||
7. 回收站里的任务不计入(软删除的语义是「这件事不算了」),已归档的计入(归档只是隐藏)。
|
||||
8. 没有完成记录时仍然渲染完整网格,只把脚注换成引导文案 —— 空白的热力图本身就在表达「还没有开始」。
|
||||
9. **月份标签绝对定位**。53 列时容器 `clientWidth` 814px、`scrollWidth` 828px,凭空多出一条横向滚动条。
|
||||
根因不是格子尺寸(11px + 3px 间距本是按 814px 排的),而是月份标签的 `whitespace-nowrap` 把所在列
|
||||
从 11px 撑到 20px 以上 —— 流内文字参与了列宽计算。改为 `absolute` 定位后列宽恒定,原位置只留
|
||||
`h-[14px]` 占位块保住行高,1280 / 1024 / 900 三种宽度下 `scrollWidth === clientWidth`。
|
||||
**_2026-10-25 修正_**:当时用 `inline-flex w-fit mx-auto` 想让整块居中,但 `inline-flex` 是**行内级**
|
||||
元素,auto margin 在水平方向按 0 处理,居中根本没生效 —— 实测 814 的容器里左边贴 15px、右边空 60px,
|
||||
整块偏左,右侧一大块空白。
|
||||
改为让周列容器 `flex-1` 占满剩余宽度、**`justify-between` 把富余空间均分到 52 个间隙**:
|
||||
宽屏时间隙从 3px 涨到 4.15px,格子保持 11×11 正方。
|
||||
窄屏用 `minWidth: weeks × 14 − 3`(53 列 = 739px)兜住 3px 最小间隙,装不下就横向滚动 ——
|
||||
格子尺寸在任何宽度下都不变,只是间隙变化。
|
||||
**_同日二次修正_**:铺满后最右列顶死在容器右边界(`rightGap = 0`),把今天那格的 `ring-1` 切掉一条 ——
|
||||
`ring` 是画在元素外侧的 box-shadow,超出 `overflow-x-auto` 边界即被裁(实测超出 `innerRight` 恰好 1px)。
|
||||
给内容补 `pr-[15px]`,即左侧星期标签列的镜像(`w-3` 12px + 父级 gap 3px),
|
||||
方块阵列距内容左右边界各 15px,几何上真正对称,同时给描边留出余量(`ringOverflow` 由 +1 变 −14)。
|
||||
`pt-[1px]` 同理,为落在最上面一行的 today 描边留出上侧空间。
|
||||
_不靠 `justify-center` 居中_:内容超宽时它会从左边缘开始裁,第一列看不见。
|
||||
_不把富余宽度摊给格子_:那会让格子变成 12.1×11 的扁方块,14px 见方的阵列里 1px 的偏差是能看出来的。
|
||||
_不给 `overflow` 容器直接加 `padding-right`_:历史实现里 padding 不计入 `scrollWidth`,
|
||||
放在内层 flex 上可以绕开这个差异。
|
||||
10. **热力图之后补两块内容**:标签分布(「时间花在哪」)与四项细节指标(平均完成周期 / 最长连续 /
|
||||
有记录 / 平均每天)。理由:热力图只回答「哪几天动了手」,回答不了「时间花在什么方向」。
|
||||
两块都是既有数据的第二种切法,没有引入新存储。
|
||||
11. **标签分布按「每个标签各计一次」**,而不是把任务分桶到某个标签。一个任务同时属于「后端」和「安全」,
|
||||
对两个方向都算贡献;因此各项之和会大于完成任务数,这一点在卡片脚注与 README 里都写明,
|
||||
免得用户拿它跟「累计完成」对不上账。
|
||||
12. **完成周期取中位数**,并且不足一天显示「当天」。平均值会被一条躺了半年的任务拉到 60 天以上,
|
||||
中位数仍然描述「大多数活多久能做完」。负数跨度(时钟回拨或被手工改过的数据)直接跳过。
|
||||
13. **「平均每天」按有记录的天数算**,不按自然日。按自然日会把周末与休假算成效率下降。
|
||||
|
||||
**被否决的方案**
|
||||
|
||||
- _新增 `dailyCompletions` 计数表_:能得到不随删除变化的准确历史,代价是「两份真相」——
|
||||
恢复一条被删的任务要不要把计数加回去?撤销删除、合并导入、快照恢复都会撞上这个问题。
|
||||
宁可数字偏保守,也不要出现「热力图说完成了 5 个,任务列表只有 3 个」。
|
||||
- _把热力图放进今日视图顶部_:今日视图腾不开(一排 7 个分组 + 统计卡片已经够满),
|
||||
而且它回答的不是「今天该做什么」。做成独立页面才能给足高度和一年的宽度。
|
||||
- _画柱状趋势图_:热力图本身就是趋势的可视化,再叠一个按周聚合的柱状图是同一组数据的第二遍表达。
|
||||
确实要看「最近几周在变多还是变少」时,热力图右侧那几列的深浅已经能看出来。
|
||||
- _按最大值相对分档上色_:见结论 4。
|
||||
- _给热力图每个格子加独立的可访问名称_:一年 371 个格子逐个朗读只会变成噪音,
|
||||
改为容器一个 `role="img"` 汇总「有多少天产生过记录、累计多少个」,格子本身 `aria-hidden`,
|
||||
悬停的 `title` 作为视觉用户的补充。
|
||||
- _把统计入口做成今日视图里的一个按钮_:不可深链、无法收藏,PWA 的 shortcuts 也没有落点。
|
||||
- _完成周期用平均值_:见结论 12。平均值仍然算出来留在 `completionCycle().mean` 里供后续使用,
|
||||
只是不作为主指标展示 —— 将来若要做「异常长尾」的提示,那个字段就是入口。
|
||||
- _标签分布用饼图_:六个标签的饼图无法按长度比较,且标签名必须挤在图例里。横向条形按长度对齐,
|
||||
标签名左侧留白足够,还能顺便带上标签自己的颜色圆点。
|
||||
- _压缩热力图格子来消除滚动条_:把 11px 压到 8px 确实能塞下,但 8px 的方块已经分不清是「一格」
|
||||
还是「两个挨着的格子」。滚动条是果,列宽被文字撑开才是因,改因不改果。
|
||||
|
||||
**影响**
|
||||
|
||||
- 新增 `src/utils/activity.ts`(纯函数:分组、热力图排布、汇总、连续天数、标签分布、完成周期、
|
||||
最长连续)与 `src/views/StatsView.vue`。
|
||||
组件的职责只有取数与渲染,所有口径都在纯函数里,时间可注入,便于测跨周与跨月的边界。
|
||||
- 路由新增 `/stats`;`AppNav` 的入口从两个变三个,`AppNav.spec.ts` 的断言同步更新。
|
||||
- 热力图格子带 `data-role="heatmap-cell"` 与 `data-date` / `data-count` / `data-level`,
|
||||
标签条形带 `data-role="tag-bar"` 与 `data-tag` / `data-count`:
|
||||
单测和 e2e 都靠它定位,避免依赖 `size-[11px]` 这类会被改动的样式类。
|
||||
- 窄屏(< 768px)热力图仍然横向滚动,这是有意的:一年 53 列在手机上无法在不失真的前提下压缩。
|
||||
- 数值会随删除而减少。这一点写进了 README 的已知限制,也写在统计页脚注里 ——
|
||||
用户看到数字变少时能立刻知道原因,而不是怀疑数据坏了。
|
||||
|
||||
Reference in New Issue
Block a user