跨端: 语义色/三级文字在深色下没提亮(设备扫出 12 处)+ 判据取样法修到第三版

## 一、判据基建先修对,否则是在听噪声

深色可读性扫描的**取样法迭代了三版**,每版都因为**假红**才改的
(记在这里,因为"判据自己错"比"界面错"更费时间):

1. **取中心一个像素** ⇒ 落在笔画之间、读到的是底色 ⇒ 一片 ratio=1.00 假红。
2. **取全局最暗/最亮** ⇒ 会采到**两个不同的东西**:细字的抗锯齿中间色。
   实例:segmented「日」报 ink `rgb(32,34,35)` / bg `rgb(98,100,101)` 2.69:1 ——
   而截图里它要么蓝底白字、要么深底浅字,两种都远高于 3:1。
   **反复核对才确认是判据的错,不是界面的错。**
3. **步长采样 + 众数** ⇒ 仍然假红:周表头「一」报 2.03:1,
   而逐像素的真实墨是 `rgb(166,167,167)`(**6.6:1**)——
   稀疏网格**整个跳过了笔画**。这不是调参能修好的(字越小越糟)。
4. **逐像素 + 众数=底 + 离底最远者=墨**(最终版)。
   理由:一个文字框里**面积最大的一定是底**,而"离底色最远"就是笔画最实那部分。
   ⚠️ 逐像素是**负担得起的**(整屏已解码在内存,一个框最多 9 万像素)——
   **不要再为了省开销退回抽样**,前两版都因此假红。

## 二、真 bug 三批(都是"深色下没提亮")

### ① 语义色前景(12 处 → 修完设备复扫 0)
WebUI 的语义色也是**双通道**,`.dark` 段把 `red/green/amber-700` 换成浅色
(`248/118/249 → 164/219/195` 那组)。鸿蒙只有一个深色值:
- 「归档」按钮 `Theme.danger` #B91C1C 压深面 ⇒ **2.47:1**(联系人页 7 处)
- 「今天」/「日」等 ⇒ **2.38–2.77:1**

加 `dangerDark/approveDark/warnFgDark`(逐字对齐 WebUI `.dark`)
+ `dangerFor()/approveFor()/warnFgFor()` 入口,22 处接入。

### ② 日历工具条整块配色抄错(不是"深色档选错")
对照 WebUI `CalendarView.tsx:313-331`:

| 元素 | WebUI | 鸿蒙(改前) |
|---|---|---|
| 「今天」 | `border-gray-300` + `text-gray-700`(**中性**) | `accentSoft` 底 + `accentStrong` 字 |
| 月/周/日 未选中 | `bg-white` + `text-gray-700` | `accentSoft` 底 |

鸿蒙把**次要的中性按钮**画成了**品牌色按钮** —— 视觉重量跟主操作一样,
三个档看起来"都像选中"。已按 WebUI 改成中性。

### ③ 三级文字在深色下 2.82:1(**71 处**,全局最大的一个)
WebUI 的 `.dark` 段注释逐字写着这个坑:

> 「`gray-400/500`(次要文字)→ **提亮**。深底上的浅色 gray-400
>  只有约 2:1 对比度,远低于 WCAG AA 的 4.5:1 —— **看得见但读不动**。」

鸿蒙用的系统三级色 `ohos_id_color_text_tertiary` 深色下是 `rgb(102,103,105)`
压深面 **2.82–3.05:1**,而它被用在 10–11px 的**小字**上
(时间戳、农历、空态、辅助说明)。

**不换掉系统令牌**("系统拥有的维度"纪律照旧),加 `textSubtleFor()`:
深色下改用二级色(实测 `rgb(166,167,167)` ≈ 7.15:1,
且与 WebUI 的 `gray-500` 同档位)。71 处接入。

**设备复扫(修后)**:通信/日历/联系三页 **176 段文字全部 ≥3:1**。

## 三、欠账与底本

- `baseline.sha` 第 4 次重算。仍**先逐个取证**:
  `git diff --quiet HEAD -- <f> && echo STALE || echo INTENTIONAL`
  ⇒ 三个全是 `INTENTIONAL`(我改的),不是变异残留。
  把这条取证命令也写进文件,下次照抄。
- `dangerDark/approveDark/warnFgDark` 已登记进 `SELF_OWNED_COLORS`
  + `Theme.ets` 登记表(各带理由与 WebUI 出处)。

`run-all.mjs` → `checks=514 pass=514 fail=0 skip=0 red=0 broken=0 unreported=0`。

**未验**:①「我的」页(底部头像按钮,扫描脚本没找到入口)没扫;
② 浅色下的对比度没扫(本轮全部针对深色)。
This commit is contained in:
2026-09-19 20:27:09 +08:00
parent 13b557a742
commit 4ecddfc2b6
13 changed files with 312 additions and 120 deletions

View File

@ -754,7 +754,7 @@ export struct SettingsPane {
InfoRow(label: string, value: string) {
Row() {
Text(label)
.fontSize(12).fontColor(Theme.textSubtle)
.fontSize(12).fontColor(Theme.textSubtleFor())
.width(80).flexShrink(0)
Text(value)
.fontSize(14).fontColor(Theme.textPrimary)
@ -823,7 +823,7 @@ export struct SettingsPane {
Blank()
Text(statusLabel(this.appearanceStatus))
.fontSize(11)
.fontColor(this.appearanceStatus === 'synced' ? Theme.textMuted : Theme.warnFg)
.fontColor(this.appearanceStatus === 'synced' ? Theme.textMuted : Theme.warnFgFor())
}
.width('100%')
@ -842,7 +842,7 @@ export struct SettingsPane {
.width('100%').margin({ top: 10 })
Text('主题由系统按色彩模式给色(深浅两套不靠手写色值);换账号时外观跟着账号走。')
.fontSize(11).fontColor(Theme.textSubtle).margin({ top: 6 })
.fontSize(11).fontColor(Theme.textSubtleFor()).margin({ top: 6 })
Divider().color(Theme.border).margin({ top: 12, bottom: 12 })
@ -889,7 +889,7 @@ export struct SettingsPane {
? '服务器未配置推送'
: (this.pushServerStatus === 'server-on' ? '服务器已开启推送通道' : '推送通道状态未知'))
.fontSize(11)
.fontColor(this.pushServerStatus === 'server-on' ? Theme.textMuted : Theme.warnFg)
.fontColor(this.pushServerStatus === 'server-on' ? Theme.textMuted : Theme.warnFgFor())
.margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start)
@ -902,7 +902,7 @@ export struct SettingsPane {
.width('100%')
Text('新邮件到达时由系统弹通知(点通知直达邮件)。未配置服务端推送时请保持关闭。')
.fontSize(11).fontColor(Theme.textSubtle).margin({ top: 6 })
.fontSize(11).fontColor(Theme.textSubtleFor()).margin({ top: 6 })
}
.width('100%').alignItems(HorizontalAlign.Start)
.padding(16).margin({ top: 8 })
@ -928,11 +928,11 @@ export struct SettingsPane {
.width('100%')
Text('第三方客户端用该密钥访问自己的邮箱(Authorization: Bearer)。它不能用于注册 Agent。')
.fontSize(11).fontColor(Theme.textSubtle).margin({ top: 6 })
.fontSize(11).fontColor(Theme.textSubtleFor()).margin({ top: 6 })
if (this.keysError.length > 0) {
Text(this.keysError)
.fontSize(12).fontColor(Theme.danger).margin({ top: 8 })
.fontSize(12).fontColor(Theme.dangerFor()).margin({ top: 8 })
}
/*
@ -944,7 +944,7 @@ export struct SettingsPane {
if (this.newKeyToken.length > 0) {
Column() {
Text('密钥已创建。全文仅显示这一次,关闭后无法再次查看。')
.fontSize(11).fontColor(Theme.warnFg)
.fontSize(11).fontColor(Theme.warnFgFor())
.width('100%')
Text(this.newKeyToken)
.fontSize(11).fontColor(Theme.textPrimary)
@ -1043,7 +1043,7 @@ export struct SettingsPane {
.margin({ right: 12 })
Text('吹销')
.fontSize(12).fontColor(Theme.danger)
.fontSize(12).fontColor(Theme.dangerFor())
.onClick(() => { this.revokeKey(k.key_id); })
}
.width('100%')
@ -1088,11 +1088,11 @@ export struct SettingsPane {
/* 两次不一致就地提示(与 WebUI `mismatch` 同一行为,不等提交才发现) */
if (this.confirmPw.length > 0 && this.newPw !== this.confirmPw) {
Text('两次密码不一致')
.fontSize(11).fontColor(Theme.danger).margin({ top: 6 })
.fontSize(11).fontColor(Theme.dangerFor()).margin({ top: 6 })
}
if (this.pwError.length > 0) {
Text(this.pwError)
.fontSize(12).fontColor(Theme.danger).margin({ top: 8 })
.fontSize(12).fontColor(Theme.dangerFor()).margin({ top: 8 })
}
if (this.pwMsg.length > 0) {
Text(this.pwMsg)
@ -1145,11 +1145,11 @@ export struct SettingsPane {
Row() {
Text('管理').fontSize(Theme.fontBody).fontColor(Theme.textPrimary)
.layoutWeight(1)
Text('›').fontSize(Theme.fontBody).fontColor(Theme.textSubtle)
Text('›').fontSize(Theme.fontBody).fontColor(Theme.textSubtleFor())
}
.width('100%')
Text('用户管理(新建 / 编辑 / 启停 / 重置密码)')
.fontSize(Theme.fontTiny).fontColor(Theme.textSubtle)
.fontSize(Theme.fontTiny).fontColor(Theme.textSubtleFor())
.width('100%').margin({ top: 4 })
}
.width('100%').alignItems(HorizontalAlign.Start)
@ -1197,7 +1197,7 @@ export struct SettingsPane {
}
Text('删除')
.fontSize(12).fontColor(Theme.danger)
.fontSize(12).fontColor(Theme.dangerFor())
.onClick(() => { this.removeAccount(account.id); })
}
.width('100%').height(72)