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 条。
229 lines
12 KiB
Plaintext
229 lines
12 KiB
Plaintext
/*
|
||
* 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 26,7826 条)。
|
||
* 而且有一条判据(`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;
|
||
}
|