跨端: 预设档补遮罩(WebUI 的 --bg-dim 不分档位)+ 主题变化时重算"我们自己算的值";缓存键差异记入表
pi 读 WebUI 源码后发现两处两边不一致,都处理了。这一提交同时改 harmony 与 electron,故自报家门。
## 1 预设档遮罩:**补上**(选"与 WebUI 一致",因为那服务的是可读性)
pi 的证据:WebUI 的 `applyBackground()` **无条件**写 `--bg-dim`(默认 24),
`.app-backdrop::after` 是 `rgb(var(--bg-scrim) / var(--bg-dim))` —— **遮罩不区分档位**;
它的注释写着目的「背景越花,正文越需要一层遮罩才读得动」。
而鸿蒙当时只在 image 档压(`resolveBackground` 里那行注释还写着"preset 档不用"),
且我们已经让出了页面底 → 正文直接压在原色渐变上,**比 WebUI 更艳更亮、更不好读**。
现在两档用**同一个浓度**(同一个服务端字段),页面在预设分支的渐变之上加一层
系统遮罩色 × 浓度(浅色由系统洗白、深色压黑,不自己写 alpha)。
判据:旧断言"preset 档不压暗"**反过来**(留着理由),另加一条钉"两档同一浓度"+
"两处遮盖层都在"+"WebUI 确实无条件写 --bg-dim"(这条差异有据可查)。
变异:预设档不压 → 红 2 条;页面预设分支的遮盖层被删 → 红。
## 2 多账号缓存键:**鸿蒙是对的,不许退回**(pi 点名)
WebUI 的键是全局常量 `agentmail.background`,后果是切到服务端没有记录的账号时
`saved=false` 分支会把**上一个账号的外观** push 上去(新账号"继承"了外观,还写进了服务端)。
这条差异进 §7.12「有意差异」表,**明确写"鸿蒙是对的"**,
判据防的就是"将来有人为了两边一致把它改回去":取键函数的**正文**里必须拼账号 id
(按块取,不是看调用点出现过 `accountId` 就当数 —— "判结构要配对/解析"那条对我自己也适用),
且不许出现 WebUI 那个全局键。变异:`prefKey` 去掉账号 → 红 2 条。
## 3 "未做" → 按 pi 的三档口径改成**已知不一致**,并把系统侧的接法做掉
pi 指出这不是"没做":色板确实按主题算了,只是没接环境变化事件 ——
现象是运行期切系统深浅色时"系统语义色/材质立刻跟随、我们自己算的色板不重算"的**撕裂**。
他给的规则我记成了通用规则(会咬到 P5/P6):**系统自动跟随的东西不会顺带把
"我们自己计算/缓存的值"一起更新** —— 凡随主题变化的自算值都要挂在**同一个主题变化事件**上。
照做:`applicationContext.on('environment')` → `onConfigurationUpdated` 里
**只在 `colorMode` 真换向时**重算(`applyAppearance()`),页面销毁 `off` 退订。
状态 = **未验**(只有真机/模拟器能验:切一次系统深浅色看预设是否跟着换)。
SDK 锚点与两个编译错都记进 §7.17b:`EnvironmentCallback.onConfigurationUpdated(config: Configuration)`
(`Configuration` 从 `@kit.AbilityKit` 取,**不在** `common` 命名空间下)、
`Configuration.colorMode` 是**枚举 | undefined**(字段写成 `number` 直接编译失败)、
`on('environment')` 返回 **number 型 callbackId**。变异:不订阅 → 红。
## 4 文档
- §7.12 加两行:缓存键(含 WebUI 侧开项)、遮罩浓度默认值那两套(`24/8` vs `12/4`,
按 `clamp` 的 12/4 对齐,因为服务端缺字段时落地的是它)。
- §四 验收纪律改成**四档口径**:没做 / 未验 / 已知不一致(要写触发条件与现象)/
机制上确定不同(必须判),并把 pi 那条"自算值必须挂主题事件"的可复用规则写进去。
## 验证
`hvigorw assembleHap` BUILD SUCCESSFUL;`npm test` 退出码 0
(12 个判据文件全绿 + vitest 258/258;harmony-appearance 20 → 23 条)。
This commit is contained in:
@ -346,7 +346,15 @@ test('画什么:none / preset / image 三档,图没取回来不许画空白'
|
||||
const preset = W.resolveBackground('preset', 'mint', 0.2, false, false);
|
||||
assert.equal(preset.kind, 'preset');
|
||||
assert.ok(preset.layers.length >= 1, 'preset 档要真的画出层来(这就是那个缺口的判据)');
|
||||
assert.equal(preset.scrim, 0, 'preset 档不压暗(压暗是给图片用的)');
|
||||
/*
|
||||
* ⚠️ 这里原来断言的是"preset 档不压暗"。pi 2026-09-14 读 WebUI 源码后指出
|
||||
* **两边不一致**:WebUI 的 `applyBackground()` 无条件写 `--bg-dim`(默认 24),
|
||||
* `.app-backdrop::after` 是 `rgb(var(--bg-scrim) / var(--bg-dim))` —— 遮罩不区分档位;
|
||||
* 它的注释写着目的「背景越花,正文越需要一层遮罩才读得动」(可读性,不是装饰)。
|
||||
* 而鸿蒙当时只在 image 档压,且我们已经让出页面底 → 正文直接压在原色渐变上,比 WebUI 更艳更亮。
|
||||
* 现在两档都压(同一个浓度),判据按"两边一致"钉住。
|
||||
*/
|
||||
assert.equal(preset.scrim, 0.2, 'preset 档**也要**压暗,浓度与 image 档一致(WebUI 的 --bg-dim 不区分档位)');
|
||||
|
||||
const img = W.resolveBackground('image', 'aurora', 0.24, true, false);
|
||||
assert.equal(img.kind, 'image');
|
||||
@ -555,3 +563,87 @@ test('★ isDarkMode:system 要看系统当时的深浅,读不到时按浅
|
||||
const callText = main.slice(callAt, callEnd + 1);
|
||||
assert.ok(/\bdark\b\s*\)/.test(callText), `深浅色要传给背景计划:${callText}`);
|
||||
});
|
||||
|
||||
test('★ 预设档的遮盖:两档同一个浓度(WebUI 的 --bg-dim 不区分档位)+ 遮盖层用系统遮罩色', () => {
|
||||
/*
|
||||
* pi 的原话:「这个遮罩在 WebUI 那里服务的是**可读性**,不是装饰。」
|
||||
* 所以判据钉三件事:浓度来自同一个入参(不是各写一个数)、preset 也要有、
|
||||
* 遮盖层用的是**系统遮罩色**(深浅换向由系统负责,不是我们写 alpha)。
|
||||
*/
|
||||
const same = W.resolveBackground('preset', 'aurora', 0.3, false, false);
|
||||
const img = W.resolveBackground('image', 'aurora', 0.3, true, false);
|
||||
assert.equal(same.scrim, 0.3);
|
||||
assert.equal(img.scrim, 0.3, '两档要用同一个浓度(同一个服务端字段)');
|
||||
for (const id of W.PRESET_IDS) {
|
||||
for (const dark of [false, true]) {
|
||||
assert.equal(W.resolveBackground('preset', id, 0.15, false, dark).scrim, 0.15,
|
||||
`${id}(dark=${dark})预设档也要压暗`);
|
||||
}
|
||||
}
|
||||
// WebUI 侧的前提:遮罩真的不区分档位(否则"对齐"就没有依据)
|
||||
const bgStore = readFileSync(join(ROOT, 'client/electron/src/stores/backgroundStore.ts'), 'utf8');
|
||||
assert.match(bgStore, /--bg-dim|setProperty\('--bg-dim'/, 'WebUI 要无条件写 --bg-dim(这是"两档都压"的依据)');
|
||||
const main = read('pages/MainPage.ets');
|
||||
// 两档各有一处遮盖层(都用系统遮罩色 + 算出来的浓度)
|
||||
const scrims = [...main.matchAll(/\.backgroundColor\(Theme\.overlay\)\s*\n\s*\.opacity\(this\.bgPlan\.scrim\)/g)];
|
||||
assert.ok(scrims.length >= 2, `预设档与图片档各要有一层遮盖(实际 ${scrims.length} 处)`);
|
||||
});
|
||||
|
||||
test('★ 多账号缓存键:**按账号**分(鸿蒙是对的,不许为"对齐 WebUI"退回全局键)', () => {
|
||||
/*
|
||||
* pi 2026-09-14:WebUI 的缓存键是**全局常量** `agentmail.background`,
|
||||
* 后果是切到一个服务端没有记录的账号时 `saved=false` 分支会把**上一个账号的外观**
|
||||
* push 上去(于是新账号"继承"了外观,而且写进了服务端)。
|
||||
* 鸿蒙这边按账号分键是对的 —— 所以这条判据**防的是将来有人为了"两边一致"把它改回去**。
|
||||
*/
|
||||
const store = read('common/AppearanceStore.ets');
|
||||
/*
|
||||
* 键由 `prefKey(accountId)` 拼 —— 所以判据要**取出这个函数的正文**再断言
|
||||
* (`prefKey` 存在不等于它带账号;这正是"判结构要配对/解析"那条),
|
||||
* 而不是看调用点有没有出现 `accountId` 就当数。
|
||||
*/
|
||||
const at = store.indexOf('prefKey(accountId: string)');
|
||||
assert.ok(at > 0, '要有一个按账号取键的函数');
|
||||
let depth = 0;
|
||||
let end = at;
|
||||
for (let i = store.indexOf('{', at); i < store.length; i++) {
|
||||
if (store[i] === '{') depth++;
|
||||
else if (store[i] === '}') { depth--; if (depth === 0) { end = i; break; } }
|
||||
}
|
||||
const body = store.slice(at, end + 1);
|
||||
assert.match(body, /KEY_PREFIX\s*\+\s*accountId/, `取键函数必须把账号拼进去(现在:${body.replace(/\s+/g, ' ')})`);
|
||||
assert.match(store, /loadLocal\([^)]*accountId/, '读缓存要按账号');
|
||||
assert.ok(!/AGENTMAIL_BACKGROUND|'agentmail\.background'/.test(store),
|
||||
'不要退回 WebUI 那个全局键(那正是"换账号继承上一个人的外观"的成因)');
|
||||
assert.match(store, /loadLocal\([^)]*accountId/, '读缓存要按账号');
|
||||
// WebUI 侧的前提也钉一下:它确实是全局键(这条差异有据可查)
|
||||
const webLib = readFileSync(join(ROOT, 'client/electron/src/lib/appearance.ts'), 'utf8');
|
||||
assert.match(webLib, /agentmail\.background/, 'WebUI 现在是全局键(差异记录的依据)');
|
||||
});
|
||||
|
||||
test('★ 主题变化时**我们自己算的值**要跟着重算(pi 的规则:系统只跟它自己那部分)', () => {
|
||||
/*
|
||||
* pi 的规则:「系统自动跟随的东西(语义色、材质)不会顺带把"我们自己算出来的值"
|
||||
* 一起更新 —— 凡是我们计算/缓存且随主题变化的值,都必须挂在**同一个主题变化事件**上重算,
|
||||
* 否则它迟早是那唯一一处不跟随的。」
|
||||
* 这里"我们自己算的"就是预设色板(`layersFor(id, dark)`):系统 surface/文字/材质会立刻换,
|
||||
* 色板不重算 → 界面上一部分跟随、一部分不跟随(撕裂)。
|
||||
*/
|
||||
const main = read('pages/MainPage.ets');
|
||||
assert.match(main, /getApplicationContext\(\)\.on\('environment'/, '要订阅系统环境变化');
|
||||
assert.match(main, /onConfigurationUpdated/, '要处理配置变化回调');
|
||||
assert.match(main, /config\.colorMode !== this\.lastColorMode/, '只在深浅色真的换了时才重算');
|
||||
assert.match(main, /off\('environment'/, '页面销毁要退订(否则回调挂在已销毁的页面上)');
|
||||
// 重算的必须是那份"我们算的值",而不是重新读一遍系统色
|
||||
const watchAt = main.indexOf("on('environment'");
|
||||
let depth = 0;
|
||||
let end = watchAt;
|
||||
for (let i = main.indexOf('(', watchAt); i < main.length; i++) {
|
||||
if (main[i] === '(') depth++;
|
||||
else if (main[i] === ')') { depth--; if (depth === 0) { end = i; break; } }
|
||||
}
|
||||
assert.ok(/applyAppearance\(\)/.test(main.slice(watchAt, end + 400)), '回调里要重算外观(色板属于"我们算的")');
|
||||
// SDK 锚点:这个 API 与回调形状不能凭记忆写
|
||||
const sdk = readFileSync(join(CLT, 'sdk/default/openharmony/ets/api/@ohos.app.ability.EnvironmentCallback.d.ts'), 'utf8');
|
||||
assert.match(sdk, /onConfigurationUpdated\(config: Configuration\): void/, 'SDK 里回调是 onConfigurationUpdated(别写错名字)');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user