跨端: 深色模式下的品牌浅底不跟随 —— 12 处「选中/未读」底色在深色页上刺眼

## 真 bug(设备实测)

深色主题下,「我的」页**选中**的那张账号卡片仍是接近纯白的浅蓝
(实测像素 `(255,255,255)` 级别的浅底压在 `(32,34,36)` 的深色页上)。

根因:`Theme.accentSoft = '#EFF6FF'` 是**写死的浅色**,而它被当作
「选中态背景」用在 12 处(未读邮件行、选中账号、分段选中、登录页模式切换…)。

**为什么只有 WebUI 没这个问题**:它有 CSS 变量的**反转发**机制 ——
`index.css:113` 的 `--c-blue-50: 239 246 255` 在 `.dark` 段(`:475`)被换成
`28 37 54`(深蓝黑)。ArkTS 的 `static readonly` **一个常量一个值**,
没有那层机制 ⇒ 静态常量必须自己提供两个取值。

## 修法

1. `Theme.accentSoftDark = '#1C2536'`(对齐 WebUI `.dark --c-blue-50` 的 `28 37 54`)
2. `Theme.accentSoftFor(dark)` 作为**唯一入口** —— 不在页面里各自
   `isDark ? a : b`:那样每处都会各写一遍,迟早漏一处
   (WebUI 那条"由 test/theme.test.mjs 逐档断言"就是为防这个)
3. **深浅色从哪来**:只有 `MainPage` 算得出(它读 `resourceManager` 的
   `colorMode`)。所以走 `AppStorage` 单向发布(与徽标、windowInsets 同一套):
   MainPage 算 → 写 `agentmail.appearance.isDark` → 各窗格 `@StorageProp` 读。
   6 个文件、12 处,全部改用 `accentSoftFor(this.isDarkNow)`。

**设备实测**:深色下「我的」页账号卡片与收件箱未读行都变成深蓝底,
像素 `(32,34,36)` 与页面底一致(不再刺眼)。

## 判据(这条是新加的,形状值得记)

`cross-client-theme` 新增:**品牌浅底必须有深色变体**。断三件事:
1. 深色变体存在,且**取值从 WebUI 的 `.dark` 段反推**(不是随手挑一个深色);
2. 有按主题选值的**入口**(防"页面各自写三元");
3. **用到它的地方真的走那个入口** —— 扫描所有 `backgroundColor(… accentSoft …)`
   并排除 `accentSoftFor`,把漏改的位置**逐行报出来**。

第 3 条在我改到一半时**当场列出了剩下 9 处**(`CalendarPage:1172`、
`ComposePage:255`、`LoginPage:327`…)—— 这就是它该有的样子:
不是"断言存在某个常量",而是"断言没有一处漏改"。

★ 写这条判据时踩了自己一次:JS 模板串里嵌了反引号包围的标识符
(`` `accentSoft` ``),直接 SyntaxError。改用字符串拼接。

## 判据

`run-all.mjs` → `checks=506 pass=506 fail=0 skip=0 red=0 broken=0 unreported=0`。
`hvigorw assembleHap` 成功;前端重建。

**未验**:日历/登录页在深色下的观感(只逐处改了底色,没逐页截图)。
This commit is contained in:
2026-09-19 16:02:26 +08:00
parent a18014e1e3
commit fc295893cb
10 changed files with 197 additions and 19 deletions

View File

@ -237,7 +237,7 @@ const SELF_OWNED_COLORS = [
*
* 三者的取值理由都已写在 `Theme.ets` 各自的注释里(本判据的要求)。
*/
'navActiveBg', 'navBrandFg', 'badgePlain',
'navActiveBg', 'navBrandFg', 'badgePlain', 'accentSoftDark',
/*
* SSE 连接指示器的四个状态色(`WideSidebar.ets` 的 `sseColorOf`)。
* 逐档对齐 WebUI 的 Tailwind 类(`ConnectionIndicator.tsx:22-25`):
@ -820,3 +820,61 @@ test('★ 断点:两端各是多少、含义是什么、差异被登记(不
+ '否则这条登记会变成没人在核的化石(登记也该跟着事实走)');
}
});
test('★ 品牌浅底必须有深色变体(深色下白底卡片 = 刺眼的 bug)', () => {
/*
* ★★ 2026-09-19 设备实测撞出来的真 bug:
* 深色主题下「多账号」里**选中**的那张卡片仍是接近纯白的浅蓝
* (`Theme.accentSoft = #EFF6FF` 是写死的),在深色页面上刺眼得像渲染错误。
*
* 根因:WebUI 靠 **CSS 变量在 `.dark` 段反转发**解决
* (`index.css:113` 的 `--c-blue-50: 239 246 255` → `:475` 的 `28 37 54`),
* 而 ArkTS 的 `static readonly` **没有那层机制** —— 一个常量一个值。
*
* 修法:显式提供深色取值 + 一个按当前主题选值的入口
* (`Theme.accentSoftFor(dark)`),**不让页面各自 `isDark ? a : b`**
* —— 那样每处都会各写一遍,迟早漏一处。
*
* 判据断三件事:
* ① 深色变体存在,且**取值来自 WebUI 的 `.dark` 段**(不是随手挑一个深色);
* ② 有按主题选值的入口(页面不该自己写三元);
* ③ 用到它的地方**真的走那个入口**(定义了不接 = 那处深色下照样刺眼)。
*/
const themeSrc = prose(join(HARMONY_ETS, 'common/Theme.ets'));
const webCss = prose(join(ROOT, 'client/electron/src/index.css'));
/* ① WebUI 深色下的 blue-50 是权威值 */
const darkBlue50 = /\.dark\s*\{[\s\S]*?--c-blue-50:\s*(\d+)\s+(\d+)\s+(\d+)/.exec(webCss);
assert.ok(darkBlue50, 'WebUI `.dark` 段要有 `--c-blue-50`(品牌浅底的深色取值)');
const [r, g, b] = [darkBlue50[1], darkBlue50[2], darkBlue50[3]].map(Number);
const hex = '#' + [r, g, b].map((v) => v.toString(16).padStart(2, '0').toUpperCase()).join('');
const darkVar = new RegExp(`static readonly accentSoftDark: string = '${hex}'`, 'i');
assert.match(themeSrc, darkVar,
'★ 鸿蒙要有 accentSoftDark = ' + hex + '(对齐 WebUI 的 .dark --c-blue-50)—— ' +
'深色下用写死的浅底会让选中卡片在深色页上刺眼');
/* ② 有按主题选值的入口 */
assert.match(themeSrc, /static accentSoftFor\(dark: boolean\): string/,
'★ 要有 `accentSoftFor(dark)` 入口 —— 页面各自写 `isDark ? a : b` 迟早漏一处');
/* ③ 用到它的地方真的走入口(不是还在直接用浅色那个) */
const pages = readdirSync(HARMONY_ETS + '/pages').filter((f) => f.endsWith('.ets'));
const offenders = [];
for (const f of pages) {
const src = code(join(HARMONY_ETS, 'pages', f));
/*
* 找"把 accentSoft 当背景色用"的地方。用 accentSoftFor 的**不算**。
* 只看 backgroundColor(...),因为 accentSoft 也可以当文字色
* (那种场景下深浅色差异不刺眼,不在本条范围)。
*/
for (const m of src.matchAll(/backgroundColor\(([^)]*Theme\.accentSoft\b[^)]*)\)/g)) {
const line = src.slice(0, m.index).split('\n').length;
offenders.push(`${f}:${line}`);
}
}
assert.deepEqual(offenders, [],
'★ 这些地方还在直接把浅色 accentSoft 当背景(深色下会刺眼):\n ' +
offenders.join('\n ') +
'\n改成 Theme.accentSoftFor(this.isDarkNow)(页面要有一个 isDarkNow 状态)');
});

View File

@ -68,7 +68,7 @@ const SUITE = [
// 「只有通信页深色正常」的回归锁 —— 38 条里后 8 条是这次新增)。
['test/theme.test.mjs', [], 38],
['test/background.test.mjs', [], 44],
['test/cross-client-theme.test.mjs', [], 16],
['test/cross-client-theme.test.mjs', [], 17],
// 左右滑动翻页的**语义契约**(两端逐项相同、数值各自定)—— 这是
// `docs/DEBTS.json` 的 `gesture-semantics` 那条债:它写着「鸿蒙侧出现滑动
// 手势代码时**立即建**(此前建 = 只有一端存在的假判据)」。