Files
MailUI4Agents/client/harmony/entry/src/main/ets/common/Theme.ets
JianFeeeee a6b5e52f45 test(harmony): 判据按 pi 复核意见补强 —— 手写色登记表、玻璃叠用形状、按标题取小节、遮罩必须被用、废弃 API 清单从 SDK 生成
pi 逐行读了 `cross-client-theme.test.mjs` 与 `Theme.ets` 后指出五处(第一处是真缺口),
外加一条建议。全部处理,并且**每一处都用变异验证过**。

## 一(真缺口):枚举 11 个"必须是系统资源"的名字,挡不住第 12 个新写死的手写色

`static readonly brandSecondary: string = '#123456'` 这种新增**三条判据都碰不到**:
A(不在名单里)、B(六位、不是半透明)、裸色值那条(只管 `pages/`)。
原理与当初 14 处 Google 色逃出去是同一条 —— **枚举挡实例,类才挡漂移**,
只是这次枚举的单位是**名字**。补 `A2` 条:

- 枚举 `Theme.ets` 里所有 `static readonly X: string = '#……'`,未登记的 → 红;
- 名单里已不存在的名字 → 红(名单不能烂成化石);
- `Theme.ets` 里的「手写色登记表」段必须逐个列出这些名字(理由不能只存在于记忆里);
- 自检:把一个**新的**手写色塞进源码字符串,确认这条抓得到。
  `Theme.ets` 因此新增登记表(17 项,每项一行理由,按品牌 / 业务语义 / 档位胶囊分组)。

变异:Theme.ets 加 `brandSecondary` → 红。

## 二:C 的"玻璃只有一处"**说错了自己断言的东西**

`assert.equal(glassCalls.length, 1)` 断言的是"全仓共一处",**不是**"没有嵌套":
同页两个**并列**玻璃面(没问题)会让它红,真嵌套它没在判。P5 正是悬浮玻璃导航,
到时若把 1 改成 2 就等于不判。改成形状判据:

- 收集每个调用点所作用的**组件块**(按括号配对回溯;注意 ArkUI 修饰符是链式的,
  `X.blur(A).blur(B)` 前面是 `)` 不是 `}` —— 第一版只看一个字符,对这种写法**静默失效**,
  是判据自检抓出来的);
- 判两种叠法:同一组件上叠多次(同一块)、以及套在另一层玻璃的子树里;
- 每一处玻璃都要在 `GLASS_REGISTRY` 里登记(附一句为什么),名单里的位置若已不存在 → 红。

变异:链式叠两层 → 红;通信页多开一处未登记玻璃 → 红;导航条块内嵌玻璃 → 红。

## 三:文档小节用 `indexOf('有意差异')` 会**拿错段落**

别的段落正文里出现这四个字,切片就从那里开始,后面所有断言都在**别的段落**上判。
改成**按标题**定位(正则匹配 `^#{2,4}…有意差异…$`),并加了表头列名断言
(WebUI / 鸿蒙 / 为什么)—— 拿错段落时表头不会是这个形状,于是它自己会红。

顺带修掉一个**我自己的**同类毛病:品牌色那行原来用"关键词 + 80 字符窗口"判,
窗口宽度在赌表格单元格字符数(该行两格之和 > 80)。改成**按行取**那一行再断言。
另外把偏移算术的切片换成**按行**切片(偏移差一个字符就会把最后一行拦腰截断,
现象是"品牌色那行只剩 57 字符"这种看着像文案、其实像切片的怪事)。

变异:文件前面插入含「有意差异」的段落 → 仍绿(按标题定位生效);
再改坏真表里的品牌色行 → 红(确实读的是那一张表)。

## 四:`Theme.overlay` 只判了"存在",没判"被用"

没使用点的令牌是自证。补断言:它必须在 `Theme.ets` **之外**有真实使用点
(实测 `SettingsPage.ets` / `MailDetailPage.ets` 两处自绘弹层在用),
并把 `overlayColor`/`overlayAlpha` 消失的理由写进 §7.12 —— 否则下一个人会当成漏改补回来。

## 五:材质档次是这次替换里唯一"判据绿但可能观感错"的地方

`COMPONENT_THICK` 的依据(对 THIN / BACKGROUND_* / ULTRA_THICK 的取舍)写进 §7.12,
并把"材质档次在导航条上的实际观感"列为**模拟器起来后第一个要看的项**(间距是数字,材质是判断)。

## 六(建议):废弃 API 判据**类化** —— 清单从 SDK 生成

原来是手写"不得再用全局 `promptAction.showToast`"(只挡已踩过的那个)。
现在从 SDK 生成:顶层(花括号深度 0)被标 `@deprecated` 的 `declare function`
—— 实测 68 个名字(含 `animateTo` / `getContext` / `px2vp`),配一份**空**的 allow-list。
深度判定是必要的:`declare namespace fileIo { declare function open() }` 里的 `open`
是命名空间成员,算进来会造一堆假红。只算全局调用(排除 `x.name(`)。

变异:调 `px2vp(10)` → 红;`this.px2vp(10)`(成员调用)→ 绿(假阳性自检)。

## 验证

`hvigorw assembleHap` BUILD SUCCESSFUL;`npm test` 退出码 0
(11 个判据文件全绿 + vitest 258/258)。cross-client-theme 13 → 14 条,harmony-system-api 4 → 5 条。
2026-09-14 14:26:45 +08:00

229 lines
12 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* AgentMail 鸿蒙客户端 — 设计令牌
*
* ## 这个文件在 2026-09-14 换了一次"来源"jianf「鸿蒙要求用系统方案」
*
* 原先这里的每一格都是**手抄 WebUI 的色值**`#F8FAFC`、`#0F172A`…)。现在分成两类,
* 分界线是"**系统有没有这份语义**"
*
* - **表面 / 文字 / 分隔 / 遮罩 / 圆角 / 材质 —— 交给系统**`$r('sys.color.*')`、
* `$r('sys.float.*')`、`BlurStyle.*`)。理由不是"看起来更原生",而是三条实际的:
* ① 它们会**跟随深色模式**(手抄的值不会,这正是 WebUI 那边"半深不浅"的病根);
* ② 跟随系统动效/无障碍设置;
* ③ 少一处会漂移的副本 —— 这些格子的值不再由我们决定,也就不会再和系统打架。
* - **品牌色与业务语义色 —— 仍然自己写**`accent = #2563EB` 是**跨客户端身份**
* (两个客户端是同一个产品),不能退化成随主题/厂商皮肤变的系统强调色;
* 权限三档(蓝/绿/琥珀)与预算三档(灰/橙/红)在系统里**没有对应物**
* (系统只有 warning/alert 两个"情绪色"),硬套会把语义丢掉。
*
* ## 名字不是猜的
*
* `sys.*` 的名字一旦写错,**编译期不报**、只有真机运行到那一行才炸 —— 而本机没有设备。
* 所以名字全部对着 SDK 自带的系统资源名表核过:
* `sdk/default/openharmony/toolchains/id_defined.json`(本机 API 267826 条)。
* 而且有一条判据(`client/electron/test/harmony-system-api.test.mjs`)持续盯着这件事:
* 源码里每个 `$r('sys.<type>.<name>')` 都必须在表里存在且类型相符。
*
* ## 保留的取舍
*
* - WebUI 的"正文面"是 0.88 不透明的玻璃(字要读得清);鸿蒙用的系统卡片底色也是不透明的,
* 结论一致:**正文面不透,只有浮在内容之上的那一层用材质**"玻璃只出现在一层")。
* - 字体大小仍与 WebUI 的 text-2xs/xs/sm 对齐(字号不是"系统方案"要解决的问题,
* 两边的版式意图是同一套)。
*/
export class Theme {
// ─────────────── 表面:系统语义色 ───────────────
/** 页面底色(系统 `ohos_id_color_background`:浅色白/深色深灰,自动跟随主题) */
static readonly pageBg: Resource = $r('sys.color.ohos_id_color_background');
/** 承载文字的面 = **列表卡片底色**(对应"每项一张卡/气泡",而不是通栏底色) */
static readonly surface: Resource = $r('sys.color.ohos_id_color_list_card_bg');
/** 次级面(分组底、列表行 hover */
static readonly surfaceMuted: Resource = $r('sys.color.ohos_id_color_sub_background');
/** 分隔线 */
static readonly border: Resource = $r('sys.color.ohos_id_color_list_separator');
// ─────────────── 文字:系统语义色(三级) ───────────────
static readonly textPrimary: Resource = $r('sys.color.ohos_id_color_text_primary');
static readonly textMuted: Resource = $r('sys.color.ohos_id_color_text_secondary');
static readonly textSubtle: Resource = $r('sys.color.ohos_id_color_text_tertiary');
// ─────────────── 遮罩与材质:交给系统 ───────────────
/**
* 遮罩(模态/淡出层):系统遮罩色。
*
* 这里原来存着 `overlayColor` + `overlayAlpha` 两个常量,再用 `overlay()` 拼成
* `#AARRGGBB` —— 之所以要"色与透明度分开",是因为**遮罩色必须随主题换向**
* (浅色主题用白把图案洗淡、深色主题必须换黑,否则浅色照片在深色界面里糊成一块亮斑)。
* 那件事现在由系统做:一个语义色就够,且换向不会再漏。
*/
static readonly overlay: Resource = $r('sys.color.ohos_id_color_mask_regular');
/**
* 导航/浮层的材质档次。**不再有 `#B8FFFFFF` / `#B80F172A` 这种手写玻璃 alpha** ——
* 那两个值等于"我们替系统猜了深色该怎么做",与"用系统方案"直接冲突;
* 而材质档次是系统给的,深浅两套颜色由系统按主题挑。
*
* 用 `COMPONENT_THICK`:贴在界面组件上的一层材质(导航条正属于这一类)。
*/
static readonly navMaterial: BlurStyle = BlurStyle.COMPONENT_THICK;
// ─────────────── 圆角:系统尺寸 ───────────────
/** 卡片圆角:系统"卡片"圆角(不再与 WebUI 的 14vp 绑死 —— 允许各自跟随系统) */
static readonly radiusCard: Resource = $r('sys.float.ohos_id_corner_radius_card');
/** 控件圆角:系统"按钮"圆角 */
static readonly radiusControl: Resource = $r('sys.float.ohos_id_corner_radius_button');
/*
* ─────────────── 手写色**登记表**(改这里要一起改判据) ───────────────
*
* 为什么需要这张表:本文件里"自己写的色值"是有理由的(品牌身份 + 系统没有对应物的
* 业务语义色),但**理由不能靠记忆**。判据(`cross-client-theme.test.mjs` 的
* A2 条)会枚举本文件里所有 `static readonly X: string = '#……'`
* 发现**未登记的名字就判红** —— 于是"新写死一个色"必须显式过一道:
* 要么登记在这里并写清理由,要么它本来就该走 `$r('sys.*')`。
*
* 这条是 pi 读出来的真缺口:只枚举 11 个"必须是系统资源"的名字,挡不住
* 第 12 个**新加的手写色**(它不在名单里,于是 A/B/裸色值三条都碰不到它)。
* 「枚举挡实例,类才挡漂移」—— 这次枚举的是**名字**,所以要有名单。
*
* 登记项17 个,名字与判据里的 SELF_OWNED_COLORS 逐字一致 —— 逐个列出而不是缩写,
* 这样 grep 一个名字就能找到它的理由):
*
* 品牌(跨客户端身份,系统给不了):
* · accent #2563EB 与 WebUI 的 --c-blue-600 逐字一致
* · accentFg #FFFFFF 品牌底上的文字
* · accentSoft #EFF6FF 品牌浅底(选中态背景)
* · accentStrong #1D4ED8 品牌深色变体(选中文字/边框)
*
* 业务语义(系统只有 list/warning/alert 这一档,没有"同意/拒绝"
* · approve #15803D 同意(绿)
* · approveBg #F0FDF4 同意底
* · approveFg #15803D 同意文字
* · danger #B91C1C 拒绝/危险(红)
* · dangerBg #FEF2F2 拒绝底
* · warnBg #FFFBEB 警示底(对应 WebUI --c-amber-50
* · warnFg #B45309 警示文字(对应 WebUI --c-amber-700
*
* 档位胶囊(档位是产品语义:系统不认识 plan / workspace / full
* · chipNeutralBg #F3F4F6 plan 档底(最宽档但不报警)
* · chipNeutralFg #5A6270 plan 档文字
* · chipSpentBg #FEE2E2 预算耗尽底
* · chipSpentFg #B91C1C 预算耗尽文字
* · chipWarnBg #FFEDD5 预算紧张底
* · chipWarnFg #C2410C 预算紧张文字
*
* 不在这里的手写色只有一种合法去处:`$r('sys.*')`(跟随系统/深色模式)。
*/
// ─────────────── 品牌色:跨客户端身份,必须自己写 ───────────────
/**
* 品牌蓝(按钮、链接、焦点环)。
*
* **不要**为了"用系统方案"把它换成系统的强调色:系统强调色会随主题/厂商皮肤变,
* 一旦换过去,"两个客户端是同一个产品"这件事就靠不住了。
* 这是整个文件里**唯一必须与 WebUI 逐字一致**的取值(判据钉住)。
*/
static readonly accent: string = '#2563EB';
static readonly accentFg: string = '#FFFFFF';
/** 品牌蓝的浅底 / 深前景(与 WebUI 的 blue-50 / blue-700 成对) */
static readonly accentSoft: string = '#EFF6FF';
static readonly accentStrong: string = '#1D4ED8';
/** 导航文字:未选中 / 选中(对应 --nav-fg-muted / --nav-active-fg */
static readonly navFg: Resource = $r('sys.color.ohos_id_color_text_secondary');
static readonly navFgActive: string = Theme.accentStrong;
// ─────────────── 业务语义色:系统没有对应物,继续自己写 ───────────────
/** 语义色:同意 / 拒绝(对应 WebUI 的 approve/danger */
static readonly approve: string = '#15803D';
static readonly danger: string = '#B91C1C';
static readonly dangerBg: string = '#FEF2F2';
static readonly approveBg: string = '#F0FDF4';
static readonly approveFg: string = '#15803D';
/** 警示面/前景(对应 WebUI 的 --c-amber-50 / --c-amber-700—— 权限 full 档、待决策徽标 */
static readonly warnBg: string = '#FFFBEB';
static readonly warnFg: string = '#B45309';
/**
* 权限档位 → 徽标底色 / 文字色。
*
* 与 WebUI 的 `PermissionChip.tsx` **同一映射**plan=蓝 / workspace=绿 / full=琥珀),
* 认不出的档位与空档位按 workspace 处理 —— 映射只有这一处实现,
* 页面不再各自 if-else 挑颜色。
*
* 为什么不用系统情绪色:系统只有 warning/alert 两个,凑不出三档;
* 而档位是**可被文档与判据按名字引用的枚举标识**plan/workspace/full
* 用情绪色顶替它,界面对了语义丢了。
*/
static permBg(mode: string): string {
if (mode === 'plan') {
return Theme.accentSoft;
}
if (mode === 'full') {
return Theme.warnBg;
}
return Theme.approveBg;
}
static permFg(mode: string): string {
if (mode === 'plan') {
return Theme.accentStrong;
}
if (mode === 'full') {
return Theme.warnFg;
}
return Theme.approveFg;
}
/** 中性 chip对应 WebUI 的 --c-gray-100 / --c-gray-500—— 预算条"还宽裕"档 */
static readonly chipNeutralBg: string = '#F3F4F6';
static readonly chipNeutralFg: string = '#5A6270';
/** 预算用尽(对应 WebUI 的 --c-red-100 / --c-red-700 */
static readonly chipSpentBg: string = '#FEE2E2';
static readonly chipSpentFg: string = '#B91C1C';
/** 预算将尽(对应 WebUI 的 --c-orange-100 / --c-orange-700 */
static readonly chipWarnBg: string = '#FFEDD5';
static readonly chipWarnFg: string = '#C2410C';
/**
* 往返预算档位 → chip 底色 / 字色。
*
* 与 WebUI 的 `BudgetChip` **同一映射**`WorkCard.tsx`
* 剩 0 = 红(用尽)、剩 ≤1 = 橙(将尽,任务需要人介入)、其余 = 中性灰。
* 档位本身由 `MailGrouping.ts` 的 `budgetState()` 算(那是可被判据执行的一层),
* 这里只管"哪个档用什么颜色"。这三个色**不进跨端"取值相同"判据**(业务局部),
* 但必须进枚举完整性判据(三档齐全、认不出的归一到中性档)。
*/
static budgetBg(state: string): string {
if (state === 'spent') {
return Theme.chipSpentBg;
}
if (state === 'warn') {
return Theme.chipWarnBg;
}
return Theme.chipNeutralBg;
}
static budgetFg(state: string): string {
if (state === 'spent') {
return Theme.chipSpentFg;
}
if (state === 'warn') {
return Theme.chipWarnFg;
}
return Theme.chipNeutralFg;
}
/** 字体大小(与 WebUI 的 text-2xs/xs/sm 对齐) */
static readonly fontTiny: number = 11;
static readonly fontSmall: number = 12;
static readonly fontBody: number = 14;
}