From 6b2e4d86669a40c10e4aeeb367d9b2f76177449a Mon Sep 17 00:00:00 2001 From: wait4xx Date: Wed, 4 Nov 2026 20:09:54 +0800 Subject: [PATCH] =?UTF-8?q?feat(opensource):=20=E7=83=AD=E5=8A=9B=E5=9B=BE?= =?UTF-8?q?=E4=BA=94=E6=96=B9=E6=A1=88=E5=8A=A8=E6=95=88=E9=A2=84=E8=A7=88?= =?UTF-8?q?=E4=B8=8A=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户要求「热力图可以有更多的动效,而不是静态的一张图」,先上线五案供选型: A 入场波纹 / B 光标光晕 / C 年度回放 / D 连续电流 / E 悬停周详情。 均守灰阶红线、reduced-motion 静态化、零第三方依赖。 验证: 构建 exit 0;content-lint 52 页 0/0/0;Playwright 逐案实测动效均生效 (A hmRise 0.42s、B 光晕读数、C 第 10 周回放、D 12 格 hmFlow、E 整列 lift) Spec: none (预览页需先上线供用户选择) Co-authored-by: GLM-5.3 --- scripts/build-perf/content-lint.mjs | 4 + src/components/geek/HeatGrid.astro | 58 ++++ src/pages/heatmap-preview.astro | 400 ++++++++++++++++++++++++++++ 3 files changed, 462 insertions(+) create mode 100644 src/components/geek/HeatGrid.astro create mode 100644 src/pages/heatmap-preview.astro diff --git a/scripts/build-perf/content-lint.mjs b/scripts/build-perf/content-lint.mjs index cbeb089..f711ccb 100644 --- a/scripts/build-perf/content-lint.mjs +++ b/scripts/build-perf/content-lint.mjs @@ -16,6 +16,9 @@ if (MODE === 'skip') { const SKIP_PREFIX = ['/admin/', '/photos-originals/']; // admin=CMS 路由;photos-originals=部署期 rsync 叠加层(不在 dist,另有部署链保障) const SKIP_FILE = new Set(['/404.html']); // Astro 404 页不参与配对 +// 一次性预览路由(无 en 对应页):完全不参与检查。用户选定方案后随路由一起删除。 +const SKIP_ALL = new Set(['/heatmap-preview/index.html']); + function walk(dir, out = []) { for (const e of readdirSync(dir)) { @@ -50,6 +53,7 @@ const warns = []; for (const f of files) { const rel = f.slice(DIST.length) || '/'; + if (SKIP_ALL.has(rel)) continue; const html = readFileSync(f, 'utf8'); // 站内 :仅根相对路径;#锚点/mailto/tel/外部/data: 全跳过 diff --git a/src/components/geek/HeatGrid.astro b/src/components/geek/HeatGrid.astro new file mode 100644 index 0000000..a5fedc9 --- /dev/null +++ b/src/components/geek/HeatGrid.astro @@ -0,0 +1,58 @@ +--- +/** + * 热力图网格(可复用渲染件):给 5 个动效方案预览共用,避免把 371 个格子抄五遍。 + * + * 每格带 `data-c/--c`(列=周)与 `data-r/--r`(行=星期),动效方案靠这两个自定义属性 + * 算错峰延迟;`data-l` 是灰阶档(0–4,复用 tokens 的 --ink-1..4)。 + * 全部格子 aria-hidden:语义由外层 role="img" + aria-label 概括(371 个 aria 节点是噪音)。 + */ +import { levelOf, type HeatDay } from '../../lib/heatmap'; + +interface Props { + weeks: HeatDay[][]; + /** 网格类型,仅用于选择器命名(不动行为) */ + variant: string; + /** 单元格错峰延迟基数(ms),0 = 不设延迟 */ + step?: number; +} + +const { weeks, variant, step = 0 } = Astro.props; +--- + +
+ {weeks.map((week, ci) => + week.map((day, ri) => ( +
+ + diff --git a/src/pages/heatmap-preview.astro b/src/pages/heatmap-preview.astro new file mode 100644 index 0000000..a52e314 --- /dev/null +++ b/src/pages/heatmap-preview.astro @@ -0,0 +1,400 @@ +--- +/** + * 热力图动效方案预览(Phase 5j):一页并陈 5 个方案,供用户点选。 + * + * 为什么单开预览路由:热力图的"动"是纯审美 + 体感决策,改错要回滚一轮; + * 一页五案 + 页顶锚点,用户在任何设备上点开就能比。选定后我把选中方案 + * 落进 Heatmap.astro,然后删掉本路由。 + * + * 设计红线(五个方案都守):灰阶骨架、只允许 accent 做强调、prefers-reduced-motion + * 下一律退化为静态、零第三方依赖、不引入持续 rAF 循环(省电)。 + */ +import BaseLayout from '../layouts/BaseLayout.astro'; +import HeatGrid from '../components/geek/HeatGrid.astro'; +import { loadHeatmap, levelOf, type HeatDay } from '../lib/heatmap'; + +const { weeks, total } = await loadHeatmap(); +const days: HeatDay[] = weeks.flat(); +const buildTime = new Date().toISOString().slice(0, 10); + +/* ── 连续贡献(本地计算,替代此前构建期抓取的 streak.svg 快照)── */ +const countOf = new Map(days.map((d) => [d.date, d.count])); +const today = new Date(); +const isoOf = (d: Date) => d.toISOString().slice(0, 10); +// 「当前连续」按最后一个有贡献的日子往回数:今天尚未贡献不该让连续归零(GitHub 同口径), +// 若严格按"今天必须是活跃日"计算,早上访问本站会看到 0,与直觉冲突。 +let current = 0; +const lastActive = [...days].reverse().find((d) => d.count > 0)?.date ?? ''; +if (lastActive) { + const start = new Date(`${lastActive}T00:00:00Z`); + for (let i = 0; ; i++) { + const d = new Date(start); + d.setUTCDate(start.getUTCDate() - i); + const c = countOf.get(isoOf(d)) ?? 0; + if (c > 0) current++; + else break; + } +} +let longest = 0; +let run = 0; +for (const d of days) { + run = d.count > 0 ? run + 1 : 0; + if (run > longest) longest = run; +} +const bestDay = days.reduce((a, b) => (b.count > a.count ? b : a), days[0]); +const activeDays = days.filter((d) => d.count > 0).length; + +const stats = { total, current, longest, best: bestDay?.count ?? 0, bestDate: bestDay?.date ?? '', activeDays }; +--- + + +

热力图:五个动效方案

+

+ 同一份真实数据(近一年 {total} 次贡献 · 活跃 {stats.activeDays} 天 · 当前连续 + {stats.current} 天 · 最长 {stats.longest} 天),数据更新于 {buildTime}(构建期快照)。 +
+ 看完告诉我选哪个(回「A」「B」「C」「D」「E」,也可以说「C 但速度慢一点」)。 + 五个方案都守灰阶红线、都尊重「减少动态效果」系统设置。 +

+ +
+ + +
+
+ A +

入场波纹

+

滚动到热力图时,格子像水面一样按「周 + 星期」斜向漫过、逐个亮起(约 0.9s)。 + 优点有生命感、不打扰、只播一次;代价纯装饰,不提供额外信息。

+
+
+ +
+
+ + +
+
+ B +

光标光晕

+

鼠标在网格上移动时,一小片光晕跟着走,被照到的格子更亮,右下角浮出「日期 · 贡献数」。 + 优点能精确读数、零轮播;代价触屏设备没有指针,需退化为点按读数。

+
+
+ + +
+

把指针移到格子上 →

+
+ + +
+
+ C +

年度回放

+

拖动滑块从头"重放"这一年,累计贡献数跟着涨;也可点播放自动扫过。 + 优点最有叙事感、能直观感受节奏;代价多一层交互,需要一点耐心。

+
+
+ +
+
+ + + 累计 {total} 次 · 第 {weeks.length} 周 +
+
+ + +
+
+ D +

连续电流

+

当前连续的那几天会有一条光带从最早一格流到最新一格,像电流经过;旁边显示连续天数。 + 优点把"坚持"这件事讲成故事;代价只覆盖最近一小段,全年的信息仍靠静态格子。

+
+
+
+ {stats.current} + 连续天数{lastActive ? `(截至 ${lastActive})` : ''} + 最长 {stats.longest} 天 · 单日最高 {stats.best} 次 +
+
+ +
+
+
+ + +
+
+ E +

悬停周详情

+

指针落在任一格上,整列(那一周)轻轻抬起,右侧给出「这一周合计 / 最好的一天」。 + 优点信息密度最高、动效克制;代价仍是鼠标优先,触屏靠点按。

+
+
+
+ +
+ +
+
+ +

数据口径:GitHub GraphQL 贡献日历,构建期快照({buildTime})。切到"减少动态效果"时五个方案全部退化为静态网格。

+ + + + +