跨端: 品牌蓝深色下没提亮(24 处字/图标看不见)+ 补深色可读性设备判据
## 一、真 bug:WebUI 深色下把品牌蓝**提亮**了,这边没有 WebUI 的强调色是**双通道**(`tailwind.config.js` 的 `backgroundColor` /`textColor` 覆盖 + `index.css` 两段定义): | 通道 | 用途 | 浅色 | 深色 | |---|---|---|---| | `--s-blue-600` | **实心按钮底** | `37 99 235` | `37 99 235`(**同值**)| | `--c-blue-600` | 内容/交互的**蓝字与图标** | `37 99 235` | **`128 175 249`** | `index.css:353` 写了理由:主按钮底跟着变「会让主按钮在深色页面上 失去『这是主操作』的视觉重量」;而蓝字必须提亮,否则深底上读不动。 鸿蒙只有一个 `Theme.accent = '#2563EB'` ⇒ 24 处字/图标在深色下 对比度 **2.61:1**(设备实测:管理页返回箭头 `‹`),低于 WCAG 图形下限 3:1。 **取证方式**:在跑着的 WebUI 上用 CDP 读**计算样式**(不是读 CSS 源)—— 浅色 `37 99 235` / 深色 `128 175 249`,实测确认。 ## 二、修法:加前景专用的深色档 + 唯一入口 - `Theme.accentDark = '#80AFF9'`(= WebUI `.dark --c-blue-600`) - `Theme.accentFor(dark?)` 作为**前景**唯一入口 - **当背景的 20 处保持 `Theme.accent` 不动**(跟 WebUI 的 `--s-*` 一致) 24 处 `.fontColor/.iconColor(Theme.accent)` → `Theme.accentFor()`。 ## 三、顺手修掉「转述一层就会漏」这个结构问题 `accentSoftFor(isDark)` 原本的约定是"页面算好深浅色传进来"。 给 `accentFor` 做准备时一数:**8 处**直接用了 `Theme.accentSoft`(没走入口) —— 约定**已经漏了**,而漏掉的症状正是上一轮那个"深色下白底卡片刺眼"。 于是把两个 `For()` 的参数都改成**可选**:`AppStorage` 是 ArkTS 全局键值存储, 静态类可以直接读(原来"拿不到 Context"的理由不成立)。 ⇒ `Theme.isDarkNow()` 成为唯一判断点,调用方不必再各自转述。 ## 四、设备判据:深色可读性**扫一屏** 原来只有悬浮球那一条(单个点)。这个 bug 类一天撞到**两批**(12 处 + 24 处), 逐处写判据追不上 ⇒ 改成把当前页所有小段文字都量一遍对比度。 三个实现要点(第一版全踩了,都写进注释): - **不能只取中心一个像素**:中心多半落在笔画之间 ⇒ 读到的是底色, 报出一片 ratio=1.00 的假红。改成**框内网格扫描取极值**(最亮=底/最暗=墨)。 - **整屏解码一次**:每点 spawn 一次 ffmpeg 太慢 ⇒ 新增 `readPixels()`(157ms 解整屏,比逐点快三个数量级)。 - **只判"有真实墨迹"的框**(`hi.L - lo.L >= 0.02`),否则跳过而不是判红。 实测:修前 1 处低对比(2.61:1),修后 **40 段文字全部 ≥3:1**。 ## 五、判据自身的三个修正 - `accentSoftFor(dark:)` 的签名断言跟着放宽成 `dark?`,并**补上 `accentFor` 的**。 - **孤儿令牌判据从"一跳"改成"走整条链"**:`KEY_IS_DARK` ← `isDarkNow()` ← `accentFor()` ← 24 处页面。只查一跳时它假红 —— ★ **"有没有人用"是可达性问题,不是邻接问题**;加中间层(抽 `For()` 入口) 恰恰是我们鼓励的写法,而旧判据会因此假红。 - 手写色登记表补 `accentDark` 一行理由。 ## 六、`baseline.sha` 重算(先核过不是残留) 三个文件哈希对不上。逐个 `git diff --quiet HEAD -- <f>` 取证: - `AdminUsersPage.ets` / `SettingsPage.ets` —— 本次**有意编辑**; - `api/AppearanceApi.ets` —— **与 HEAD 逐字节相同** ⇒ 底本取完后被**合法改过** (提交 `f811c98`),属 `stale` 不是 `residue`。 按该文件自己那条纪律(「重算必须是一次有记录的动作」)在文件里记了理由。 `run-all.mjs` → `checks=514 pass=514 fail=0 skip=0 red=0 broken=0 unreported=0`。
This commit is contained in:
@ -552,3 +552,55 @@ export function closeColor(a, b, tol = 12) {
|
||||
if (!a || !b) return false;
|
||||
return Math.abs(a.r - b.r) <= tol && Math.abs(a.g - b.g) <= tol && Math.abs(a.b - b.b) <= tol;
|
||||
}
|
||||
|
||||
/**
|
||||
* 把整张 PNG 解码成内存里的 RGB 网格,返回一个 `{ w, h, at(x, y) }` 句柄。
|
||||
*
|
||||
* ★ 为什么要有它(2026-09-19 加):
|
||||
* 上面那个 `pixelAt()` 每读**一个**点就 spawn 一次 `ffmpeg`。单点够用,
|
||||
* 但"扫一个文字框找最亮/最暗"这种用法要读几百个点 ⇒ 几百次进程启动,
|
||||
* 一条判据就跑了几十秒(实测 19 个短文本 ~700 次 spawn)。
|
||||
*
|
||||
* 整屏解码是**一次** ffmpeg 调用(3184×2232×3 ≈ 21MB 内存),
|
||||
* 之后所有采样都是数组下标 —— 快三个数量级。
|
||||
*
|
||||
* 坐标系与 `dumpLayout` 的 `bounds` 一致(屏幕像素,原点左上)。
|
||||
*/
|
||||
export function readPixels(pngPath, screenW = 3184, screenH = 2232) {
|
||||
if (!existsSync(pngPath)) return null;
|
||||
const out = join(tmpdir(), `hm-rgb-${process.pid}-${Date.now()}.raw`);
|
||||
const r = spawnSync('ffmpeg', [
|
||||
'-loglevel', 'error', '-y', '-i', pngPath,
|
||||
'-f', 'rawvideo', '-pix_fmt', 'rgb24', out,
|
||||
], { encoding: 'utf8', timeout: 60000 });
|
||||
if (r.error || !existsSync(out)) return null;
|
||||
let buf;
|
||||
try {
|
||||
buf = bytes(out);
|
||||
} catch {
|
||||
return null;
|
||||
} finally {
|
||||
try { unlinkSync(out); } catch { /* 清不掉就算了 */ }
|
||||
}
|
||||
/* 尺寸从数据长度反推 —— 这样调用方不必传对屏幕尺寸 */
|
||||
const px = buf.length / 3;
|
||||
let w = screenW;
|
||||
let h = Math.round(px / w);
|
||||
if (h * w !== px) {
|
||||
/* 退一步:按常见屏宽找整除(设备可能是折叠态/别的分辨率) */
|
||||
w = 0;
|
||||
for (const cand of [3184, 2232, 2560, 1920, 1280, 1080]) {
|
||||
if (px % cand === 0) { w = cand; h = px / cand; break; }
|
||||
}
|
||||
if (!w) return null;
|
||||
}
|
||||
return {
|
||||
w,
|
||||
h,
|
||||
at(x, y) {
|
||||
if (x < 0 || y < 0 || x >= w || y >= h) return null;
|
||||
const i = (y * w + x) * 3;
|
||||
return { r: buf[i], g: buf[i + 1], b: buf[i + 2] };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user