From 71585dc42b961d4eed9a5540ea32a734f0483ade Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Mon, 14 Sep 2026 15:04:07 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B7=A8=E7=AB=AF:=20=E9=A2=84=E8=AE=BE?= =?UTF-8?q?=E6=A1=A3=E8=A1=A5=E9=81=AE=E7=BD=A9=EF=BC=88WebUI=20=E7=9A=84?= =?UTF-8?q?=20--bg-dim=20=E4=B8=8D=E5=88=86=E6=A1=A3=E4=BD=8D=EF=BC=89+=20?= =?UTF-8?q?=E4=B8=BB=E9=A2=98=E5=8F=98=E5=8C=96=E6=97=B6=E9=87=8D=E7=AE=97?= =?UTF-8?q?"=E6=88=91=E4=BB=AC=E8=87=AA=E5=B7=B1=E7=AE=97=E7=9A=84?= =?UTF-8?q?=E5=80=BC"=EF=BC=9B=E7=BC=93=E5=AD=98=E9=94=AE=E5=B7=AE?= =?UTF-8?q?=E5=BC=82=E8=AE=B0=E5=85=A5=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 条)。 --- .../electron/test/harmony-appearance.test.mjs | 94 ++++++++++++++++++- .../entry/src/main/ets/model/Wallpaper.ts | 15 ++- .../entry/src/main/ets/pages/MainPage.ets | 69 ++++++++++++++ docs/HARMONY-ALIGN-PLAN.md | 39 ++++++-- 4 files changed, 208 insertions(+), 9 deletions(-) diff --git a/client/electron/test/harmony-appearance.test.mjs b/client/electron/test/harmony-appearance.test.mjs index 19a708e..4805821 100644 --- a/client/electron/test/harmony-appearance.test.mjs +++ b/client/electron/test/harmony-appearance.test.mjs @@ -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(别写错名字)'); +}); diff --git a/client/harmony/entry/src/main/ets/model/Wallpaper.ts b/client/harmony/entry/src/main/ets/model/Wallpaper.ts index 6c76321..efe68ad 100644 --- a/client/harmony/entry/src/main/ets/model/Wallpaper.ts +++ b/client/harmony/entry/src/main/ets/model/Wallpaper.ts @@ -221,7 +221,18 @@ export class BackgroundPlan { dark: boolean = false; presetId: string = ''; layers: PresetLayer[] = []; - /** 压暗(0~1)—— image 档压在图上;preset 档不用 */ + /** + * 遮盖浓度(0~1):**两档都用**(预设档也压),用系统遮罩色刷一层。 + * + * ⚠️ 这里原来写的是"preset 档不用",pi 2026-09-14 读 WebUI 源码后指出**两边不一致**: + * WebUI 的 `applyBackground()` **无条件**写 `--bg-dim`(默认 24), + * `.app-backdrop::after` 是 `rgb(var(--bg-scrim) / var(--bg-dim))` —— + * 遮罩**不区分档位**,预设档一样被压。它的注释写着目的: + * **「背景越花,正文越需要一层遮罩才读得动」** —— 遮罩服务的是可读性,不是装饰。 + * + * 对这边的实际后果:我们已经让出了页面底(`bgActive` 一路到三个 pane), + * 于是正文直接压在原色渐变上 —— 比 WebUI 更艳更亮、更不好读。 + */ scrim: number = 0; } @@ -239,6 +250,8 @@ export function resolveBackground(bgKind: string, presetId: string, scrim: numbe plan.kind = 'preset'; plan.presetId = normalizePreset(presetId); plan.layers = layersFor(plan.presetId, dark); + // 预设档**也**套遮罩:与 WebUI 的 `--bg-dim` 一致(它不区分档位),服务的是可读性 + plan.scrim = scrim; return plan; } if (bgKind === 'image' && hasImage) { diff --git a/client/harmony/entry/src/main/ets/pages/MainPage.ets b/client/harmony/entry/src/main/ets/pages/MainPage.ets index b6a8b72..038aebe 100644 --- a/client/harmony/entry/src/main/ets/pages/MainPage.ets +++ b/client/harmony/entry/src/main/ets/pages/MainPage.ets @@ -11,6 +11,7 @@ import { MailApi, InboxResponse } from '../api/MailApi'; import { AccountManager, AccountInfo } from '../api/AccountManager'; import { SseService, SseEvent } from '../api/SseService'; import { AppearanceStore } from '../common/AppearanceStore'; +import { Configuration, ConfigurationConstant, EnvironmentCallback } from '@kit.AbilityKit'; import { image } from '@kit.ImageKit'; import { AppearanceSnapshot, isDarkMode, scrimOpacity } from '../model/Appearance'; import { BackgroundPlan, PresetLayer, TRANSPARENT, resolveBackground } from '../model/Wallpaper'; @@ -1477,6 +1478,14 @@ struct MainPage { * 逐个加 class 必然漏(漏掉的那块就是一张不透明卡片浮在背景上)」。 */ @State bgActive: boolean = false; + /** 环境变化回调 id(-1 = 没订阅);`lastColorMode` 用来只在真的换向时重算 */ + private envCallbackId: number = -1; + /** + * 上一次看到的系统深浅色。类型跟着 SDK 走(`Configuration.colorMode` 是 + * `ConfigurationConstant.ColorMode | undefined`)—— 我先写成 `number` 初值取 `NOT_SET`, + * 编译直接报"枚举类型不能赋给 number",于是照 SDK 的类型写。 + */ + private lastColorMode: ConfigurationConstant.ColorMode | undefined = undefined; @State wallpaperImage: image.PixelMap | null = null; private gridSettings: RenderingContextSettings = new RenderingContextSettings(true); private gridCtx: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.gridSettings); @@ -1484,6 +1493,57 @@ struct MainPage { aboutToAppear(): void { // 主界面也要应用外观:只在设置页生效的话"一进主界面就变回去"(WebUI 侧踩过) this.applyAppearance(); + this.watchEnvironment(); + } + + aboutToDisappear(): void { + this.unwatchEnvironment(); + } + + /** + * 订阅系统环境变化(深浅色切换),**重算我们自己算出来的那部分**。 + * + * pi 2026-09-14 给的规则(这条会被 P5/P6 咬到): + * **系统自动跟随的东西(语义色、材质)不会顺带把"我们自己算出来的值"一起更新** —— + * 凡是我们计算/缓存且随主题变化的值,都必须挂在**同一个主题变化事件**上重算, + * 否则它迟早是界面上唯一一处不跟随的。 + * + * 这里"我们自己算的"就是预设色板(`layersFor(id, dark)`):系统的 surface/文字/材质 + * 会立刻跟着深浅色换,而色板是我们算的 —— 不重算就会出现"一部分跟随、一部分不跟随"的撕裂, + * 正是这一整轮在治的病。所以除了 `applyAppearance()`(把色板按当前深浅重算一遍), + * 不引入别的机制。 + */ + private watchEnvironment(): void { + const ctx = this.getUIContext().getHostContext(); + if (ctx === undefined) { + return; + } + if (this.envCallbackId >= 0) { + return; // 已经订阅过(aboutToAppear 可能被多次触发) + } + const cb: EnvironmentCallback = { + onConfigurationUpdated: (config: Configuration) => { + if (config.colorMode !== this.lastColorMode) { + this.lastColorMode = config.colorMode; + // 只重算我们自己的那部分;系统色/材质由系统自己换 + this.applyAppearance(); + } + }, + onMemoryLevel: () => { + } + }; + this.envCallbackId = ctx.getApplicationContext().on('environment', cb); + } + + private unwatchEnvironment(): void { + if (this.envCallbackId < 0) { + return; + } + const ctx = this.getUIContext().getHostContext(); + if (ctx !== undefined) { + ctx.getApplicationContext().off('environment', this.envCallbackId); + } + this.envCallbackId = -1; } /** @@ -1585,6 +1645,15 @@ struct MainPage { }) } }, (layer: PresetLayer, idx: number) => layer.kind + idx) + /* + * 遮盖层:**预设档也要**(与 WebUI 的 `--bg-dim` 一致,它不区分档位)。 + * 用**系统遮罩色** + 服务端浓度:浅色下由系统"洗白"、深色下"压黑", + * 不自己写 alpha。目的不是装饰,是让压在渐变上的正文读得动。 + */ + Column() + .width('100%').height('100%') + .backgroundColor(Theme.overlay) + .opacity(this.bgPlan.scrim) } .width('100%').height('100%') } else if (this.bgPlan.kind === 'image' && this.wallpaperImage !== null) { diff --git a/docs/HARMONY-ALIGN-PLAN.md b/docs/HARMONY-ALIGN-PLAN.md index 728afd5..7ef63de 100644 --- a/docs/HARMONY-ALIGN-PLAN.md +++ b/docs/HARMONY-ALIGN-PLAN.md @@ -89,10 +89,18 @@ WebUI 侧踩过这个坑,见 `gateway/handler/permission.go` 的 Note 传递 漏掉了"侧栏点了不翻页")。 2. 判据不许只看截图:要量几何/对比度/命中区。 3. 无法验证的要**如实标注**(例如"编译通过、视觉未验"),不能写成"已完成")。 - ⚠️ 两条口径(这轮各踩过一次,pi 复核时点名): - - **"未验"只能用于"步骤做过、结果没看"**;功能不存在必须写"**没做**" - (把"没做"写成"没验"会让人以为只剩观感风险,实际那里什么都没有); - - **"机制上确定不同"要判,不许记成"未验"**(深色档预设那次就是)。 + ⚠️ **四档口径**(pi 2026-09-14 定,这轮每一条都踩过): + - **没做** —— 功能不存在。不许写成"未验"(那会让人以为只剩观感风险,实际那里什么都没有); + - **未验** —— 做过、没看结果(例如"编译通过、视觉未验"); + - **已知不一致** —— 做了,但**某条件下会露出破绽**:必须写清**触发条件与现象** + (例:"运行期切系统深浅色时,系统语义色/材质立刻跟随,而我们自己算的预设色板不重算 + → 那一刻一部分跟随、一部分不跟随")。这一档最容易被误写成"没做", + 而两者的风险读法完全不同; + - **机制上确定不同** —— 可判的差异,**必须判**、不许记成"未验"(深色档预设那次就是)。 + + **可复用规则**(会咬到 P5/P6):**系统自动跟随的东西(语义色、材质)不会顺带把 + "我们自己算出来的值"一起更新** —— 凡是我们计算/缓存且随主题变化的值, + 都必须挂在**同一个主题变化事件**上重算,否则它迟早是那唯一一处不跟随的。 4. **判据怎么写**:见 `client/electron/test/CRITERIA.md` —— 那份规范管**两个客户端**的判据 (判结构与行为、不判字面与邻接;清单与 allow-list 的形状;变异要红在预期位置; "判据自己不会跑"那个家族的六种宿主)。**写在 electron 的测试目录下只是因为 @@ -508,6 +516,8 @@ deb 也不必从 targets 里摘。已写进 `client/electron/BUILD.md`(含排 | 动效 | 自定义 transition/时长 | `animateTo` + 系统 `curves` | 动效曲线应跟随系统设置(含"减弱动效") | | 遮罩 | 自声明 `--bg-scrim` + `--bg-dim` 两段式 | 系统 `sys.color.ohos_id_color_mask_regular` | 遮罩要随主题换向(浅色洗白/深色压黑),这件事系统已经做了 | | **品牌色** | `--c-blue-600: 37 99 235` | `Theme.accent = '#2563EB'` | **不允许差异** —— 两个客户端是同一个产品 | +| **本地外观缓存的键** | 全局常量 `agentmail.background` | **按账号**:`appearance.` | **鸿蒙是对的,不许为"对齐"退回全局键** —— 全局键的后果是切到服务端没有记录的账号时,`saved=false` 分支会把**上一个账号的外观** push 上去(新账号"继承"了外观,而且写进了服务端)。pi 2026-09-14 确认这条记在 WebUI 侧为开项(换键,或至少在把继承来的值当"本地的"推给从未有记录的账号前停一下) | +| **遮罩浓度的默认值** | 两套:store 的 `dim=24/blur=8` 与 `lib/appearance.ts` 的 `clamp(...,12,4)` | 12 / 4(只有一套) | 服务端**缺字段**时真正落地的是 `clamp` 的默认值,所以按 12/4 对齐;WebUI 那两套值迟早要统一(pi 记在他那边) | **关于 `overlayColor` / `overlayAlpha` 消失**(pi 要求把删除理由记在这里,否则下一个人会当成漏改补回来): 鸿蒙这边的模态走**系统弹窗**(`bindSheet` / 自绘 `Stack` 只做位置,遮罩本身用系统遮罩色), @@ -696,6 +706,9 @@ pi 的原话:「WebUI 的背景有**预设渐变**,服务端存的是 preset **网格档**(CSS 的 `repeating-linear-gradient`)系统没有对应原语 → 用系统 `Canvas` 画线 (线色/间隔照抄 CSS:gray-200 / 0.55 / 28),理由写在模块里; - 图片档:`Image(pixelMap)` + 系统遮罩色按服务端浓度压暗; + 预设档**同样压**(pi 2026-09-14 指出两边不一致:WebUI 的 `--bg-dim` **不区分档位**, + 它的注释写着目的「背景越花,正文越需要一层遮罩才读得动」——遮罩服务的是可读性; + 而这边已经让出页面底,不压的话正文直接压在原色渐变上,比 WebUI 更艳更亮); - `image` 档但图没取回来 → **什么都不画**(画一块空白会被当成"壁纸坏了")。 **判据**(`harmony-appearance` 11 → 17 条):预设 id/顺序/标签与 WebUI `PRESETS` 逐字一致; @@ -797,9 +810,21 @@ pi 指出"邻接不是结构"这条已经在同一个仓库露头**三次**( - 判据:两套值与 `index.css` 的 `:root` / `.dark` 段**逐个相等**;每个预设的深浅两套 **必须真的不同**(否则"两套"是抄了两遍);网格线色也要跟着换;`isDarkMode` 五种输入。 -**已知边界(未做)**:用户在应用**运行期间**改系统深浅色,这边不会自动重算 -(要重进页面/重进应用)。系统侧的正确做法是订阅 `applicationContext.on('environment', …)` -的配置变化回调 —— 记在这里,没做。 +**运行期切深浅色(原记"未做",现按 pi 的三档口径改)**:这**不是"没做"** —— +色板确实按主题算了,只是当时没接环境变化事件,于是运行期切系统深浅色会出现 +"系统语义色/材质立刻跟随、我们自己算的色板不重算"的**撕裂**(这正是**已知不一致**那一档: +触发条件 = 运行期切系统深浅色;现象 = 一部分跟随、一部分不跟随)。 + +现在按 pi 的规则接上了:`applicationContext.on('environment')` 的 `onConfigurationUpdated` +里,**只在 `colorMode` 真的换向时**重算我们自己的那部分(`applyAppearance()`), +页面销毁时 `off` 退订。**状态 = 未验**(这条只有真机/模拟器能验:切一次系统深浅色, +看预设是否跟着换)。 + +SDK 锚点(免得后人凭记忆写):回调接口是 `@ohos.app.ability.EnvironmentCallback` +的 `onConfigurationUpdated(config: Configuration): void` 与 `onMemoryLevel(level)`; +`Configuration` 类型从 `@kit.AbilityKit` 取(不在 `common` 命名空间下)。 +踩过两个编译错:`Configuration.colorMode` 是**枚举 | undefined**,字段写成 `number` 直接报错; +`ApplicationContext.on('environment')` 返回的是 **number 型 callbackId**(不是 void)。 ### 7.17c 手写色清册升级成**跨文件按类扫**(pi 建议)