# Dino English · 看板页面标准

本文写**怎么做页面**:文案、提示、颜色、图表、结构、自检。
数据口径与取数规则见 [`DASHBOARDS.md`](./DASHBOARDS.md),两份不重复。

参考实现:[`health-auth.html`](./health-auth.html)。新页面照抄它的结构与 CSS 变量即可。

---

## 1. 文案:表面写人话,术语进 hover

这是本标准的第一条,也是唯一一条会被反复违反的。

### 1.1 规则

1. **正文里不出现技术名词。**字段名、统计术语、文档章节号一律不进正文,进 hover 提示或表格。
2. **先说结论,再给证据。**每条判断第一句必须是人能直接理解的结论,细节跟在后面。
3. **标题用问句或动作,不用名词堆。**
4. **数字必须带上它的意思。**「604 / 604」旁边要有「一次失败都没记到」。

### 1.2 禁用词替换表

写完后按这张表全文搜一遍,搜到就改。

| 禁用 | 改成 |
| --- | --- |
| 口径 | 算法 / 怎么算的 |
| 环比 | 跟上一期比 |
| 判别力 | 说明不了问题 / 这个数不可信 |
| 切片 | 分开看 / 按⋯⋯拆开 |
| 枚举 | 有哪几种取值 |
| 留存率 | 还剩多少 |
| 外溢 / 同源 | 是同一个病 / 连带拖累 |
| 普遍性劣化 | 到处都在坏 |
| 有效尝试 | 真正走到判定的那些 |
| 分母里没有失败样本 | 一次失败都没记到 |
| 非技术流失 | 不算技术问题的那些 |
| 样本不足以证伪 | 数太少,说明不了 |
| 类型漂移 | 两个平台类型不一样 |
| 上报口径 | 有上报的那些 |
| P50 / P95(正文中) | 一半人用时 / 95% 人在此之内 |
| 段① / authorization 段 | 第 1 步 / 三方授权那一段 |
| user_abort / tech_fail / rejected | 用户自己取消 / 技术故障 / 服务端拒绝 |

字段名(`duration_ms`、`auth_entry_source` 等)**只能出现在三个地方**:表格单元格、hover 提示的字段行、第「这页有哪些坑」一节。

### 1.3 结果枚举的固定叫法

四分类在页面上永远用这四个词,不要临时换说法:

| 事件值 | 页面上写 |
| --- | --- |
| `success` | 成功 |
| `tech_fail` | 技术故障 |
| `user_abort` | 用户自己取消 |
| `rejected` | 服务端拒绝 |

---

## 2. Hover 提示

### 2.1 三层结构

每条提示固定三层,第三层可省:

```
标题     —— 这个词的学名(P95 · 95 分位)
正文     —— 人话解释 + 为什么这么算
字段出处 —— 取数用的字段或公式
```

对应 HTML:

```html
<div class="t-h">P95 · 95 分位</div>
<div class="t-b">95% 的成功登录在这个时间内完成⋯⋯</div>
<div class="t-f">取 backend_exchange 且 result=success 的 duration_ms</div>
```

### 2.2 必须挂提示的元素

漏一个就算不合格:

| 元素 | 提示要回答 |
| --- | --- |
| **全部表头** | 这一列是什么字段、怎么算的 |
| 统计术语(成功率 / P50 / P95 / 覆盖率) | 公式是什么、分母含谁不含谁 |
| 时间窗口 chip | **为什么这一节只能取这么长**(埋点上线日期) |
| 状态标签(「仅 iOS」「一次失败都没记到」) | 完整证据链 |
| 漏斗每一步 | 这个数怎么数出来的 |
| 漏斗的百分比 | 分母是谁 —— 「还剩」的分母是第一步,「走到下一步」的分母是上一步 |
| 「没记耗时」「字段缺失」类占位 | **缺的是哪个 param**,以及是数据丢了还是压根不报 |

### 2.3 实现

术语词典 `TIPS` 集中维护,渲染完调 `decorate()` 统一挂载:

- 表头按 `textContent` 精确匹配 `TIPS` 的 key,不用逐个改 HTML
- 一次性的提示用 `data-tip="x" data-tip-t="标题" data-tip-b="正文" data-tip-f="字段"`
- `.hint` 是一条淡虚线 + `cursor:help`;`@media (hover:none)` 下隐藏虚线

**新增表头时,同步往 `TIPS` 加一条。**页面自检会列出没挂提示的表头。

---

## 3. 颜色

### 3.1 只有一个指标上色

**只有技术成功率上色。**放弃率、覆盖率、耗时、次数一律不上色 —— 它们不是好坏指标。

页面其余部分只有黑白灰。**哪里有颜色,哪里就在说数据。**

### 3.1.1 身份用形状,不用颜色

**Android / iOS 这类「是谁」的区分,一律用图标(形状),不用颜色。**

颜色通道已经被健康度占满了。再拿颜色去表示平台,读者就得同时记两套色义 ——
「这个红是危险,还是安卓?」一旦要问这句,整页的配色就废了。

具体做法:

- 平台身份 = SVG 图标 + 名字,跟着正文色走
- 折线图两端 = **同一色相的深浅两阶**(`--plat-a` / `--plat-i`),不引入第二个色相
- 端点直接画图标 + 数字,**不靠图例回查**

深浅两阶只是让两条线能分开,不承担「谁好谁坏」的含义。真要判好坏,看数字的颜色。

### 3.2 三档与两个特例

```js
tier = v >= 0.99 ? 'ok' : v >= 0.95 ? 'warn' : 'bad'
```

| 档 | 阈值 | 含义 |
| --- | --- | --- |
| 🟢 健康 | ≥ 99% | 青 |
| 🟡 需关注 | 95% ~ 99% | 琥珀 |
| 🔴 危险 | < 95% | 红 |
| **灰色** | 失败样本少到不像真的 | **这个数不可信,不判档** |
| **—** | 连成功记录都没有 | 算不出来 |

灰色这一档是本项目特有的,**必须保留**:登录看板有两个 100% 都是埋点没记失败造成的,直接标绿是误导。

**灰色的判据是失败率,不是「失败数恰好等于 0」。**

```js
const DISTRUST_N = 500, DISTRUST_RATE = 0.001;
untrusted = (fails, n) => !fails || (n >= DISTRUST_N && fails / n < DISTRUST_RATE);
```

这条是踩了坑之后加的。原本写的是 `!fails ? 'none' : ...`,后来某天数据里冒出**一条**迟到的失败记录,
`sdk_auth_launch_result` 的 Android 一格当场从灰色的「不可信」翻成**绿色的 99.93%「健康」** ——
问题一点没解决,页面却开始说它健康了。

> **凡是「有没有」型的判据,都要问一句:多一条会怎样?**
> 会翻盘的,就改成「有多少」型 —— 用比率配样本量下限,别用绝对值。

同一个坑还有另一面:**分子恒为 0 的比率,不能显示成 0%**。
三方授权那段成功了不上报,`rate(0, fails)` 算出来是 0,标红成「成功率 0%」纯属造谣 ——
这种要显示「不适用」,并在 hover 里说明为什么算不出来。

### 3.3 色值

四种结果的颜色**顺序固定**。这个排列是为色觉障碍(CVD)辨识挑过的 —— 换顺序会让相邻色块分不开:

| 结果 | 亮色 | 暗色 |
| --- | --- | --- |
| 成功 | `#0ca5b8` | `#22c3d6` |
| 用户自己取消 | `#fab219` | `#fab219` |
| 技术故障 | `#e03131` | `#ff6b6b` |
| 服务端拒绝 | `#ec835a` | `#ec835a` |

排列必须是 **成功 → 用户自己取消 → 技术故障 → 服务端拒绝**,关键是<b>别让黄和橙挨着</b>。同族色值实测过:黄橙相邻时常视觉分离度只有 13.6,低于 15 的底线,全色觉的人都难分辨。

**改色值时必须重跑校验**,不能靠眼睛看。用 dataviz skill 的 `scripts/validate_palette.js`,亮暗两种模式都要过:

```bash
node scripts/validate_palette.js "#0ca5b8,#fab219,#e03131,#ec835a" --mode light
node scripts/validate_palette.js "#0ca5b8,#fab219,#e03131,#ec835a" --mode dark
```

单系列图表用 `--series-1`(亮 `#2a78d6` / 暗 `#3987e5`)。

---

## 4. 排版:字要够粗够大

看板是拿来长时间盯着看的,不是海报。**宁可粗一点大一点,也不要「精致但费眼」。**

### 4.1 三条硬规则

1. **不要写 `-webkit-font-smoothing: antialiased`。**
   这是 macOS 上字发虚发细的头号原因 —— 它关掉了次像素渲染,笔画会瘦一圈。看着"精致",读半小时眼睛就受不了。保持浏览器默认。
2. **正文不低于 15px,最小的辅助文字不低于 12px。**
   中文字形比拉丁字母复杂,同样字号下更难认。11px 以下的中文在普通屏幕上基本是糊的。
3. **次要文字要有对比度。**
   `--text-muted` 对白底至少 4.5:1。灰得太浅等于没写。

### 4.2 现用的字号阶梯

| 用途 | 字号 |
| --- | --- |
| 正文 / 表格 | 15.5px / 行高 1.7 |
| 小节说明、卡片副标 | 13.5px |
| 表头、脚注、图例 | 12.5 ~ 13px |
| 最小的辅助标注 | 12px(底线,不再往下) |
| 主指标大字 | 68px |
| 副指标 | 31px |

改字号时**整条阶梯一起动**,不要单独调某一处 —— 否则层级会乱。

### 4.3 文字色

| 角色 | 亮色 | 暗色 |
| --- | --- | --- |
| 正文 | `#111111` | `#fafaf8` |
| 次要 | `#3a3a36` | `#cbcbc4` |
| 辅助 | `#6b6b63` | `#9b9b93` |

### 4.4 不用 emoji

试过,撤了。原因:

- **在图例和数据区是干扰。**颜色映射已经由色块承担,再加一个符号就是两套语义,而且 emoji 的颜色和图表里的对不上。
- **在标题上抢视觉重量。**字号调到跟标题一样大就压过标题,调小又看不清,没有合适的尺寸。
- 跨平台字形不一致,部分系统还会回退成单色。

导航靠**编号 + 短标题**就够了,这也是为什么 §1 要求标题写成人能一眼看懂的短句。

---

## 5. 图表

### 5.1 漏斗:先证明能画

**不同事件、不同分母、不同窗口的数据不能叠成漏斗。**画之前先答三个问题,有一个答不上就不画:

1. 这几步是同一个事件里的吗?
2. 分母是同一批对象吗?
3. 时间窗口一样吗?

登录看板的三步分属三个 `diagnostic_area`,**不能画**;第 2 步内部的两段是同一事件同一窗口,**可以画**。页面上要写明为什么不能画。

漏斗必须给出:

- 每步的**绝对数**和**占起点的比例**
- 步间的**转化率**与**掉量的分类归因**(掉的这些里,哪些是技术问题、哪些不是)
- 脚注解释**整段成功率为什么不等于末步比例**

### 5.2 通用规格

- 柱 ≤ 24px 粗,顶端 4px 圆角、基线端方角
- 线 2px,端点圆点 ≥ 8px 直径,带 2px 表面色描边
- 网格线 1px 实线,不用虚线
- 堆叠段之间留 2px 表面色缝隙,不画描边
- **不做双轴图**。两个量纲不同就画两张图
- 直接标注只标端点或极值,**不要每个点都标数字**

### 5.3 每张图配一份表格

图表旁边给「表格」切换按钮,或直接在下方给表。**数值不能只能通过 hover 才读得到。**

---

## 6. 页面结构

固定顺序,不要自创:

```
封面        标题 + 一句话说明这页回答什么问题 + 元信息(Project/Source/Location/Timezone/Range)
颜色说明条   上色规则 + 结果枚举图例
结论        主指标大字 + 分档标签 + 3~4 个副指标
归因        主指标为什么是这个档 —— 失败集中在谁身上、集中在哪天
播报        一行摘要:量 · 健康率 · 耗时 · 主要死因
告警        埋点缺口、线上劣化,按严重度排
分节        每节:标题 + 时间窗口 chip + 数据来源 chip + 一段人话说明 + 图表/表格
这页有哪些坑  最后一节,取数和解读前必读
页脚        数据来源表、时间范围、口径文档链接
```

要求:

- **结论必须在第一屏。**滚动之前就能看到主指标和它是什么档。
- 每节的**时间窗口独立标注**。埋点上线日期不同,窗口就不同,不要强行对齐。
- 左侧导航的文案与节标题**逐字一致**。

---

## 7. 分维度:分开算,但不分页

**「不合并计算」和「放在两页」是两件事。**先分清这两个问题,再决定怎么做:

| 问题 | 答案 |
| --- | --- |
| 数据要不要合并算? | **看埋点是否对齐** |
| 页面要不要拆开放? | **看给谁看、怎么看** |

结论:**同一页两列并排,每列独立计算,任何一处都不相加。**

### 7.1 什么时候必须分开算

满足任意一条,这个维度的数就**不许求和、不许算合计**:

1. **有一端埋点缺失。**这时「合计」不是合计,是有数据那一端的数披着「整体」的外衣 —— 会同时误导两边。登录看板就是这个情况:Android 的「登录后同步」一条都没有,合并后的 100% 其实是纯 iOS 的数。
2. **两端口径不一样。**比如失败在 Android 标 `tech_fail`、在 iOS 标 `fail`,合并前得先对齐,否则加出来的是两个不同的东西。
3. **每一端的故事独立成立。**Android 的问题是签名刷新崩了 + 埋点缺失;iOS 的问题是登录成功率低。两条线不重叠,合并只会互相稀释。

### 7.2 但不要因此拆页

拆页的代价比想象中大:

- **对照证据没了。**「Android 0 条」单看是一节空白,读成「还没做」;必须和「同期 iOS 1,010 条」并排,才读得出「这是埋点缺失」。
- **同一个故事要讲两遍。**开会时来回切页,讲完 Android 再讲一遍 iOS,时间翻倍还显得没效率。
- **两份页面必然漂移。**

所以默认做法是**一页两列**:

- 用 `table.cmp` 这种左右分组的对照表,列宽由同一张表保证对齐 —— 不要两张表上下堆,那样列宽各算各的,永远对不齐
- 折线图两端**画进同一张图、共用一个坐标轴**(前提是同指标同单位)
- 每个区块底下固定写一句「**两列各算各的,不相加**」
- 页面上**不出现任何一处合计**

### 7.3 只按平台拆列,不按版本拆

平台是**稳定的两个值、对应两个固定团队**;版本会不断迭代,按版本拆列会无限增殖。版本留作表内维度。

### 7.4 一份数据,一个外壳

```
auth-data.js    ← 全部数据,带平台维度。更新只改这个
auth-page.js    ← 渲染逻辑 + 术语词典。正文里不写死任何数字
auth-page.css   ← 样式
health-auth.html ← 十行外壳
```

**正文里的数字、窗口、结论文案全部由数据推导。**换一份数据不用动渲染代码 —— 否则数据更新之后,页面上的叙述会和表里的数字对不上。这不是洁癖:实测过一次,数据一换,「各自一次失败都没记到」那句话当场变成假话,而表格里明明写着 1 次。

---

## 8. 「这页有哪些坑」怎么写

这一节是页面里最有价值的部分,标准最高:

1. **每条以人话结论开头并加粗。**「同一个字段两个平台类型不一样,取数会静默漏掉一半」,不是「布尔字段类型双端不一致」。
2. **给证据,不给判断。**写清楚哪个数、多少条、哪天、哪个平台。
3. **能指名道姓就指名道姓。**「该问 Android:这个判断你们做了吗」比「需研发确认」有用。
4. **区分「没发生」和「没记录」。**这两件事在数据上长得一模一样,必须写清是哪种。
5. 数据本身的坑(会追平、类型不一致、字段缺失)和口径的坑(算不了环比、查不了单用户)**都要写**。
6. **提了假设就要说清它为什么还只是假设。**比如「登录太慢所以有人放弃」—— 耗时只统计成功的登录,放弃的人压根没走到那一步,两个数来自不同人群。写清楚差什么才能证实,别让假设穿着结论的衣服出门。

---

## 8.1 窗口怎么标

**标事件自己的首报日,不是查询区间。**

埋点是分批上线的。查询窗口写「30 天」,不代表这个事件有 30 天数据 ——
登录看板实测:`auth_login_result` Android 首报 07-28、iOS 07-27,`sdk_auth_launch_result` 两端 07-29,
而查询窗口是 07-07 起。直接标「30 天」,读者会把灰度期的爬坡量当成日常水位。

做法:每个区块的 chip 显示**该区块数据实际覆盖的天数**,从数据里推,不写死。

### 8.2 最新一天要标成暂定

日表会被回填。实测某天的数隔一天重拉多了 11 条,主指标从 95.21% 掉到 94.74%。

**而折线图端点标签指的恰好就是最新那天** —— present 时最容易被念出口的,正是最不稳的数。

所以最新一张日表统一标成暂定,**不删数据,只是不许当定论**:

- 端点画**空心圆**,不是实心
- 通向它的那一段线画**虚线**
- 数字后面加 `*`,图右下角写清楚
- 页头 Range 旁边挂「末日暂定」,hover 说明为什么

### 8.3 静态页要自己说自己有多旧

页面是静态的,别人下周打开还以为是实时数。用 JS 算 `now - pulled`:

- 超过 1 天:Pulled 旁边标「N 天前」
- 超过 3 天:页头挂一条提示,写明**怎么重新生成**

---

## 9. 交付前自检

逐条过,全过才算完成:

**数据**

- [ ] 各维度分组求和 = 总数(方式、入口、平台、版本、网络、地区各自都要对得上)
- [ ] 归因表的失败数求和 = 主指标的技术故障数
- [ ] 分日数据求和 = 合计(设备数除外,那是去重值)
- [ ] **正文里一个写死的数字都没有** —— 全部由数据推导(换一份数据,叙述必须跟着变)
- [ ] 失败判定用的是**统一常量**,不是就地写的字面量
- [ ] 跑过 result 词表哨兵,没有表外新词、也没有未记录在案的两端不一致

**文案**

- [ ] 按 §1.2 禁用词表全文搜一遍,零命中
- [ ] 正文里没有裸露的字段名
- [ ] 左侧导航文案与节标题逐字一致
- [ ] 页面上没有 emoji
- [ ] 每个区块底下都写了「两列各算各的,不相加」
- [ ] 页面上不存在任何一处跨平台合计
- [ ] 提出的假设都注明了「还差什么才能证实」

**提示**

- [ ] 每个 `<th>` 都挂了提示(用自检脚本列出漏的)
- [ ] **提示词典里没有重复键** —— 对象字面量里同名键后者覆盖前者,**不报错、不警告**,新写的文案会静默失效
- [ ] 时间窗口 chip 说明了为什么是这个长度
- [ ] 每个「缺失/没记录」占位说清了缺的是哪个 param

**排版**

- [ ] 没有 `-webkit-font-smoothing: antialiased`
- [ ] 正文 ≥ 15px,最小辅助文字 ≥ 12px
- [ ] 卡片里的数字对齐(标签行高固定,不因换行错位)

**渲染**

- [ ] 无 JS 报错
- [ ] 表格数、图表数、坑条数与预期一致
- [ ] 亮色 / 暗色都截图看过
- [ ] 800 / 1024 / 1180 / 1440 / 1600px 逐档测过,页面均无横向滚动
- [ ] 所有 grid 用 `minmax(0, 1fr)` 而不是 `1fr` —— 后者会被宽表撑破轨道
- [ ] 响应式断点按**内容区**宽度算,不是视口 —— 侧栏占 245px

**打印**

- [ ] 导过一次 PDF,两列对照没被折成上下
- [ ] 表格、结论卡、告警没有被切在两页之间
- [ ] 侧栏、主题按钮、hover 虚线在纸上都不见了

**元信息**

- [ ] Range 是日历日期,不是 `30daysAgo` 这类相对值
- [ ] Pulled 时间已更新并注明时区
- [ ] 各区块标的是**该事件自己的首报日**,不是查询区间
- [ ] 最新一天标了「暂定」(空心点 + 虚线 + `*`)
- [ ] 页面会自己报「这份数据几天前拉的」

自检脚本参考(node,直接读 HTML 里的 `DATA` 常量):

```js
const D = eval('(' + src.match(/const DATA = (\{[\s\S]*?\n\});/)[1] + ')');
const sum = (rows, k) => rows.reduce((a, r) => a + r[k], 0);
console.assert(sum(D.loginMethod, 'n') === D.loginTotal.n, '方式维度求和对不上');
```

渲染自检用 Chrome headless `--dump-dom`,检查 `Uncaught`、表格数、以及没挂提示的表头。

```python
# 表头覆盖率 + 词典重复键。两个都是「不报错但静默出错」的类型
import re, collections
ok, miss = 0, collections.Counter()
for m in re.finditer(r'<th(\s[^>]*)?>(.*?)</th>', dom, re.S):   # 注意 \s ——
    a = m.group(1) or ''                                        # 写成 <th([^>]*)> 会把 <thead> 也吃掉,
    t = re.sub(r'<[^>]+>', '', m.group(2)).strip()              # 整段被吞,结果全是假阳性
    ok += 1 if 'data-tipped' in a else 0
    if 'data-tipped' not in a and 'colspan' not in a: miss[t] += 1

keys = re.findall(r"^\s*'([^']+)':", tips_block, re.M)
dup = {k: v for k, v in collections.Counter(keys).items() if v > 1}
```

> 自检脚本本身也会骗人。上面那个 `<th` 匹配到 `<thead` 的坑,一度报出 16 个「漏挂提示」的表头 ——
> 实际一个都没漏。**先证明脚本是对的,再信它的结论。**

---

## 10. 不要做的事

- 不要把不同事件的数叠成漏斗
- 不要给不是好坏指标的数字上色
- 不要把 100% 当成健康 —— 先确认失败样本少到什么程度
- **不要用「有没有」判可信度。**`!fails` 这种判据,来一条迟到的失败记录就会翻盘,把灰色的「不可信」变成绿色的「健康」。用比率 + 样本量下限
- **不要把分子恒为 0 的比率显示成 0%。**那一段本来就不上报成功,显示「不适用」并说明原因
- **不要在正文里写死数字。**数据一换,叙述就会和表格打架 —— 而且没人会发现
- **不要用颜色表示身份。**颜色只留给健康度,平台之类的身份用图标
- **不要把两端的数相加。**要对照就并排放,不要求和
- 不要用双轴图
- 不要每个数据点都标数字
- 不要在正文里写字段名和统计术语
- 不要为了凑满 30 天而混用不同窗口的数据
- 不要在数据缺失时留空或填 0,写「条件未满足」并说明缺什么
- 不要用 emoji
- 不要开 `-webkit-font-smoothing: antialiased`
- 不要用裸 `1fr` 做网格列
- 不要在一端埋点缺失时还给出「合计」
- 不要靠复制文件来拆页
