diff --git a/client/electron/src/components/ContactPanel.tsx b/client/electron/src/components/ContactPanel.tsx index ef8d42d..48145de 100644 --- a/client/electron/src/components/ContactPanel.tsx +++ b/client/electron/src/components/ContactPanel.tsx @@ -226,7 +226,7 @@ function ContactRow({ return (
diff --git a/client/electron/test/harmony-appearance.test.mjs b/client/electron/test/harmony-appearance.test.mjs index 83daa62..009739c 100644 --- a/client/electron/test/harmony-appearance.test.mjs +++ b/client/electron/test/harmony-appearance.test.mjs @@ -245,3 +245,181 @@ test('判据自检:把「服务端没记录」判成覆盖本地,必须判 }); export const __coverage = ['snapshotFromResponse', 'payloadFromLocal', 'mergeAppearance', 'localOnly', 'blurStyleFor', 'colorModeFor', 'scrimOpacity', 'statusLabel']; + +// ───────────────────── P4b:预设档的画法(能力对等,pi 指出的信息对等缺口) ───────────────────── + +const WALL_TS = join(HARMONY_ETS, 'model/Wallpaper.ts'); +const W = await import(pathToFileURL(WALL_TS).href); +const webCss = readFileSync(join(ROOT, 'client/electron/src/index.css'), 'utf8'); +const webBgStore = readFileSync(join(ROOT, 'client/electron/src/stores/backgroundStore.ts'), 'utf8'); + +test('★ 预设清单与 WebUI 一致(id + 顺序 + 中文标签)—— 少一个档就是"用户设了在鸿蒙看不到"', () => { + /* + * pi 的原话(2026-09-14):WebUI 的背景有预设渐变,若鸿蒙只认 image/none, + * 那"换账号后外观跟随"对预设档就是**不成立**的 —— 用户设了预设, + * 在鸿蒙看到的是没有背景。这是**信息对等**缺口,比"能不能上传壁纸"更基础。 + * 所以判据从 WebUI 的源码里抽 id 与标签来比,不在这边再抄一遍。 + */ + const block = webBgStore.slice(webBgStore.indexOf('export const PRESETS'), webBgStore.indexOf('];', webBgStore.indexOf('export const PRESETS'))); + const webIds = [...block.matchAll(/id:\s*'([a-z]+)'/g)].map(m => m[1]); + const webLabels = [...block.matchAll(/label:\s*'([^']+)'/g)].map(m => m[1]); + assert.ok(webIds.length >= 4, `WebUI 的 PRESETS 要能抽出 id,实际 ${webIds.length} 个`); + assert.deepEqual(W.PRESET_IDS, webIds, '预设 id 与顺序必须与 WebUI 一致'); + assert.deepEqual(W.PRESET_IDS.map(W.presetLabel), webLabels, '预设标签必须与 WebUI 一致(用户看到的就是这两个字)'); + // 每一个预设都要真能画出层来("有 id 但画不出东西"就是这个缺口的原始形态) + for (const id of W.PRESET_IDS) { + const layers = W.layersFor(id); + assert.ok(layers.length >= 1, `预设 ${id} 至少要有一层`); + assert.ok(layers.every(l => l.colors.length > 0 || l.kind === 'grid'), `预设 ${id} 的层要有色标`); + assert.ok(layers.some(l => l.kind === 'grid' || l.colors.length >= 2), `预设 ${id} 要能画出渐变`); + } +}); + +test('★ 预设色值与 WebUI 的调色板变量逐个对照(不是凭印象写的)', () => { + /* + * 这类"看起来差不多"的色值是跨端最容易悄悄分叉的东西:两边各写一遍十六进制, + * 谁也说不清哪个是当前的。所以从 CSS 的调色板变量里读出 RGB,再比到这边写死的色值。 + */ + const cssVar = (name) => { + const m = new RegExp(`${name}:\\s*(\\d+)\\s+(\\d+)\\s+(\\d+);`).exec(webCss); + assert.ok(m, `CSS 里要有 ${name}`); + return '#' + [m[1], m[2], m[3]] + .map(n => Number(n).toString(16).padStart(2, '0').toUpperCase()) + .join(''); + }; + const used = new Set(); + for (const id of W.PRESET_IDS) { + for (const l of W.layersFor(id)) { + for (const c of l.colors) if (c !== W.TRANSPARENT) used.add(c); + if (l.lineColor) used.add(l.lineColor); + } + } + const palette = { + '#F3F4F6': cssVar('--c-gray-100'), + '#EAECF1': cssVar('--c-gray-200'), + '#DBEAFE': cssVar('--c-blue-100'), + '#BFDBFE': cssVar('--c-blue-200'), + '#DCFCE7': cssVar('--c-green-100'), + '#FEF3C7': cssVar('--c-amber-100'), + '#FFEDD5': cssVar('--c-orange-100') + }; + for (const c of used) { + assert.ok(palette[c] !== undefined, `预设里出现了不在登记色板内的色:${c}`); + assert.equal(c, palette[c], `预设色 ${c} 与 WebUI 调色板不一致(CSS 里是 ${palette[c]})`); + } + // 反向:登记色板里的每个色都要真被用上(登记了不用 = 名单在过期) + for (const c of Object.keys(palette)) { + assert.ok(used.has(c), `色板里 ${c} 没有被任何预设使用(色板过期了)`); + } + // 透明必须用关键字而不是 8 位色值(8 位色值在本仓是"手写玻璃"的证据,另有一条判据禁) + for (const id of W.PRESET_IDS) { + for (const l of W.layersFor(id)) { + for (const c of l.colors) { + assert.ok(!/^#[0-9A-Fa-f]{8}$/.test(c), `预设里的透明要用 Color.Transparent 语义(${c} 是 8 位色值)`); + } + } + } + // 层数/层序有据可依:每个预设的层数与 CSS 里的渐变段数对应 + for (const id of W.PRESET_IDS) { + const cssBlock = webCss.slice(webCss.indexOf(`.bg-preset-${id}`)); + const cssBody = cssBlock.slice(0, cssBlock.indexOf('}')); + const cssSegments = (cssBody.match(/(radial|linear)-gradient/g) || []).length; + const mine = W.layersFor(id).filter(l => l.kind !== 'grid').length + (W.layersFor(id).some(l => l.kind === 'grid') ? 1 : 0); + assert.ok(cssSegments >= 1, `CSS 里 ${id} 要有渐变段`); + assert.equal(mine >= cssSegments, true, `${id}:CSS 有 ${cssSegments} 段,鸿蒙只画了 ${mine} 层`); + } +}); + +test('画什么:none / preset / image 三档,图没取回来不许画空白', () => { + // 认不出的 preset → 默认档(与 WebUI 的 normalize 同一规则),不是"没有背景" + assert.equal(W.normalizePreset('不认识'), 'aurora'); + assert.equal(W.normalizePreset(''), 'aurora'); + assert.equal(W.layersFor('不认识').length, W.layersFor('aurora').length, '认不出要走默认档的画法'); + + const none = W.resolveBackground('none', 'aurora', 0.2, false); + assert.equal(none.kind, 'none'); + assert.deepEqual(none.layers, [], 'none 档不该画任何层'); + + const preset = W.resolveBackground('preset', 'mint', 0.2, false); + assert.equal(preset.kind, 'preset'); + assert.ok(preset.layers.length >= 1, 'preset 档要真的画出层来(这就是那个缺口的判据)'); + assert.equal(preset.scrim, 0, 'preset 档不压暗(压暗是给图片用的)'); + + const img = W.resolveBackground('image', 'aurora', 0.24, true); + assert.equal(img.kind, 'image'); + assert.equal(img.scrim, 0.24, '压暗浓度要传给画的那一层'); + + const imgMissing = W.resolveBackground('image', 'aurora', 0.24, false); + assert.equal(imgMissing.kind, 'none', 'image 档但图没取回来 → 什么都不画(画一块空白会被当成"壁纸坏了")'); +}); + +test('★ 页面真的把背景画出来了(这一条是补漏:P4 第一版只取回了图,没有任何东西去画)', () => { + /* + * P4 第一版的实际状态:`AppearanceStore` 取回了 `PixelMap`、算好了快照, + * 但**没有任何组件去画它** —— 也就是说壁纸只有数据没有画面。 + * 当时的提交信息没写错("取回 PixelMap"),但文档里把它列成"未验渲染", + * 听着像已经画出来了 —— 那是我说得比证据强。这条判据盯的就是这一层不许再缺。 + */ + const main = read('pages/MainPage.ets'); + assert.match(main, /WallpaperLayer\(\)/, '主界面要真的铺一层背景'); + assert.match(main, /resolveBackground\(/, '画什么由纯逻辑决定(不是页面里现编)'); + assert.match(main, /radialGradient\(\{/, '预设档用系统径向渐变'); + assert.match(main, /linearGradient\(\{/, '预设档用系统线性渐变'); + assert.match(main, /Canvas\(this\.gridCtx\)/, '网格档用系统 Canvas 画线(没有对应的系统渐变原语)'); + assert.match(main, /Image\(this\.wallpaperImage\)/, '图片档要真的把图渲染出来'); + assert.match(main, /\.objectFit\(ImageFit\.Cover\)/, '图片要铺满(不是拉伸变形或留白)'); + // 压暗用系统遮罩色 + 服务端浓度 + assert.match(main, /\.backgroundColor\(Theme\.overlay\)[\s\S]{0,80}?\.opacity\(this\.bgPlan\.scrim\)/, '压暗层要用系统遮罩色与算出来的浓度'); + // 背景在主界面这一层:Tabs 之上不该再有不透明的底色把背景盖死 + assert.match(main, /Stack\(\) \{[\s\S]{0,120}?this\.WallpaperLayer\(\)/, '背景要铺在内容之下(Stack 的底层)'); +}); + +test('★ 模糊归属:壁纸层**不许**再模糊,导航条必须有系统材质(互斥形式,pi 修正后的口径)', () => { + /* + * pi 撤回了他原来那句"模糊只由壁纸层负责":那是 WebUI 的架构结论 + * (它的壁纸图层自带 `filter: blur()`,浮在它上面的面再 backdrop-filter 就是把 + * 同一张糊过的底糊第二遍),不是通用规则。正确的形式是两条: + * ① 同一张底只许被模糊**一次**; + * ② 模糊该出现在"背后是**可变内容**"的层(导航条背后是滚动内容,壁纸层背后什么都没有)。 + * 于是判据写成互斥/分工,而不是"归谁"。 + */ + const main = read('pages/MainPage.ets'); + /* + * 取"壁纸层 builder 的正文"要**按行**截:用 `indexOf('build() {')` 两头夹, + * 要么撞上文件里更早的那个 build()(切片成空串),要么一路跨到后面的 + * TabBarBuilder(那里**正当地**有 `.backgroundBlurStyle`)——于是判据会误报。 + * 这是我自己的切片毛病,和 pi 指出 §三 那条是同一类。 + */ + const mainLines = main.split('\n'); + const start = mainLines.findIndex(l => l.includes('WallpaperLayer() {')); + assert.ok(start > 0, '要能找到壁纸层的 builder'); + let stop = mainLines.length; + for (let i = start + 1; i < mainLines.length; i++) { + if (/^ (@Builder|build\()/.test(mainLines[i])) { stop = i; break; } + } + const wallpaperBuilder = mainLines.slice(start, stop).join('\n'); + assert.ok(!/TabBarBuilder/.test(wallpaperBuilder), '自检:切片不该跨到别的成员上去'); + assert.ok(wallpaperBuilder.length > 200, '要能取到壁纸层的 builder 正文'); + assert.ok(!/backgroundBlurStyle/.test(wallpaperBuilder), '壁纸层不许再用材质(同一张底糊两遍 = 更脏更掉帧)'); + assert.ok(!/blur\(/i.test(wallpaperBuilder), '壁纸层不许出现任何模糊调用'); + // 导航条那一次模糊仍然在(背后是会滚动的内容,遮蔽有意义) + assert.match(main, /\.backgroundBlurStyle\(Theme\.navMaterial\)/, '导航条必须有系统材质'); + // 材料档次由用户偏好映射而来(不是写死的半径) + const store = read('common/AppearanceStore.ets'); + assert.match(store, /colorModeValue\(theme\)/, '主题走系统色彩模式'); + /* + * "理由写清了没"要读**原文**(`read()` 会剥注释 —— 而理由就在注释里)。 + * 剥注释读源码是为了防"注释里的调用被当成真调用",但断言"注释里写了理由"时正好相反。 + */ + const wallRaw = readFileSync(join(HARMONY_ETS, 'model/Wallpaper.ts'), 'utf8'); + assert.match(wallRaw, /系统没有对应的渐变原语/, '网格档为什么用 Canvas 要写清(否则以后会被当成绕开系统方案)'); + assert.match(wallRaw, /repeating-linear-gradient/, '要指名道姓写出 CSS 用的是哪个原语(后人查得到)'); +}); + +test('判据自检:预设清单少一档必须判红', () => { + // 自检:把 id 列表裁掉一个,确认"与 WebUI 一致"那条会红 + const block = webBgStore.slice(webBgStore.indexOf('export const PRESETS'), webBgStore.indexOf('];', webBgStore.indexOf('export const PRESETS'))); + const webIds = [...block.matchAll(/id:\s*'([a-z]+)'/g)].map(m => m[1]); + const trimmed = webIds.slice(0, -1); + assert.notDeepEqual(trimmed, W.PRESET_IDS, '自检:裁掉一档后必须与实现不一致(否则这条判据没有分辨力)'); +}); diff --git a/client/harmony/entry/src/main/ets/model/Wallpaper.ts b/client/harmony/entry/src/main/ets/model/Wallpaper.ts index 2438e6e..d2dce75 100644 --- a/client/harmony/entry/src/main/ets/model/Wallpaper.ts +++ b/client/harmony/entry/src/main/ets/model/Wallpaper.ts @@ -49,7 +49,7 @@ export class PresetLayer { const GRAY_100: string = '#F3F4F6'; // --c-gray-100 const GRAY_200: string = '#EAECF1'; // --c-gray-200 const BLUE_100: string = '#DBEAFE'; // --c-blue-100 -const BLUE_200: string = '#BFDCFE'; // --c-blue-200 +const BLUE_200: string = '#BFDBFE'; // --c-blue-200(191 219 254) const GREEN_100: string = '#DCFCE7'; // --c-green-100 const AMBER_100: string = '#FEF3C7'; // --c-amber-100 const ORANGE_100: string = '#FFEDD5'; // --c-orange-100