Files
MailUI4Agents/client/harmony/entry/src/main/ets/model/Wallpaper.ts
JianFeeeee 77aa42623a 跨端: debt-visibility 补登记(新判据文件不会自动跑守卫)+ blurStyleFor 删除后的注释真相
## 一、`debt-visibility` 那条红:**新文件不会自动跑一遍守卫**

```
这些文件里有"边界声明",但一次都没登记:
  harmony-deviceprobe.test.mjs(2 处)      ← e917b87/4880c31 新加的判据文件
```

**这是同一个洞在新文件上的复发**:上一轮我刚修完 `harmony-admin` / `harmony-imageprep`,
下一个**新建的**判据文件又踩了同一个坑。pi 之所以看见,只是因为他跑了整个套件 ——
**缺的不是"记得登记",是"新建判据文件"这个动作没有守卫**。这条形状与"写了判据忘了接线"同族,
只是这次忘的是**登记边界**。

处理:**按次数登记(2),不整文件放行** —— 整文件放行的话,将来在这个文件里写一句
真实的「这里没判 / 已知缺口」就**不会红**。那 2 处本身也不是"这块没验",
而是对**词表本身**的断言(`unverifiedReason(...)` 必须含「未验」)。

同时在 `docs/DEBTS.json` 补一笔 `deviceprobe-fixture-timing`(`where` 指向该文件)——
`debt-visibility` 的第二条要求"声明必须有对应的一笔",两处各写各的会让审计只找到一处。
这笔的**到期前提是"两份 fixture 变成当场采集而不是人工存文件"**。

⚠️ **Go 侧未能本机验证**:`go test ./internal/repo/` 在本机报
`module cache not found: neither GOMODCACHE nor GOPATH is set`。我读了
`TestDebtLedgerMatchesMeasurement`:它只校验"每笔都有 due/where"+"三笔必须同处登记",
**没有"所有 id 必须在 Go 侧也列出"的断言**,所以新增一笔不需要改 Go。
但这是**读代码得出的结论,不是跑出来的** —— 如实标成未验。

## 二、`blurStyleFor` 删除后:生产代码里 5 处注释在说一个**不存在的函数**

函数已按 pi 的裁定删除(别的会话的 `9a10ab2` 落的)。但删除后
`Wallpaper.ts`(4 处)与 `MainPage.ets`(1 处)还在用现在时提它:

```
Wallpaper.ts:242  "由 `model/Appearance.ts` 的 `blurStyleFor` 映射成系统材质档"
Wallpaper.ts:248  "页面拿它去问 `blurStyleFor`"
Wallpaper.ts:251  "`blurStyleFor` 也写了、就是没有任何调用点"
Wallpaper.ts:311  "`blurStyleFor` 里面也有一次 clamp,那是它自己的防线"
MainPage.ets:1711 "(`blurStyleFor` 那张表服务的是**材质档**…)"
```

**这正是本会话反复在消的"注释描述一份不存在的代码"**,而它现在比之前更危险:
下一个人读注释会去找一个**已经被有意删掉**的函数,找不到就会**重新实现它** ——
而"它为什么不该回来"恰恰是那次删除唯一值钱的东西。

已全部改成**过去时 + 它已删除**,并在 `Appearance.ts` 原处留了碑文(函数没了,理由不能没)。
`:251` 那处尤其要改:原文说"`blurStyleFor` 也写了、就是没有任何调用点"——
函数已不存在,这句会让读者以为**还差一个调用点没补**,而事实是**连函数都不该有**。

## 三、这条碑文判据我做了变异验证

`harmony-appearance.test.mjs` 里那条「碑文不许回来」的判据**确实在校验**(不是摆着好看):
把 `Wallpaper.ts` 那段碑文抹掉 ⇒ **红**;还原 ⇒ **绿**。
顺带核了它的**指向**:碑文现在的主要落点是 `Appearance.ts`(函数原来所在处),
而判据的正则锚的是 `Wallpaper.ts` —— 两处都有内容才过,我保留了 `Wallpaper.ts` 里的引用
(它说明"这里的 px 不是材质档"),所以判据成立。

## 四、未做

- 到期闸门那 **7 条**(pi 更正过我:`STATIC_ONLY` 是 7 不是 8,我上封记串了)**仍然没动**。
- `PROBE_DEVICE=none` 下**剩 5 条红**,都是**别的会话**新加判据但没更新登记数
  (`narrow-layout` 88>64、`nav-merge` 9>8、`harmony-presets` 6>5、`commit-hygiene` 3>2)
  加 `build-stamp`(`dist` 没重构建,与本次改动无因果)。**我没有替他们改**。
2026-09-15 12:22:23 +08:00

341 lines
14 KiB
TypeScript
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.

/*
* 壁纸的**画法**:预设档(渐变)与图片档 —— 纯逻辑,无 UI 依赖。
*
* 为什么单独一层:服务端存的只是 `bg_kind` + `bg_preset_id` + 参数,
* 「这个 id 该画成什么样」是客户端的事。这层决定"画哪些层、什么色、什么位置",
* 页面只负责把层交给系统原语(`radialGradient` / `linearGradient` / `Canvas`)。
*
* ⚠️ 这一层是**能力对等**的关键:pi 指出(2026-09-14)WebUI 的背景有 6 个预设渐变,
* 若鸿蒙只认 `image` 与 `none`,那"换账号后外观跟随"对预设档就是**不成立的** ——
* 用户设了预设,在鸿蒙看到的是没有背景。这是信息对等缺口,比"能不能上传壁纸"更基础。
*
* 参考实现:`client/electron/src/index.css` 的 `.bg-preset-*` 与
* `stores/backgroundStore.ts` 的 `PRESETS`(id / 标签 / 归一化规则)。
* 色值取自那边的调色板变量(浅色档),**逐一对照**,不是凭印象写的。
*
* ⚠️ 类型可擦除(无 enum / namespace / 构造器参数属性),判据用 node strip-types 直接跑它。
*/
/**
* 一层渐变(预设由 1~3 层叠出来,与 WebUI 的多重 background-image 一一对应)。
*
* `colors` 与 `stops` 分开存(而不是 `[色, 位置][]`):ArkTS 对嵌套元组数组支持有限,
* 而分开存更好判。页面按 `stops[i]` 给 `colors[i]` 组对。
*/
export class PresetLayer {
/** 'radial' | 'linear' | 'grid' */
kind: string = 'radial';
/** radial:中心点百分比(0~100);linear/grid 不用 */
cx: number = 50;
cy: number = 50;
/** radial:半径(交给系统,用百分比或 vp);linear:角度(度) */
radius: string = '55%';
angle: number = 180;
colors: string[] = [];
/** 色标位置(0~100),与 `colors` 一一对应 */
stops: number[] = [];
/** grid 用:线色 / 透明度 / 间隔 */
lineColor: string = '';
lineAlpha: number = 0;
step: number = 0;
}
/**
* 预设色板 —— **两套**(浅色 / 深色),值逐个取自 WebUI `index.css` 的调色板变量。
*
* 这些色不走系统语义色:它们是这套预设的设计内容(与品牌蓝同类),
* 系统里没有"极光渐变"这种东西。
*
* ⚠️ **为什么必须两套**(pi 2026-09-14 指出,这一条不是"观感未验"而是机制上确定不同):
* WebUI 的 `.bg-preset-*` 写的是 `rgb(var(--c-gray-200) / 0.55)`、`rgb(var(--c-blue-100))`,
* 而 `--c-*` 在 `.dark` 里**整体换了一套**(blue-100 → `30 43 67`)——
* 也就是说 WebUI 的预设**自动随主题变**。若这边只有浅色那套,
* 深色主题下就会是"浅色渐变垫在深色系统表面之下",正是这一整轮在治的那种病。
* 它不需要真机就能发现:机制写在代码里。
*
* 判据把两套值与 `index.css` 的 `:root` 段、`.dark` 段**逐个比对**(和抓 `#BFDCFE` 同一个手法)。
*/
const LIGHT_GRAY_100: string = '#F3F4F6'; // --c-gray-100
const LIGHT_GRAY_200: string = '#EAECF1'; // --c-gray-200
const LIGHT_BLUE_100: string = '#DBEAFE'; // --c-blue-100
const LIGHT_BLUE_200: string = '#BFDBFE'; // --c-blue-200(191 219 254)
const LIGHT_GREEN_100: string = '#DCFCE7'; // --c-green-100
const LIGHT_AMBER_100: string = '#FEF3C7'; // --c-amber-100
const LIGHT_ORANGE_100: string = '#FFEDD5'; // --c-orange-100
const DARK_GRAY_100: string = '#20242C'; // --c-gray-100(.dark:32 36 44)
const DARK_GRAY_200: string = '#272B34'; // --c-gray-200(.dark:39 43 52)
const DARK_BLUE_100: string = '#1E2B43'; // --c-blue-100(.dark:30 43 67)
const DARK_BLUE_200: string = '#203454'; // --c-blue-200(.dark:32 52 84)
const DARK_GREEN_100: string = '#1A362B'; // --c-green-100(.dark:26 54 43)
const DARK_AMBER_100: string = '#3B301D'; // --c-amber-100(.dark:59 48 29)
const DARK_ORANGE_100: string = '#3C291F'; // --c-orange-100(.dark:60 41 31)
/** 一套色板(浅或深)。做成类而不是散着的常量:选哪套是**一处**决定。 */
export class Palette {
dark: boolean = false;
gray100: string = LIGHT_GRAY_100;
gray200: string = LIGHT_GRAY_200;
blue100: string = LIGHT_BLUE_100;
blue200: string = LIGHT_BLUE_200;
green100: string = LIGHT_GREEN_100;
amber100: string = LIGHT_AMBER_100;
orange100: string = LIGHT_ORANGE_100;
}
/** 当前该用哪套色板 */
export function paletteFor(dark: boolean): Palette {
const p: Palette = new Palette();
p.dark = dark;
if (dark) {
p.gray100 = DARK_GRAY_100;
p.gray200 = DARK_GRAY_200;
p.blue100 = DARK_BLUE_100;
p.blue200 = DARK_BLUE_200;
p.green100 = DARK_GREEN_100;
p.amber100 = DARK_AMBER_100;
p.orange100 = DARK_ORANGE_100;
}
return p;
}
/**
* `transparent` 用**关键字**而不是 8 位半透明色值:
* 一是系统认识它,二是本仓有一条判据禁止 8 位色值(那类色值曾经是手写玻璃的证据)。
* 页面把它映射成 `Color.Transparent`。
*/
export const TRANSPARENT: string = 'transparent';
/** 预设清单:id 与顺序与 WebUI 的 `PRESETS` 逐字一致(判据会去比对) */
export const PRESET_IDS: string[] = ['aurora', 'dusk', 'mint', 'sand', 'ink', 'mesh'];
export function presetLabel(id: string): string {
if (id === 'aurora') {
return '极光';
}
if (id === 'dusk') {
return '暮色';
}
if (id === 'mint') {
return '薄荷';
}
if (id === 'sand') {
return '沙丘';
}
if (id === 'ink') {
return '墨色';
}
if (id === 'mesh') {
return '网格';
}
return '';
}
const DEFAULT_PRESET: string = 'aurora';
/** 认不出的 preset id → 回到默认(与 WebUI 的 `normalize` 同一规则,避免"空白背景") */
export function normalizePreset(id: string): string {
return PRESET_IDS.indexOf(id) >= 0 ? id : DEFAULT_PRESET;
}
function radial(cx: number, cy: number, radius: string, color: string, stop: number): PresetLayer {
const l: PresetLayer = new PresetLayer();
l.kind = 'radial';
l.cx = cx;
l.cy = cy;
l.radius = radius;
l.colors = [color, TRANSPARENT];
l.stops = [0, stop];
return l;
}
function linear(angle: number, from: string, to: string): PresetLayer {
const l: PresetLayer = new PresetLayer();
l.kind = 'linear';
l.angle = angle;
l.colors = [from, to];
l.stops = [0, 100];
return l;
}
/**
* 某个预设由哪些层叠出来(与 `.bg-preset-*` 的多重背景一一对应)。
*
* 层序 = 绘制顺序:先画的在下层。WebUI 的 CSS 里第一段是最上层(CSS 多重背景
* 是后面的在下),为保持观感一致,这里**反过来给**:CSS 里最后一段先画。
*/
export function layersFor(id: string, dark: boolean): PresetLayer[] {
const key: string = normalizePreset(id);
const pal: Palette = paletteFor(dark);
if (key === 'dusk') {
return [
radial(85, 25, '55%', pal.blue200, 55),
radial(12, 80, '55%', pal.orange100, 55)
];
}
if (key === 'mint') {
return [
radial(78, 70, '55%', pal.blue100, 55),
radial(25, 30, '55%', pal.green100, 55)
];
}
if (key === 'sand') {
return [
radial(80, 75, '55%', pal.orange100, 55),
radial(20, 25, '60%', pal.amber100, 60)
];
}
if (key === 'ink') {
return [
linear(160, pal.gray100, pal.gray200),
radial(30, 20, '60%', pal.gray200, 60)
];
}
if (key === 'mesh') {
const g: PresetLayer = new PresetLayer();
g.kind = 'grid';
/*
* 网格:CSS 用的是 `repeating-linear-gradient`,**系统没有对应的渐变原语**,
* 所以这一档用系统 `Canvas` 画线(仍是系统绘制能力,不是手写模糊那一类)。
* 线色与间隔照抄 CSS:gray-200 / 0.55 / 28px。
*/
g.lineColor = pal.gray200;
g.lineAlpha = 0.55;
g.step = 28;
const base: PresetLayer = linear(180, pal.gray100, pal.gray100);
return [base, g];
}
// aurora(默认)
return [
radial(68, 82, '55%', pal.blue100, 55),
radial(82, 12, '50%', pal.green100, 50),
radial(18, 22, '55%', pal.blue200, 55)
];
}
/** 背景计划:页面照着画,判据照着判 */
export class BackgroundPlan {
/** 'none' | 'preset' | 'image' */
kind: string = 'none';
/** 用的是深色那套色板吗(跟着主题走,不是"未验") */
dark: boolean = false;
presetId: string = '';
/**
* **静默兜底的痕迹**(pi 2026-09-14 裁定):服务端/WebUI 先加了新档、而这台设备是旧构建时,
* `normalizePreset` 会把认不出的 id **静默换成 aurora** —— 用户以为选的是新档。
* 这条缺省语义已登记在 `CRITERIA.md` §10,但登记只防"被误报成 bug",
* **防不住"没人知道它正在发生"**(真实环境里不会留下任何痕迹)。
*
* 所以把"发生过替换"记在这里:换过就是**原来那个 id**(非空),没换过就是空串。
* 页面拿到非空值时**打一行日志**(`hilog`/`console.info` 皆可)——
* 于是这个后果从"可能发生"变成"**可数**":出问题时能立刻确认是版本偏移,而不是猜。
*
* ⚠️ 为什么痕迹记在**返回值**里、而不在这一层直接打日志:这一层是**纯逻辑**
* (无 `@ohos` 依赖 ⇒ 判据能用 node `--experimental-strip-types` 直接跑它,
* 见 `harmony-presets.test.mjs`)。为了打一行日志而引入 `@ohos.hilog`,
* 会把"能真跑的行为判据"换成"只能读源码的形态判据" —— 那是不划算的交易。
* 记事实在这一层(可判),打日志在页面层(可观测)。
*/
presetSubstitutedFrom: string = '';
layers: PresetLayer[] = [];
/**
* 模糊强度:**服务端给的 px 原值**(0~40),页面拿它做 `.blur(px)` —— **像素半径**。
*
* ★ 只**搬运**,不在这里分档:这一层是**纯逻辑**(零 `@ohos` 依赖),
* 分档会变成第二张表,而两张表迟早分叉。**这里也再没有"档"这回事了** ——
* 曾经的 `px → 材质档` 映射函数(`Appearance.ts` 的 `blurStyleFor`)已随
* "导航条改回固定档"一起删除(2026-09-15):它没有、也不该有消费者。
* ⇒ 想加"档位"请先读 `Appearance.ts` 那段碑文,那里写着为什么不该加回来。
*
* ★ 为什么一度"只写不读":服务端那个模糊字段曾经**没有任何调用点**(服务端存、
* 两端同步,页面却没人用)
* (`harmony-appearance.test.mjs` 有一条判据把"消费侧出现次数为 0"钉住,
* 就是为了让"有人开始消费"这一刻**必须停下来**补判据而不是偷偷把 0 改成 1)。
* P4c 加背景选择器时踩到了这条线:滑杆能拖、值能存,但壁纸**一点没糊**。
* 所以这次把**消费点**补上了(`.blur(this.bgPlan.blurPx)`)。
*/
blurPx: number = 0;
/**
* 遮盖浓度(0~1):**两档都用**(预设档也压),用系统遮罩色刷一层。
*
* ⚠️ 这里原来写的是"preset 档不用",pi 2026-09-14 读 WebUI 源码后指出**两边不一致**:
* WebUI 的 `applyBackground()` **无条件**写 `--bg-dim`(默认 12 —— 与服务端
* `DefaultAppearance()` 一致;这里原写 24,是因为 WebUI 当时自写了一套 24/8,
* 2026-09-14 已统一到服务端契约),
* `.app-backdrop::after` 是 `rgb(var(--bg-scrim) / var(--bg-dim))` ——
* 遮罩**不区分档位**,预设档一样被压。它的注释写着目的:
* **「背景越花,正文越需要一层遮罩才读得动」** —— 遮罩服务的是可读性,不是装饰。
*
* 对这边的实际后果:我们已经让出了页面底(`bgActive` 一路到三个 pane),
* 于是正文直接压在原色渐变上 —— 比 WebUI 更艳更亮、更不好读。
*/
scrim: number = 0;
}
/**
* 快照 + "有没有图" → 画什么。
*
* 与 `mergeAppearance` 的分工:那边决定**用谁的值**(服务端 vs 本地),
* 这里决定**画成什么**。`image` 档但图没取回来 → `none`:
* 宁可什么都不画,也不要画一块空白(用户会以为壁纸坏了)。
*/
export function resolveBackground(bgKind: string, presetId: string, scrim: number, hasImage: boolean, dark: boolean, blurPx: number = 0): BackgroundPlan {
const plan: BackgroundPlan = new BackgroundPlan();
plan.dark = dark;
// 模糊是**全档通用**的(WebUI 的 `--bg-blur` 也不区分档位),所以先搬运、各档都带上
plan.blurPx = normalizeBlur(blurPx);
if (bgKind === 'preset') {
plan.kind = 'preset';
plan.presetId = normalizePreset(presetId);
// 留痕:认不出的 id 被静默替换时,把**原来那个 id**带出去(页面据此打一行日志)
plan.presetSubstitutedFrom = plan.presetId === presetId ? '' : presetId;
plan.layers = layersFor(plan.presetId, dark);
// 预设档**也**套遮罩:与 WebUI 的 `--bg-dim` 一致(它不区分档位),服务的是可读性
plan.scrim = scrim;
return plan;
}
if (bgKind === 'image' && hasImage) {
plan.kind = 'image';
plan.scrim = scrim;
return plan;
}
plan.kind = 'none';
return plan;
}
/**
* 模糊 px 归一:0~40 的整数。
*
* ★ 边界 40 与 `model/Appearance.ts` 的 `clampNumber(bgBlur, 0, 40, 4)` **必须同值** ——
* 两边不一致的话,服务端存 40、这边按 80 画,判定与显示就对不上了
* (曾经 `Appearance.ts` 的 `blurStyleFor` 里也有一道 clamp —— 它已删除;
* 现在这条 clamp 就是**唯一**一道,所以更不能松)。
*/
export function normalizeBlur(px: number): number {
if (!Number.isFinite(px)) {
return 0;
}
const r: number = Math.round(px);
/*
* ★ 判 `!(r > 0)` 而不是 `r < 0` —— 因为 `Math.round(-0.4)` 是 **-0**,
* 而 `-0 < 0` 是 **false**(`-0 === 0`)。用 `r < 0` 会让 `-0` 漏过去,
* 于是 `blur(-0)` 被喂进 ArkUI —— 这是个**静默**的怪值(判据把它抓出来了:
* `assert.equal(normalizeBlur(-0.4), 0)` 报的是 `+ -0`,一眼看不出问题在哪)。
*/
if (!(r > 0)) {
return 0;
}
if (r > MAX_BLUR_PX) {
return MAX_BLUR_PX;
}
return r;
}
/** 模糊上限(px)。与 `model/Appearance.ts` 的 clamp 上界同值。 */
export const MAX_BLUR_PX: number = 40;
/** 预设缩略图/选择器要显示的清单(id + 标签) */
export function presetChoices(): string[] {
return PRESET_IDS;
}