fix(harmony): 令牌收尾 —— 裸色值清零、遮罩拆两段式、权限档位配色对齐 WebUI

接手核对时发现:「不许写死颜色」那条判据**只挡得住枚举的 8 个旧值** —— 判据全绿的
同期,pages/ 里还留着 14 处另一套写死的色(Google/Material:#E8F0FE、#E8F5E9、
#FFF3E0、#D93025、#777777×2、#555555、#444444、#cccccc、#aaaaaa、
遮罩 #80000000×2、透明 #00000000×2)。枚举挡不住漂移,只有"类"能挡。

## 改法

- 14 处全部换成令牌。新增 accentStrong / warnBg / warnFg(取值对齐 WebUI `:root`
  的 blue-700 / amber-50 / amber-700)与 `Theme.permBg/permFg` —— 权限档位徽标与
  WebUI 的 `PermissionChip.tsx` **同一映射**(plan=蓝 / workspace=绿 / full=琥珀);
  原先写成 `full ? 绿 : 橙`(Material 色),与 WebUI **反着来**。
- 遮罩拆成 `overlayColor` + `overlayAlpha`(照 WebUI 的 `--bg-scrim` + `--bg-dim`
  两段式):遮罩色要能随主题换向,色与透明度焊死成一个 `#AARRGGBB` 等于把枚举写回
  代码。ArkUI 只认单值,故由 `Theme.overlay()` 组装。
- 判据从"枚举旧值"改成"按类挡":pages/ 下**一个裸色值都不许有**(含 8 位
  `#AARRGGBB`),页面清单从硬编码 7 个文件名改成**扫目录** —— 旧写法下,
  接下来要加的发件箱/授权/日历会自动逃出判据。另加一条判据:权限档位配色与
  WebUI `:root` 变量逐一比对,防"看起来差不多"。

## 验证

- 变异测试:往 `InboxPage.ets` 塞一个 `#E8F0FE` → 判据红;撤回 → 绿。
- cross-client 判据 6 → 8 条全绿;`hvigorw assembleHap` BUILD SUCCESSFUL。
- **视觉未验**(模拟器在本机文件沙箱下起不来,见 `docs/HARMONY-ALIGN-PLAN.md` 5.4),
  未写成"已完成"。
This commit is contained in:
2026-09-14 13:29:50 +08:00
parent fac30eb09c
commit b041ea51e4
7 changed files with 148 additions and 34 deletions

View File

@ -10,7 +10,7 @@
*/
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { readFileSync, readdirSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
@ -60,20 +60,79 @@ test('鸿蒙的令牌文件说明了与 WebUI 的对应关系(不是凭空一
assert.match(harmony, /与 WebUI 的令牌\*\*一一对应|对应 WebUI/);
});
test('★ 鸿蒙页面里不得出现与 WebUI 不同的旧调色板', () => {
test('★ 鸿蒙页面里不得出现任何裸色值(枚举挡不住漂移,"类"才能挡)', () => {
/*
* 对齐之前的鸿蒙调色板是另一套(#1A73E8 Google 蓝、#333333、#F5F7FA…
* 与 WebUI 的品牌色/灰阶并不同。203 处已换成 Theme 令牌;这条判据防止
* 以后新写的页面又随手写死一个"差不多"的颜色 —— 那正是漂移的开始。
* 上一版这条判据只列了 8 个旧色值(#1A73E8 / #333333…于是判据全绿的
* 同时pages/ 里还留着 14 处**另一套**写死的色Google/Material 的
* #E8F0FE、#E8F5E9、#FFF3E0、#D93025 与 #777777/#555555/#444444/
* #cccccc/#aaaaaa、遮罩 #80000000、透明 #00000000。
* 枚举只能挡住"列出来的实例",挡不住漂移本身 —— 改成按类挡:
* pages/ 下一个裸色值都不许有(含 8 位 #AARRGGBB颜色只能来自 Theme.ets。
*
* 页面清单也一并改成扫目录:旧版硬编码 7 个文件名,**新加的页面会自动
* 逃出判据** —— 而发件箱/授权/日历正是接下来要加的页面。
*/
const pages = ['MainPage', 'InboxPage', 'SessionsPage', 'MailDetailPage', 'ComposePage', 'SettingsPage', 'LoginPage'];
const old = ['#1A73E8', '#333333', '#666666', '#999999', '#F5F7FA', '#F5F5F5', '#FF4444', '#F0F7FF'];
for (const name of pages) {
const src = readFileSync(join(ROOT, `client/harmony/entry/src/main/ets/pages/${name}.ets`), 'utf8');
for (const c of old) {
assert.ok(!src.includes(`'${c}'`), `${name}.ets 里还有旧调色板色值 ${c}(应改用 Theme 令牌)`);
}
const dir = join(ROOT, 'client/harmony/entry/src/main/ets/pages');
const files = readdirSync(dir).filter(f => f.endsWith('.ets')).sort();
assert.ok(files.length >= 8, `pages/ 下只扫到 ${files.length} 个 .ets判据大概扫错了目录`);
const literal = /#[0-9A-Fa-f]{6,8}\b/g;
for (const f of files) {
const hits = readFileSync(join(dir, f), 'utf8').match(literal);
assert.equal(hits, null, `${f} 里有裸色值 ${hits ? hits.join('、') : ''}(应改用 Theme 令牌)`);
}
// 反向对照:判据本身要能抓到
assert.ok("#1A73E8".length > 0 && old.includes('#1A73E8'));
// 反向对照:判据要真能抓到裸色值(否则写错了正则也是一片绿)
assert.deepEqual("fontColor('#E8F0FE')".match(literal), ['#E8F0FE']);
assert.deepEqual("color('#80000000')".match(literal), ['#80000000']);
// 令牌文件本身必须是**唯一**的颜色来源,否则上面那条会因为"哪儿都没有色值"而空转
assert.ok(/#[0-9A-Fa-f]{6,8}/.test(harmony), 'Theme.ets 里应该有真正的色值');
});
test('权限档位徽标两边同一套色plan=蓝 / workspace=绿 / full=琥珀)', () => {
// WebUI 的映射写在 PermissionChip.tsx 的类名里;鸿蒙的映射是 Theme.permBg/permFg。
const chip = readFileSync(
join(ROOT, 'client/electron/src/components/PermissionChip.tsx'),
'utf8'
);
assert.match(chip, /plan'[\s\S]{0,80}bg-blue-50 text-blue-700/, 'WebUI plan 档应是蓝');
assert.match(chip, /full'[\s\S]{0,80}bg-amber-50 text-amber-700/, 'WebUI full 档应是琥珀');
assert.match(chip, /bg-green-50 text-green-700/, 'WebUI workspace 档应是绿');
// 鸿蒙侧取的是同一批值 —— 直接和 WebUI 的 :root 变量比,防的是"看起来差不多"
const cssVar = (name, src) => {
const m = src.match(new RegExp(`--${name}:\\s*(\\d+)\\s+(\\d+)\\s+(\\d+)`));
assert.ok(m, `WebUI 要有 --${name}`);
return hex('#' + [m[1], m[2], m[3]].map(n => Number(n).toString(16).padStart(2, '0')).join(''));
};
const token = name => {
const m = harmony.match(new RegExp(`${name}: string = '(#[0-9A-Fa-f]{6})'`));
assert.ok(m, `鸿蒙要有令牌 ${name}`);
return hex(m[1]);
};
assert.equal(token('accentSoft'), cssVar('c-blue-50', web), 'plan 底色应与 blue-50 一致');
assert.equal(token('accentStrong'), cssVar('c-blue-700', web), 'plan 字色应与 blue-700 一致');
assert.equal(token('approveBg'), cssVar('c-green-50', web), 'workspace 底色应与 green-50 一致');
assert.equal(token('approveFg'), cssVar('c-green-700', web), 'workspace 字色应与 green-700 一致');
assert.equal(token('warnBg'), cssVar('c-amber-50', web), 'full 底色应与 amber-50 一致');
assert.equal(token('warnFg'), cssVar('c-amber-700', web), 'full 字色应与 amber-700 一致');
});
test('鸿蒙的遮罩是「色 + 透明度」两段式(照 WebUI 的 --bg-scrim + --bg-dim', () => {
/*
* WebUI 的遮罩是**两段**`--bg-scrim`(颜色:浅色=白、深色=黑)+
* `--bg-dim`(透明度),见 `index.css` 的 `.app-backdrop::after`。
* 理由:遮罩色要能**随主题换向** —— 浅色主题用白把图案洗淡,深色主题必须换黑,
* 否则浅色照片在深色界面里糊成一块亮斑、正文读不动WebUI 侧实测踩过)。
* 鸿蒙侧原先写成一个焊死的 `#80000000`,换向时只能再写一个常量 —— 又变成枚举。
*/
assert.match(harmony, /overlayColor: string = '#000000'/, '鸿蒙的遮罩**色**要单独成令牌');
assert.match(harmony, /overlayAlpha: number = 0\.5/, '鸿蒙的遮罩**透明度**要单独成令牌');
// 只查**代码**不查注释Theme.ets 的注释里正当地引用了旧值来解释为什么拆开
const harmonyCode = harmony.replace(/\/\*[\s\S]*?\*\//g, '');
assert.ok(!/#80000000/.test(harmonyCode), '遮罩不该再写成色与透明度焊死的 #AARRGGBB 单值');
// WebUI 侧同构:颜色两套(浅/深,同一个变量名换向)+ 透明度独立
assert.match(web, /--bg-scrim: 255 255 255/, 'WebUI 浅色遮罩色');
assert.match(web, /--bg-scrim: 0 0 0/, 'WebUI 深色遮罩色');
assert.match(web, /--bg-dim:/, 'WebUI 的透明度是独立变量');
// 反向对照:判据要真能抓到"焊死单值"
assert.ok(/#80000000/.test("backgroundColor('#80000000')"), '自检:正则抓不到焊死的单值');
});