jianf:「鸿蒙也同步,但是鸿蒙要求用系统方案」。按 pi 对齐的形状(A/B/C + 品牌色防线)落地。
## 鸿蒙侧改了什么
- `Theme.ets` 的表面/文字/分隔/遮罩/圆角**来源换成系统**:
`pageBg→sys.color.ohos_id_color_background`、`surface→…_list_card_bg`、
`surfaceMuted→…_sub_background`、`border→…_list_separator`、三级文字 `→…_text_primary/secondary/tertiary`、
`overlay→…_mask_regular`、`radiusCard/Control→sys.float.ohos_id_corner_radius_card/button`。
于是这些维度自动跟随深色模式与无障碍设置 —— 这正是"手抄 WebUI 色值"做不到的事。
- **删掉手写玻璃** `#B8FFFFFF`/`#B80F172A`:那两个值等于"我们替系统猜了深色该怎么做"。
换成一个**档次**声明 `navMaterial: BlurStyle = BlurStyle.COMPONENT_THICK` + 导航条上的
`.backgroundBlurStyle(...)`;深浅两套颜色与模糊半径由系统按主题给。
遮罩的两段式(色 + 透明度)同样删掉:拆两段本就是为了"随主题换向",而这件事系统已经做了。
- **每项一张卡/气泡**:收件箱行、会话组头、联系人列表行改成卡片(圆角 + 卡片底色 + 行间距),
联系人列表那条贯通分隔线删除。
- 仍然自己写的只有两类:**品牌色**(`accent = #2563EB`,跨客户端身份)与**业务语义色**
(权限三档、预算三档 —— 系统只有 warning/alert 两个情绪色,凑不出三档,硬套会丢语义)。
## 判据:从"取值相同"改"意图相同"(两侧一起改)
- 品牌蓝**唯一保留取值钉**,并新增防线:不得退化成 `$r('sys.color.*')`
(系统强调色随主题/厂商皮肤变,"两个客户端是同一个产品"就靠不住了)。
- 圆角/材质/遮罩:改成"WebUI 自声明令牌 + 鸿蒙来自系统 + 差异被记录"(§7.12 有意差异表)。
- 新增 A(系统拥有的维度唯一来源是 `$r('sys.*')`,且不得退回 string/number)、
B(旧机制不得回来:`navBg*`、8 位半透明色、`rgb(`/`rgba(`、写死的 14/8)、
C(玻璃位置必须调 `backgroundBlurStyle` 且**只许一层**)。
- pi 指出的洞已补:裸色值判据原来只扫 `#RRGGBB(AA)`,抓不到 `rgba(`/`0xRRGGBBAA` ——
而这几种恰是"改用系统材质"时最容易混进来的形态。现在四种一起扫,且**先剥注释**
(注释里正当地引用旧写法不该被judged红)。
- pi 的 §5 建议也已落地:新增"版本库不得跟踪缓存/构建产物"判据(`.tmp/` 那次 554 个文件的事故判据化),
并放行 `server/internal/static/static/placeholder.html`(go:embed 落点的有意占位,删了 Go 侧编不过)。
## 变异验证(能红,且红在对的地方)
| 变异 | 结果 |
|---|---|
| 品牌蓝 → 系统强调色 | 红 3 条 |
| 手写玻璃 `navBgLight` 回来 | 红 2 条 |
| 导航改用写死半透明色、不调 `backgroundBlurStyle` | 红 2 条 |
| 卡片上再开一层模糊 | 红 1 条("玻璃应只出现在一处,实际 2 处") |
## 验证 / 未验
`hvigorw assembleHap` BUILD SUCCESSFUL;`npm test` 退出码 0
(10 个判据文件全绿:跨端 13 条、系统资源名 4 条、harmony-logic 19 条… + vitest 258/258)。
**未验**:观感(卡片间距、系统材质在导航条上的实际效果、深色模式)—— 需真机/模拟器;
模拟器要人在命令行跑一次 `harmony-emu start`。`sys.*` 名字全部对着 SDK 名表核过,且判据持续盯着。
89 lines
4.7 KiB
JavaScript
89 lines
4.7 KiB
JavaScript
/**
|
||
* 界面必须带一个可核对的构建戳(2026-09-14)。
|
||
*
|
||
* # 为什么
|
||
*
|
||
* 用户连续三轮说「webui 还没改」,而我每次都能证明部署是活的:入口页 `no-cache`、
|
||
* 资源哈希 immutable、线上 bundle 与本地构建逐字节一致……但**隔着屏幕谁也说不清
|
||
* 对方的浏览器跑的是哪一份**。这行字把这件事变成可核对的:它在界面设置里显示
|
||
* `git短哈希·月日-时分`,刷新后数字变了就是拿到了新构建。
|
||
*
|
||
* 判据落在三处接线:vite 注入 → 组件渲染 → 产物里真的有。
|
||
*/
|
||
import { test } from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { execFileSync } from 'node:child_process';
|
||
import { readFileSync, readdirSync } from 'node:fs';
|
||
import { dirname, join } from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
|
||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||
const ROOT = join(HERE, '..', '..', '..');
|
||
const read = p => readFileSync(join(HERE, p), 'utf8');
|
||
const STAMP = /[0-9a-f]{7,}·\d{4}-\d{4}/;
|
||
|
||
test('vite 注入 __BUILD_STAMP__', () => {
|
||
const cfg = read('../vite.config.ts');
|
||
assert.match(cfg, /__BUILD_STAMP__/);
|
||
assert.match(cfg, /git rev-parse --short HEAD/, '戳里要有 git 短哈希');
|
||
});
|
||
|
||
test('界面渲染这个戳', () => {
|
||
const cmp = read('../src/components/BackgroundPicker.tsx');
|
||
assert.match(cmp, /__BUILD_STAMP__/, '组件必须渲染它,而不是只定义');
|
||
assert.match(cmp, /界面构建/, '文案里要能认出来');
|
||
});
|
||
|
||
test('★ 构建产物里真的带着戳', () => {
|
||
const dir = join(HERE, '../dist/assets');
|
||
const js = readdirSync(dir).filter(f => f.startsWith('index-') && f.endsWith('.js'));
|
||
assert.ok(js.length > 0, '先跑 npm run build');
|
||
const found = js.some(f => STAMP.test(readFileSync(join(dir, f), 'utf8')));
|
||
assert.ok(found, '产物里没有构建戳 —— 界面上就永远看不出自己跑的是哪一份');
|
||
});
|
||
|
||
test('判据自检:这个正则不能匹配随便一段文本', () => {
|
||
assert.equal(STAMP.test('界面构建 index-abcdef.js'), false);
|
||
assert.equal(STAMP.test('d2904fc·0914-0910'), true);
|
||
});
|
||
|
||
/*
|
||
* 仓库里不得跟踪缓存/构建产物(`.tmp/` 那次事故的判据化,pi 提议)。
|
||
*
|
||
* 2026-09-14:一次 `git add -A` 把 `.tmp/node-compile-cache/**` 与 `.tmp/studtmp-*`
|
||
* (554 个文件、2.6MB)提交进了版本库。靠"记得先看 git status"防不住下一次 ——
|
||
* 而这件事**不报错、不报警**,代价却落在别人身上:复核者读 diff 判断"改了什么",
|
||
* 554 个缓存文件直接把改动面埋了(同一工作区里还有并发写入者,这尤其致命)。
|
||
* 所以按 pi 的建议,改成一条结构性判据。
|
||
*/
|
||
test('★ 版本库里不得跟踪缓存/构建产物(.tmp、node_modules、dist、release、embed 产物)', () => {
|
||
const tracked = execFileSync('git', ['ls-files'], { encoding: 'utf8', cwd: ROOT })
|
||
.split('\n')
|
||
.filter(Boolean);
|
||
assert.ok(tracked.length > 100, `git ls-files 只列出 ${tracked.length} 个文件,判据大概跑错目录了`);
|
||
|
||
/*
|
||
* `server/internal/static/static/` 是 go:embed 的落点:**它的存在**靠一个
|
||
* 有意跟踪的 `placeholder.html` 撑着(.gitignore 里也是 `*` + `!placeholder.html`)——
|
||
* 那是设计,不是产物。所以这条只放行那一个名字,其余一律算产物。
|
||
*/
|
||
const EMBED_PLACEHOLDER = 'server/internal/static/static/placeholder.html';
|
||
const poison = tracked.filter(f =>
|
||
/^(\.tmp\/|.*\/node_modules\/|client\/electron\/dist\/|client\/electron\/release\/|release\/|server\/internal\/static\/static\/)/.test(f)
|
||
&& f !== EMBED_PLACEHOLDER
|
||
);
|
||
assert.deepEqual(
|
||
poison.slice(0, 10),
|
||
[],
|
||
`版本库里跟踪了缓存/构建产物(共 ${poison.length} 个,前 10 个如下)—— 它们会让复核者看不清真实改动面:\n ${poison.slice(0, 10).join('\n ')}`
|
||
);
|
||
|
||
// 反向对照:判据要真能认出这类路径(否则正则写错也是一片绿)
|
||
const looksTracked = p => /^(\.tmp\/|.*\/node_modules\/|client\/electron\/dist\/|client\/electron\/release\/|release\/|server\/internal\/static\/static\/)/.test(p);
|
||
assert.ok(looksTracked('.tmp/node-compile-cache/v22/0014c7b4'), '自检:认不出 .tmp 下的缓存');
|
||
assert.ok(looksTracked('client/electron/release/agentmail-web_0.1.0_amd64.deb'), '自检:认不出安装包产物');
|
||
assert.ok(!looksTracked('client/electron/src/index.css'), '自检:把源码当成产物了');
|
||
// 那个占位文件必须还在:它是 go:embed 落点存在的唯一理由,删了 Go 侧就编不过
|
||
assert.ok(tracked.includes(EMBED_PLACEHOLDER), 'go:embed 的占位文件丢了 —— 没有它 static/ 目录不存在,Go 侧编不过');
|
||
});
|