# 前端现代化方案
- **日期**:2026-10-09
- **范围**:视觉层升级 —— `app.css`(1114 行)+ 24 个模板的样式类使用;**不动**信息架构、路由、模板数据结构
- **方法**:先用无头 Chrome 在真实视口量现状(390 / 430 / 1440),再定方案。下面每个数字都是量的,不是估的
- **前提约束**(这些都是现状,方案不得破坏):无前端构建链(服务端渲染 + 单 CSS + 手写 JS)/ 1C1G 服务器 / 已有暗色模式(`prefers-color-scheme` 3 段)/ 已按 WCAG AA 调过 `--text-3`
> ⚠️ `shots/` 里的截图(`mobile-home.png` 等)是**旧状态**:那张顶栏折成 3 行、右按钮竖排的图早就不成立了,
> 现在 390 下品牌名只留 🐾、按钮是「后台」。做视觉判断前一律**现场重新截图**,别拿 `shots/` 当现状。
## 一、现状实测
| 观测 | 实测值 | 说明 |
|---|---|---|
| 卡片样式数 | 17 个卡片盒子(`.card`/`.kpi`/`.filters-card`)→ **只有 2 种样式指纹** | 全站一个「白盒 + 1px 边 + 浅阴影 + 10px 圆角」用到底,KPI / 地图 / 图表 / 表格 / 筛选器全等重 ⇒ 眼睛没有落点 |
| 字阶 | 字号 **8 档**:10/12/13/14/15/18/21/23px,其中 **10–15px 挤了 5 档** | 中段过密、大段太少;层级挤不出来,只能靠加粗 |
| 字重 | 400 / 500 / 600 / **650** | 650 是半档,等于没规范 |
| 正文色 | **4 种**(`--text`/`--text-2`/`--text-3` + 链接)+ 1 个主色 | 整页几乎单色 |
| 图表 | 手写 `.bars`(无图表库),月度/一天分布只有色块 | 无基线、无峰值标注、无静止态数值 |
| 类型色 | 10 种(家/工作/餐厅/…)**只用在 8px 圆点和柱子上** | 没进入界面语义层(chips、表格类型列、KPI 都用不到) |
| 窄屏导航 | 站长导航(6 项)`scrollWidth=277`,可用宽:**430→254(溢 23)**、**390→214(溢 63)**、**320→144(溢 133)** | 靠 `.nav-links` 横向滚动兜底;390 下「统计」被切成半个字紧贴「后台」按钮,渐隐提示实际看不见 ⇒ 像坏了 |
| 暗色模式 | 已存在(586/590/742 三段 media) | 任何配色改动要同步两套变量 |
| 已有优点(别改坏) | KPI 数字已是 `tabular-nums`;`--text-3` 按 AA 调过;地图/时间线的 z-index 与安全区已有处理 | 这些是已经踩过坑换来的 |
## 二、为什么不重写(明确排除项)
- **不引入前端框架 / 构建链**(Vue/React/Tailwind/Vite):现状是 Go 模板直出 + 单 CSS + 无 npm。引构建链会让「改一行字要跑 node」、部署变复杂,而站点是个人足迹站、并发极低 —— 收益不抵成本。
- **不上图表库**:为几个柱图多背 100KB+ 与一个依赖,不值得。`.bars` 手写继续用,只升级表达力。
- **不重做信息架构**:页面、路由、模板数据结构都不动;只改「长什么样、怎么看」。
## 三、四批方案(由低风险到高收益)
### 批次 A:设计令牌收敛(纯 CSS,约半天)
1. **字阶砍到 6 档**:`11 / 13 / 15 / 21 / 26 / 34`
- 11 = 脚注/单位,13 = 正文(默认),15 = 卡片标题,21 = 页面标题,26 = KPI,34 = 首屏主数
- 现在 10/12/14/18/23 全部并档;`--text-3` 的灰阶不动(它承载 a11y 口径)
2. **字重砍到 3 档**:400 / 500 / 600(删掉 650)。层级主要靠**字号 + 颜色**,不靠加粗 —— 这更接近现代做法
3. **间距令牌**:`--sp-1..5` = 4/8/12/16/24,替换散落的 padding/margin 字面量
4. **圆角令牌**:`--r-sm/md/lg` = 6/10/14(现在只有 `--radius:10px` 和一个 8px)
5. **阴影分 3 档**:`--sh-1`(卡片)/`--sh-2`(浮层)/`--sh-3`(hero);现在只有 1 档
> 判据:`cardVariants` 2 → ≥3;(字号/字重) 组合 10 → ≤6;CSS 里裸 `border-radius:` 数字 0 处。
### 批次 B:给卡片分主次(约 1 天)
三档面,替代现在唯一的一种:
| 类 | 用途 | 样子 |
|---|---|---|
| `.card--hero` | 首屏 KPI 区、地图 | 更大内边距 + `--sh-3`、无边框、左侧 3px 主色条 |
| `.card`(默认) | 图表、列表、表单 | 保持现状(已够干净) |
| `.card--quiet` | 说明块、次级列表 | `--bg-2` 浅底、无边框无阴影 |
配套把首屏 KPI 从「4×2 等重白盒」改成 **1 个 hero 大数 + 3 个指标**:首屏必须有一个一眼能落下去的点。手机端同理(hero 占整行,后 3 个两列)。
> 判据:把首屏截图给人看 3 秒,对方能指出「最重要的那个数」。
### 批次 C:让图表说人话(约 1–2 天,仍不用库)
- **月度分布**:加基线、峰值柱顶部标数值 + 一处「最高 197 次」文案、每根柱 `
` 与 hover 值
- **一天分布**:加 0/6/12/18 轴标 + 早/午/晚三段极浅底色,让「作息」这个概念不用读标题也懂
- **类型分布**:柱色改用类型色(见批次 D),数值列 `tabular-nums`(数字列宽不再跳)
- **空态统一**:现在有的写「暂无数据」、有的空着;统一一个 `emptystate`(模板里已有这个定义,直接用)
> 判据:不看图例、不看标题,也能说出这张图在讲什么。
### 批次 D:色彩语义化 + 窄屏导航(约 1 天)
1. **类型色升级为语义令牌**:`--cat-home` / `--cat-work` / `--cat-food` … 10 个,并用 `color-mix()` 派生各自浅底(沿用现有 `--accent-soft` 的做法,不手写 10 套浅色)
- 用在:筛选 chips、地点表「主要类型」列、KPI 强调、图表
- 硬约束:正文用色对比 ≥4.5:1、大字/图形 ≥3:1(延续现有口径),**暗色模式各派生一套**
2. **窄屏导航**:现状溢出 23–133px。
- 第一步(低风险):主链接收到 4 个(主页/地图/地点/统计),「时间线/报告」进「更多」;把 `.overflow-right` 的渐隐加重到看得见
- 第二步(可选,需评估):底部 tab bar。⚠️ 地图页的浮层 z-index 与 `--nav-h` 已经踩过坑(审计 F11/F12),上底部栏要先确认与 Leaflet 控件、安全区的冲突
> 判据:320/390 下**没有任何链接被切半个字**;导航高度不增(现 51px);暗色模式下新色阶对比度同样达标。
## 四、建议顺序
先 **A + B**(约 1.5 天):纯 CSS + 少量模板 class,风险最低、观感提升最直接,改完能立刻判断方向对不对。
C 独立(图表可以慢慢磨),D 涉及配色与导航,放最后一次性做,避免中途改两次色。
## 五、验收方式(沿用项目既有纪律)
- 视觉:`rond-web-verify` 技能在 **320/390/430/1440** 四个宽度现场截图 + 量数(别用 `shots/` 里的旧图)
- 不改坏:`./scripts/verify.sh` 必须仍然 **0 skip 全绿**(模板改动会影响 `tb_stack_test`、`TestVisitorPagesUseThirdPersonCopy` 等)
- 暗色:`--color-scheme dark` 跑同一组断言
- 无障碍:`audit/a11y.js` 的对比度与字号判据不得比改动前更差(改动前基线要先跑一遍存档)
- 本次只出方案,**未改任何代码**。
---
# 落地记录:A + B 批次(2026-10-09)
方案通过后先做了 A(设计令牌收敛)与 B(卡片分主次)。下表每个数字都是**同一份探针在改动前后各跑一遍**得到的。
## A:设计令牌收敛
**字阶**:文本 6 档 `--fs-xs/sm/md/lg/xl/2xl` = 11 / 13 / 15 / 21 / 26 / 34px。
- app.css 原来 19 种字号、96 处声明;模板里另有 54 处内联字号 —— 全部并档。
- **两个字外档,都不是漏掉的**:`--fs-control: 16px`(窄屏表单控件必须 ≥16px,否则 iOS 聚焦放大整页,属功能约束,不能进字阶);`--glyph-*` 6 档(✕ / 箭头 / 空态图标,按控件尺寸定 —— 把 22px 的 ✕ 塞进字阶只会把按钮撑坏)。
- 字重收敛到 400 / 500 / 600(删掉 650 与 700 两个半档)。
**圆角** 4 档 `--r-xs/sm/md/lg` = 3 / 6 / 8 / 12px(原来 2.5/4/5/6/7/8/9/10/12/20px 混用;卡片 10 → 12)。
**阴影** 3 档 `--sh-1/2/3`(原来只有 1 档变量 + 6 处写死的阴影);暗色模式整套覆盖。
**间距** 5 档 `--sp-1..5`。⚠️ **本批只用在卡片内边距这类主节奏上,没有做全量迁移** —— 一次性铺开的改动面太大、收益最低,留到需要时再逐批替换。
**`--card-pad` 单列**:折叠卡片的标题行要用负 margin 顶到边,两处必须永远相等。
## B:卡片分主次
- `.card--hero`(首屏地图:无边框 + `--sh-3` + 更大内边距)、`.card`(默认)、`.card--quiet`(设置页「地图底图 / 图片存储」两块只读说明:页面底色 + 细边 + 无阴影)。
⚠️ quiet 第一版用 `--bg-2`(#fafbfb),实测与页面底 #f6f7f8 只差 **4/255** —— 根本看不出边界,卡片像「破了个洞」。改成「页面底色 + 保留细边」才对。
- 首屏 KPI 从「4×2 等重白盒」改成 `.kpi-lead`(主数 + 城市/省份上下文)+ 5 个常规指标。**8 个数字一个没少**,只是分了主次。
## 改动前后(同一探针)
| 页面 / 视口 | (字号,字重) 组合 | 卡片样式档 | 最小字号 |
|---|---|---|---|
| `/` 390 | 13 → **10** | 2 → **3** | 10 → 11 |
| `/` 1440 | 15 → **10** | 2 → **3** | 10 → 11 |
| `/places` 390 | 10 → **5** | 2 → 2 | 12 → 12 |
| `/timeline` 390 | 10 → **6** | 2 → 2 | 11.5 → 11 |
| `/stats` 1440 | 15 → **9** | 2 → 2 | 10 → 11 |
| `/admin/settings` 1440 | 10 → **7** | 1 → **2** | 12 → 13 |
首页 390 的字阶从
`10/400 11.5/400 12.5/400 13/400 13/500 13.5/400 14/400 14/500 15/600 17/400 18/600 21/600 21.8/650`
收敛到
`11/400 13/400 13/500 15/500 15/600 18/400 18/600 21/600 21.8/600 28.1/600`
(字重只剩 400/500/600;多出来的 28.1px 是新加的首屏主数)
## 回归
- `./scripts/verify.sh`:**PASS=347 FAIL=0 SKIP=0**(含本轮新增的两条守卫)
- 移动端 390 审计(溢出 / 小触控 / 小字 / iOS 自放大):**逐值一致**
- 桌面 1440 溢出审计:唯一差异是 `/map` 露外元素 9 → 8
- 暗色模式:三档阴影与主色强调条都按暗色变量生效(实测 computed 值)
## 途中发现并修掉的四处
1. **Leaflet 的字号跟着根字号漂**。vendor 里 `.leaflet-container { font-size: 0.75rem }`,根字号 14 → 13px 之后,地图右下角的**署名与比例尺缩到 9.8px**(掉到本站 11px 下限之下),弹窗正文也跟着缩。已按自己的字阶钉死:容器 = `--fs-sm`,署名/比例尺 = `--fs-xs`;`/map` 上小于 11px 的元素从 2 个降到 0 个。
2. **静态守卫抓到并档**。`TestNavLinksNarrowRuleHasNoDeadPadding` 里写死了 `font-size: 13px`,改成令牌后它红了 —— 守卫本身是对的(它就该在写法漂移时叫),已把判据从「钉具体值」改成「钉窄屏那条规则还在、且设了字号」。另补 `TestNoBareFontSizePx`:静态扫 app.css + 全部模板,禁止再出现裸 px 字号(做过变异验证:注入一条 `font-size: 14px` 会红)。
3. **`h1..h4` 从来就没设过字号**,一直靠浏览器默认的 `2em / 1.5em / 1.17em` —— 也就是跟着根字号漂。根从 14 → 13px 后,`/stats` 里没被 `.card > h2` 命中的两个 h2(「我的结论」「访客点赞」)从 21px 悄悄变成 19.5px,比同页其它卡片标题(15px)大一圈。已显式给出字号:h1 = `--fs-lg`、h2 = `--fs-md`、h3/h4 = `--fs-sm`;实测 `/stats` 的 12 个 h2 现在统一 `15px/600`。
4. **` / ` 走浏览器默认 700**,比全站最高层级(标题 600)还重。已钉成 600。
> 3 与 4 都是「量 DOM 的 (字号,字重) 组合」才浮出来的:肉眼看页面完全正常,只是觉得「有点不齐」。
> 这也是为什么这一批要**先存基线、再用同一份探针逐值 diff**,而不是改完看一眼截图。
## 附:一处历史遗留(本批未动)
7 个模板(`admin/index.html`、`admin/login.html`、`admin/twofa-login.html`、`admin/twofa.html`、`map.html`、`places.html`、`timeline.html`)在**工作区里是 CRLF**,而 `.gitattributes` 要求 `eol=lf`。
本批按字节替换、原样保住了各自行尾,所以 diff 里没有行尾噪声。要不要统一成 LF,另开一次清理。
---
# 落地记录:C + D 批次(2026-10-09)
## C:让图表说人话
- **月度分布**:柱底压一道基线(原来柱子直接悬在空白里,高低只能靠互相比);峰值柱不透明拉满、并在柱顶标出次数(`.pv`),副标题里就不再重复写一遍「最高 N 次」。
- **一天中的分布**:原来只能靠 0/6/12/18 四个数字猜。现在换成一行 **6 小时一段的标尺**(深夜 0-6 / 上午 6-12 / 下午 12-18 / 晚上 18-24),用的是**同一套 24 列网格**(不是 `flex: 6`),所以段边界与柱子严格对齐;峰值小时也标了出来。
- 首页那份「月度活跃度」同样加了峰值锚点 —— 为此给 `HomeData` 补了 `MaxMonthly`,与统计页同一口径(`maxOf`)。
- **空态这条划掉了**:查下来已经统一 —— 8 个页面共用 `emptystate`,局部空态一律 ``,没有需要收拾的。
## D1:类型色语义化(结论和原计划差得比较多)
先把 13 个色在浅/暗两个底上**算了对比度**:
| 底 | 结果 |
|---|---|
| 白底(浅色面板) | **6 个色低于图形对象 3:1**:yellow 1.87、mint 2.12、orange 2.20、green 2.22、cyan 2.54、teal 2.57 |
| 暗底 `#1c2024` | 只有 indigo 2.90 不够,其余都在 3.9 以上 |
所以**没有**按原计划把类型色用到 chips / 表格类型列 / KPI 强调 —— 那几处要的是**文字**,这套色连图形 3:1 都到不了,
做成彩色标签只会更难读。这也正好解释了全站为什么从头到尾只用 8px 小圆点做身份标记。
浅色那 6 个色**一个没动**:它们是手机端与地图上的同一套身份色,改暗会破坏对应关系;而且它们只当标记、不承载唯一信息。
实际做了三件事:
1. **色值收口到 CSS**:`--cat-*` 13 个(原样 Apple 色名值)+ `--cat-*-soft` 13 个浅底(半透明,两个主题自适应)。
Go 侧 `appleColors` 退成「哪些色名合法」的名单;`colorOf` 已成死代码,删掉。
2. **模板函数换成 `csscolor` / `csssoft`**(17 + 2 处)。⚠️ 返回值必须是 `template.CSS`:
`html/template` 的 CSS 上下文过滤器**不认识 `var()`**,普通 string 会被替换成 `ZgotmplZ` ——
页面上所有圆点当场变透明,而且不报错、模板测试照过(本批实测踩到)。
挂了 `template.CSS` 等于关掉这道过滤,所以数据侧色值必须先过严格校验(只有 `#` + 6 位十六进制放行)。
守卫 `TestCSSColorSurvivesTemplateSanitizer` 把两个方向都钉住:不出现 `ZgotmplZ`、恶意色值不得逃出 `style` 属性(做过变异验证)。
3. **暗色只提亮一个**:`--cat-indigo` 掺 10% 白(2.90 → 3.55:1),其余不动。
地图那份调色板(`rond-map.js` 的 `APPLE`)**保持独立**:地图瓦片在暗色下仍是浅色,标记就该用浅色底那套值 —— 硬统一反而会错。
## D2:窄屏导航
改前 ≤620px 靠横向滚动兜底:**390 溢 63px、320 溢 133px**,最深处的「统计」被切成半个字紧贴后面的按钮,
而 `.overflow-right` 的渐隐提示实际看不出来。现在:
- 链接本体抽成 `{{define "navitems"}}` 渲染两遍:桌面在 `.nav-links` 平铺,窄屏收进 `.nav-menu`(原生 ``,不靠 JS 也能开合)。
- JS 只补原生不给的三件事:点外面 / Esc / 点链接后收起。
- ⚠️ **顶栏 z-index 抬到 `--z-nav: 1200`**:菜单属于 `.nav` 这个层叠上下文、自己抬不动,不抬就会被
地图页的 `--z-map-overlay`(1100) 盖住。已在 `/map` 用 `elementFromPoint` 实测(`menuOnTop=true`)。
- 顺手删掉窄屏那几条已经够不着的滚动/遮罩规则;宽屏保留横向兜底(以后又加入口时宁可能滑,也别切字)。
## C + D 的回归
- `./scripts/verify.sh`:**PASS=348 FAIL=0 SKIP=0**
- 移动端 390 / 320:**溢出全 0**(改前 `/admin` 在 320 还溢 34px,现在也归零)
- 桌面 1440:溢出与改前一致(−15 是滚动条宽,按既有口径属正常)
- **7 个页面都没有低于 11px 的文字**(审计的「小字」判据是 <12px,会把我们 11px 的脚注档一并算进去,属设计内)
- 暗色:菜单、类型色、三档阴影都按暗色变量生效(实测 computed 值)