diff --git a/client/electron/test/align-refs.test.mjs b/client/electron/test/align-refs.test.mjs index dbd2b0f..8f6a5f8 100644 --- a/client/electron/test/align-refs.test.mjs +++ b/client/electron/test/align-refs.test.mjs @@ -64,3 +64,24 @@ test('★ 圆角走令牌:鸿蒙侧必须有 radius 令牌,且每一条语 assert.ok(r.judgement && r.judgement.includes('语义'), `${r.semantic} 必须写明按语义对齐还是按数值对齐(否则下一个人会去比数字)`); } }); + +/* +★ 包名必须与 AGC 下发的配置**完全一致**(pi 2026-09-14): +不一致时 Push Kit 推不到设备 —— 这是**功能性约束**,不是风格问题。 +AGC 实测拒绝 `com.agentmail.harmony`(`harmony` 是包名保留字),用户定为 `com.jianf.agentmail`。 +这条把"两处必须一致"变成可判的,免得改一处忘另一处(那种错只有在真机上表现为"收不到推送")。 +*/ +test('★ bundleName 必须与 agconnect-services.json 的 package_name 一致(否则推送送不到)', () => { + const reg = JSON.parse(prose(join(ROOT, 'docs', 'ALIGN-REFS.json'))); + const app = prose(join(ROOT, 'client', 'harmony', 'AppScope', 'app.json5')); + const m = /"bundleName"\s*:\s*"([^"]+)"/.exec(app); + assert.ok(m, '要能从 AppScope/app.json5 取到 bundleName'); + const bundle = m[1]; + assert.ok(reg.agc && reg.agc.packageName, 'ALIGN-REFS.json 里要登记 AGC 的 package_name'); + assert.equal(bundle, reg.agc.packageName, + `bundleName(${bundle})与 AGC 的 package_name(${reg.agc.packageName})不一致 —— ` + + `**正确修法**:改 AppScope/app.json5 让它与 AGC 一致(或按新包名在 AGC 重建应用)。` + + `**最常见的错误修法**:只改这一处断言里的期望值 —— 那会让"收不到推送"变成一个测不出来的状态。`); + assert.ok(!/harmony/i.test(bundle), + `bundleName 里带 harmony 是 AGC 保留字(实测被拒):${bundle}`); +}); diff --git a/client/electron/test/cross-client-theme.test.mjs b/client/electron/test/cross-client-theme.test.mjs index be03ed3..547f10a 100644 --- a/client/electron/test/cross-client-theme.test.mjs +++ b/client/electron/test/cross-client-theme.test.mjs @@ -419,8 +419,7 @@ test('C|玻璃:位置用系统材质、不许叠、每一处都要登记( // 导航条那一处必须真的还在(形状判定之外,位置本身也要在) const main = code(join(ROOT, 'client/harmony/entry/src/main/ets/pages/MainPage.ets')); - assert.match(main, /\.backgroundBlurStyle\(BLUR_STYLE_OF\[blurStyleFor\(this\.bgPlan\.blurPx\)\] \?\? BlurStyle\.NONE\)/, - '导航条要用系统材质(不是手写 alpha),且档位由用户偏好经 blurStyleFor 映射而来'); + assert.match(main, /\.backgroundBlurStyle\(NAV_MATERIAL_OF\[navMaterialFor\(this\.bgPlan\.blurPx\)\] \?\? Theme\.navMaterial\)/, '导航条要用系统材质(不是手写 alpha)'); /* * 自检:造一次**链式叠用**(`X.blur().blur()`),确认上面的判定抓得到。 @@ -428,8 +427,8 @@ test('C|玻璃:位置用系统材质、不许叠、每一处都要登记( * 第一版只看"前一个字符是不是 }",对这种写法**静默失效**,正是这条自检抓出来的。 */ // 样本要与**真实写法同形**(否则自检会变成"拿一段判据认不出来的代码去验判据") - const sample = 'Row() { Text("x") }\n .backgroundBlurStyle(BLUR_STYLE_OF[blurStyleFor(x)] ?? BlurStyle.NONE)\n' + - ' .backgroundBlurStyle(BLUR_STYLE_OF[blurStyleFor(x)] ?? BlurStyle.NONE)'; + const sample = 'Row() { Text("x") }\n .backgroundBlurStyle(NAV_MATERIAL_OF[navMaterialFor(x)] ?? Theme.navMaterial)\n' + + ' .backgroundBlurStyle(NAV_MATERIAL_OF[navMaterialFor(x)] ?? Theme.navMaterial)'; const sampleEnds = [...sample.matchAll(/backgroundBlurStyle\(/g)].map(m => blockEndBefore(sample, m.index)); assert.ok(sampleEnds.every(e => e >= 0), '自检:链式写法要能解析出所作用的块'); assert.equal(new Set(sampleEnds).size, 1, '自检:同一组件的两处调用必须解析到同一个块(否则叠用判不出来)'); @@ -536,6 +535,73 @@ test('遮罩:交给系统的遮罩语义色("随主题换向"这件事现在 .map(f => f.slice(etsRoot.length + 1)); assert.ok(overlayUsers.length > 0, 'Theme.overlay 声明了却没有任何使用点 —— 那就是个死令牌(要么删掉,要么写出它的使用处)'); + /* + * ★ pi 2026-09-15 的第二刀:上面这条**只覆盖了 `overlay` 一个令牌**。 + * 我当时把 `MainPage` 里的 `Theme.navMaterial` 换成了另一个表达式, + * `navMaterial` 就**再没有任何使用点**了 —— 而它上面那条判据 + * ("declaration 存在且不是 NONE")**照样绿**:它守的是声明, + * 坏的是那条活的调用路径。**判据名替实现作证**,我们这一路反复在消的形状。 + * ⇒ 把这条规则**铺到 Theme 的每一个令牌**上(同一个文件里早就写着正确的形状, + * 只是覆盖面只有一处)。令牌只在自己的文件里被别的方法读**不算**(那是内部实现细节, + * 由那个方法自己的使用点担保)。 + */ + const themeSrc = code(themePath); + const declared = [...themeSrc.matchAll(/static readonly (\w+)\s*[:=]/g)].map(m => m[1]); + assert.ok(declared.length > 20, `要从 Theme.ets 里读到令牌清单(读到 ${declared.length} 个)`); + const others = collectEts(etsRoot).filter(f => f !== themePath); + const othersSrc = others.map(f => ({ f: f.slice(etsRoot.length + 1), src: code(f) })); + /* + * ⚠️ **量的是"外部引用数"**,这一点是踩出来的:第一版我数"任何引用", + * 结果是 `chipSpentBg`/`chipSpentFg` 被判死 —— 而它们**不是**死的:它们被 + * `Theme.budgetBg()` 返回,而 `budgetBg()` 在 `MainPage` 里用着 + * (`Theme.budgetBg(budgetState(...))`,那个 `budgetState` 本身有真值判据)。 + * 也就是说"只在 Theme 内部被别的方法读"**不算死** —— 那是内部实现细节, + * 由那个方法的**外部**使用点担保。 + * 但 `navMaterial` 必须仍然**被抓**:它当时唯一的消费者是我在 `MainPage` 里 + * 写的一张**局部只读表**(`BLUR_STYLE_OF`),而那张表可以整体删掉/改写 + * (我真删过一次)—— 它不提供任何"担保"。 + * ⇒ 区分这两者的唯一办法是**量外部引用数**:页面/组件里引用 0 次,就记一笔, + * 连同它在 Theme 内部被哪些方法读(供人判断"那个方法自己有没有人用")。 + */ + const deadTokens = []; + for (const name of declared) { + const re = new RegExp(`Theme\\.${name}\\b`); + const users = othersSrc.filter(o => re.test(o.src)).map(o => o.f); + if (users.length > 0) continue; + // 在 Theme.ets 内部找"读它的那个成员":看每个 `Theme.` 出现处**前面最近**的成员声明 + const internalUsers = []; + const re2 = new RegExp(`Theme\\.${name}\\b`, 'g'); + let m2; + while ((m2 = re2.exec(themeSrc)) !== null) { + const before = themeSrc.slice(0, m2.index); + const owners = [...before.matchAll(/static\s+(?:readonly\s+)?(\w+)/g)]; + if (owners.length === 0) continue; + const owner = owners[owners.length - 1][1]; + if (owner !== name && !internalUsers.includes(`Theme.${owner}`)) internalUsers.push(`Theme.${owner}`); + } + /* + * 只被 Theme 内部的方法读 **且那个方法自己在外部有调用点** ⇒ 不算死 + * (`chipSpentBg` ← `Theme.budgetBg()` ← `MainPage` 的 `Theme.budgetBg(budgetState(…))`)。 + * 否则仍然算死:这正是 `navMaterial` 的形状 —— 它当时那个"内部消费者"是 + * `MainPage` 里的一张**局部表**,不提供任何担保。 + */ + if (internalUsers.length > 0) { + const ownersAlive = internalUsers.some((u) => { + const methodName = u.replace('Theme.', '').replace('()', ''); + return othersSrc.some((o) => new RegExp(`Theme\\.${methodName}\\b`).test(o.src)); + }); + if (ownersAlive) continue; + deadTokens.push(`${name}(只在 ${internalUsers.join('、')} 内部被读,而那些方法**外部也没有调用点**)`); + continue; + } + deadTokens.push(`${name}(任何地方都没读)`); + } + assert.deepEqual(deadTokens, [], + `★ 这些 Theme 令牌在**页面/组件里一次都没被引用**:\n ${deadTokens.join('\n ')}\n` + + ' 要么删掉,要么写出它的使用处;若只是被 Theme 内部的方法读,确认那个方法自己还有外部调用点。\n' + + ' **这条要抓的形状**:把一根线接到别处,让某个令牌**意外变成孤儿** —— ' + + '`navMaterial` 就这么变成过孤儿(它当时唯一的消费者是 MainPage 里一张可以整体删掉的局部表),' + + '而只盯声明的判据("声明了且不是 NONE")**照样绿**。'); // 用它的必须是**自绘遮罩**的地方(系统自带遮罩的弹窗不需要它) assert.ok(overlayUsers.every(f => f.endsWith('.ets')), `遮罩使用点应该是页面:${overlayUsers.join('、')}`); assert.ok(!/#[0-9A-Fa-f]{8}/.test(stripComments(harmony)), '遮罩不该再写成色与透明度焊死的 #AARRGGBB 单值'); diff --git a/client/electron/test/debt-visibility.test.mjs b/client/electron/test/debt-visibility.test.mjs index 6e5fd7d..1e2dd23 100644 --- a/client/electron/test/debt-visibility.test.mjs +++ b/client/electron/test/debt-visibility.test.mjs @@ -32,7 +32,9 @@ const REGISTERED = new Map([ ['background.test.mjs', 3], // 两条反向断言的未覆盖路(route B / route C)+ 说明 ['harmony-appearance.test.mjs', 4], // bgBlur 消费侧/映射、运行期形态类边界 ['harmony-logic.test.mjs', 1], // `.ets` 状态机要跑起来才算数 - ['debt-visibility.test.mjs', 9] // 本文件:N 处是词表定义 + 报错文案(第 N+1 处即红) + ['debt-visibility.test.mjs', 9], // 本文件:N 处是词表定义 + 报错文案(第 N+1 处即红) + ['harmony-admin.test.mjs', 1], // 用户管理页:本机无设备 ⇒ 只能证明"代码里这么写" + ['harmony-imageprep.test.mjs', 4] // 图片上传:压图/选图/服务端收下,三样本机都验不了 ]); const HERE = dirname(fileURLToPath(import.meta.url)); diff --git a/client/electron/test/harmony-appearance.test.mjs b/client/electron/test/harmony-appearance.test.mjs index bb8dfa6..a8fc437 100644 --- a/client/electron/test/harmony-appearance.test.mjs +++ b/client/electron/test/harmony-appearance.test.mjs @@ -165,18 +165,24 @@ test('★ 模糊值映射到**系统材质档次**(不是把 40 当半径塞 * 这里判据自己去页面源码里把键取出来,逐个对着 SDK 的成员名核。 */ const main = read('pages/MainPage.ets'); - const tableAt = main.indexOf('const BLUR_STYLE_OF: Record = {'); - assert.ok(tableAt > 0, '页面里要有 `BLUR_STYLE_OF`(档位名 → SDK 枚举)那张表'); + const tableAt = main.indexOf('const NAV_MATERIAL_OF: Record = {'); + assert.ok(tableAt > 0, '页面里要有 `NAV_MATERIAL_OF`(档位名 → SDK 枚举)那张表'); const tableEnd = main.indexOf('};', tableAt); const table = main.slice(tableAt, tableEnd); const keys = [...table.matchAll(/'([A-Z_]+)':\s*BlurStyle\.([A-Z_]+)/g)]; - assert.ok(keys.length >= 4, `要从表里读到键(实际 ${keys.length} 项)`); + assert.ok(keys.length >= 3, `要从表里读到键(实际 ${keys.length} 项)`); for (const [, key, val] of keys) { assert.equal(key, val, `表项 '${key}': BlurStyle.${val} —— 键与值必须同名(不同名几乎必然是写错了)`); assert.ok(members.includes(key), `'${key}' 不是 SDK 的 BlurStyle 成员(写自造名字会编译不过或不生效)`); } - // 四档都要在(漏一档会让那个档位静默回落成 BlurStyle.NONE) - for (const tier of ['NONE', 'COMPONENT_THIN', 'COMPONENT_REGULAR', 'COMPONENT_THICK']) { + /* + * 导航条那张表**不许有 NONE**:`navMaterialFor` 有下限(永不返回 NONE), + * 表里留着 NONE 只会让人以为"导航条可以不糊"(那正是我上一笔的缺陷)。 + * 三档(薄/中/厚)都要在,漏一档会让该档静默回落。 + */ + assert.ok(!keys.some(([, k]) => k === 'NONE'), + '导航空的表里不该有 NONE —— `navMaterialFor` 有下限(见 harmony-nav 的可达性判据)'); + for (const tier of ['COMPONENT_THIN', 'COMPONENT_REGULAR', 'COMPONENT_THICK']) { assert.ok(keys.some(([, k]) => k === tier), `表里缺 ${tier} ⇒ 该档会静默回落成"不模糊"`); } }); @@ -476,8 +482,8 @@ test('★ 模糊归属:壁纸层**不许**再模糊,导航条必须有系统 const imgBlur = [...wallpaperBuilder.matchAll(/\.blur\(/g)]; assert.equal(imgBlur.length, 1, `壁纸层的图片内容模糊只许一次(实际 ${imgBlur.length} 次)`); // 导航条的**面板材质**仍然在(背后是会滚动的内容,遮蔽有意义),且档位来自用户偏好 - assert.match(main, /\.backgroundBlurStyle\(BLUR_STYLE_OF\[blurStyleFor\(this\.bgPlan\.blurPx\)\] \?\? BlurStyle\.NONE\)/, - '导航条的面板材质要由 `bg_blur` **映射**而来(计划文档 §7.12 要求的那个调用点)'); + assert.match(main, /\.backgroundBlurStyle\(NAV_MATERIAL_OF\[navMaterialFor\(this\.bgPlan\.blurPx\)\] \?\? Theme\.navMaterial\)/, + '导航条的面板材质用**固定系统档**(`Theme.navMaterial`)—— 不跟随 `bg_blur`'); // 材料档次由用户偏好映射而来(不是写死的半径) const store = read('common/AppearanceStore.ets'); assert.match(store, /colorModeValue\(theme\)/, '主题走系统色彩模式'); @@ -871,7 +877,7 @@ test('★ 模糊字段的消费侧:逐文件登记 + 计数(P4c 起不再是 * ⇒ 这是"消费点出现时按判据要求补判据"的正常流程走完一遍,不是把 0 改成 1 了事。 */ const plumbing = new Map([ - ['model/Appearance.ts', { max: 11, why: '域模型:声明 + clamp + 合并 + 映射函数 blurStyleFor(px → 材质档,见下一条判据)—— 搬运与映射,都不是消费' }], + ['model/Appearance.ts', { max: 12, why: '域模型:声明 + clamp + 合并 + 两个映射函数(blurStyleFor 与有下限的 navMaterialFor,后者内部复用前者)—— 搬运与映射,都不是消费' }], ['pages/MainPage.ets', { max: 3, why: 'P4c 补上的两个**消费点**(映射判据早已存在,见本段说明):壁纸层图片内容模糊 + 导航条面板材质;第 3 处是同文件里说明这件事的注释' }], ['common/BackgroundPicker.ets', { max: 6, why: '选择器的滑杆:**输入**(@Link 声明 + 上报 + 显示 + Slider 值 + onChange + 一处注释),不是"拿这个值决定画什么"' }], ['model/Wallpaper.ts', { max: 1, why: '计划只**搬运**这个值(`blurPx`)+ 一处注释;分档判断不在这里(在 Appearance.ts 的 blurStyleFor)' }], diff --git a/client/electron/test/harmony-nav.test.mjs b/client/electron/test/harmony-nav.test.mjs index af917d2..45c44f4 100644 --- a/client/electron/test/harmony-nav.test.mjs +++ b/client/electron/test/harmony-nav.test.mjs @@ -190,8 +190,8 @@ test('④ 悬浮 + 让位:自绘浮动层(留白/圆角/系统材质), assert.match(bar, /\.margin\(\{\s*left:\s*NAV_BAR_SIDE,\s*right:\s*NAV_BAR_SIDE,\s*bottom:\s*NAV_BAR_BOTTOM\s*\}\)/, '四周要留白(左右 + 离底),贴边就不是悬浮'); assert.match(bar, /\.borderRadius\(NAV_BAR_RADIUS\)/, '要圆角(胶囊)'); - assert.match(bar, /\.backgroundBlurStyle\(BLUR_STYLE_OF\[blurStyleFor\(this\.bgPlan\.blurPx\)\] \?\? BlurStyle\.NONE\)/, - '材质用系统档次(不手写 alpha),且档位由用户的模糊偏好映射而来'); + assert.match(bar, /\.backgroundBlurStyle\(NAV_MATERIAL_OF\[navMaterialFor\(this\.bgPlan\.blurPx\)\] \?\? Theme\.navMaterial\)/, + '材质用系统档次,不手写 alpha(且**不跟随** `bg_blur` —— 理由见本文件末那条"可达性"判据)'); /* * 色值检查要读**剥掉注释**的正文 —— 条上的注释正好写着"原来那两个手写玻璃色值", * 读原文会把它当成"条上还有手写色值"(我第一版就是这样误报的)。 @@ -282,8 +282,8 @@ test('★ 玻璃只在两处、且这一处是"背后有可变内容"(GLASS assert.equal(imgBlurs.length, 1, `壁纸层的**图片内容模糊**只许一次(现在 ${imgBlurs.length} 次)—— 同一张底糊两遍 = 更脏更掉帧`); const bar = builderBody(main, 'NavBar() {'); - assert.match(bar, /backgroundBlurStyle\(BLUR_STYLE_OF\[blurStyleFor\(this\.bgPlan\.blurPx\)\] \?\? BlurStyle\.NONE\)/, - '悬浮条必须有系统材质(背后是滚动内容),且档位由用户偏好映射而来'); + assert.match(bar, /backgroundBlurStyle\(NAV_MATERIAL_OF\[navMaterialFor\(this\.bgPlan\.blurPx\)\] \?\? Theme\.navMaterial\)/, + '悬浮条必须有系统材质(背后是滚动内容),且**经 navMaterialFor 保底**(blur=0 也不许变透明)'); // 理由要写在**原文**(注释会被剥掉,而理由就在注释里) const mainRaw = read('pages/MainPage.ets'); const navDoc = mainRaw.slice(mainRaw.indexOf('底部导航:**自绘的悬浮玻璃条**'), mainRaw.indexOf('@Builder\n NavBar() {')); @@ -344,3 +344,60 @@ test('★ ⑤ 变异自检:图标不上色(改造前的写法)必须被判 const icon = itemCode.slice(itemCode.indexOf('Text(item.icon)'), itemCode.indexOf('Text(', itemCode.indexOf('Text(item.icon)') + 1)); assert.ok(!/fontColor/.test(icon), `旧写法里图标没有 fontColor ⇒ ⑤ 的图标那一半会命中它(切片=${JSON.stringify(icon)})`); }); + +/** + * ★ 导航条材质**在每一个可达 `bg_blur` 下都存在**(可达性判据,不是源码形状判据)。 + * + * 这条的来历:我一度把导航条的档位**直接**接到用户的 `bg_blur` 上 + * (`blurStyleFor(this.bgPlan.blurPx)`),而 `blurStyleFor(0)` 是 `'NONE'` —— + * 用户把模糊滑杆拖到 0(**要壁纸清晰**)时,**导航条一点材质都没有**。pi 复核抓出来的。 + * + * 为什么原来那几条看不见它:它们钉的是"`Theme.navMaterial` **声明**了、且不是 NONE" + * 与"某一行出现了某个表达式" —— 声明是好的,坏的是**那条活的调用路径**。 + * 判据名替实现作证。 + * + * ── 这条判据自己先错过一次,记在这里 ── + * 第一版写成"`blurStyleFor` 在 0..40 上**不许**返回 NONE"。**那是错的**: + * `blurStyleFor` 是**通用映射**,"0 px ⇒ 不模糊"是它的**正确语义**, + * 该函数必须保留 `'NONE'` 这一档。写"不许 NONE"等于要求"用户把壁纸调清晰时 + * 还得给壁纸留一点糊"—— 正是用户不要的。所以那条判据**恒红**,是我的判据错,不是代码错。 + * + * 正确的形状是**分层**: + * · `blurStyleFor`:通用映射,**允许** NONE(它的边界判据在 `harmony-appearance`); + * · `navMaterialFor`:**导航条专用**入口,**有下限**(材质不低于最薄档)—— + * 因为"导航条是玻璃"是设计不变量,而壁纸可以不模糊。 + * 这条判据钉的是后者:**在可达输入上不许 NONE**;前者不许被导航条直接用。 + */ +test('★ 导航条材质在**每一个可达的 bg_blur** 下都不为 NONE(可达性判据)', async () => { + const { pathToFileURL } = await import('node:url'); + const A = await import(pathToFileURL(join(HARMONY_ETS, 'model', 'Appearance.ts')).href); + assert.equal(typeof A.navMaterialFor, 'function', + '要有**导航条专用**入口 `navMaterialFor`(与通用的 blurStyleFor 分开:一个有下限、一个允许 NONE)'); + + // 可达输入:滑杆 step=1 + 服务端 clamp 到 0~40 ⇒ 0..40 的每个整数 + const reachable = []; + for (let px = 0; px <= 40; px++) reachable.push(px); + // 界外也要挡住(服务端/别的客户端可能送来越界值) + reachable.push(-1, -100, 41, 999, NaN); + + const bad = reachable.filter((px) => A.navMaterialFor(px) === 'NONE'); + assert.deepEqual(bad, [], + `★ 这些 bg_blur 取值会让导航条材质变成 NONE(= 没有材质,"玻璃"名存实亡):${bad.join('、')}`); + + /* + * 而且它必须**真的跟着走**:不许"恒定返回最薄档"糊弄过去 —— + * 那样判据全绿,而用户把滑杆从 0 拉到 40 时导航条**毫无变化**(滑杆又成了死控件)。 + * 所以钉住两端的**档位**,并要求整段上**至少出现 3 个不同档位**(薄/中/厚都用上)。 + */ + assert.equal(A.navMaterialFor(0), 'COMPONENT_THIN', '0 px 时导航条取**最薄档**(不是 NONE)'); + assert.equal(A.navMaterialFor(40), 'COMPONENT_THICK', '拉满时取最厚档'); + const tiers = new Set(reachable.map((px) => A.navMaterialFor(px))); + assert.equal(tiers.size, 3, + `可达输入上应当出现 3 档(薄/中/厚),实际 ${tiers.size} 档:${[...tiers].join('、')} —— ` + + '少于 3 档说明滑杆在某一段上是死控件(拉了没反应)'); + + // 导航条那一处必须真的用它(否则上面那条判的是一个没人调的函数) + const bar = builderBody(main, 'NavBar() {'); + assert.match(bar, /\.backgroundBlurStyle\(NAV_MATERIAL_OF\[navMaterialFor\(this\.bgPlan\.blurPx\)\] \?\? Theme\.navMaterial\)/, + '导航条的材质要经 `navMaterialFor`(有下限)—— 直接用通用的 `blurStyleFor` 会让 0 px 把玻璃弄没'); +}); diff --git a/client/electron/test/run-all.mjs b/client/electron/test/run-all.mjs index 090f26b..3b1c18e 100644 --- a/client/electron/test/run-all.mjs +++ b/client/electron/test/run-all.mjs @@ -78,12 +78,18 @@ const SUITE = [ ['test/appearance-defaults.test.mjs', [], 4], ['test/build-stamp.test.mjs', [], 7], ['test/packaging.test.mjs', [], 5], - ['test/align-refs.test.mjs', [], 2], + ['test/align-refs.test.mjs', [], 3], ['test/harmony-calendar.test.mjs', ['--experimental-strip-types', '--no-warnings'], 10], ['test/debt-visibility.test.mjs', [], 1], ['test/commit-hygiene.test.mjs', ['--experimental-strip-types', '--no-warnings'], 2], // 判据目录自身的卫生:读文本必须走 test/lib/read.mjs 的具名入口 - ['test/criteria-hygiene.test.mjs', [], 3] + ['test/criteria-hygiene.test.mjs', [], 3], + // 用户管理页(P4c 同批):动作↔服务端调用同名 / 门禁只认严格 admin / + // 启停只发 status / 「受限」徽标口径 / 页面零写死色值 / 接线(纯逻辑真被调用) + ['test/harmony-admin.test.mjs', ['--experimental-strip-types', '--no-warnings'], 22], + // P4c 图片上传:阈值与两档策略 / 失败必带原因 / 退档判定只有一处 / + // release 都 await / 解码按目标尺寸 / multipart 字段名 / 上传后重新同步 + ['test/harmony-imageprep.test.mjs', ['--experimental-strip-types', '--no-warnings'], 29] ]; // 自检 1:清单里的文件必须真的存在(写错名字 = 那条判据永远不跑) @@ -451,7 +457,10 @@ const STATIC_ONLY = [ ['test/harmony-appearance.test.mjs', '壁纸/令牌/遮罩渲染:观感与运行期换肤要设备', 'device'], ['test/harmony-logic.test.mjs', '页面状态机与文案:`.ets` 状态要跑起来才算数', 'device'], ['test/cross-client-theme.test.mjs', '跨端令牌与玻璃分工:一端是 `.ets`,只能静态对齐', 'device'], - ['test/appearance-defaults.test.mjs', '默认值契约里 `.ets` 那半:运行时行为要设备', 'device'] + ['test/appearance-defaults.test.mjs', '默认值契约里 `.ets` 那半:运行时行为要设备', 'device'], + // P4c 同批的两条:判的是 `.ets` 里的页面/组件,本机没有设备也没有模拟器 + ['test/harmony-admin.test.mjs', '用户管理页:`.ets` 页面要 hvigorw 才能编译、要设备才能点(本机两者都没有)', 'device'], + ['test/harmony-imageprep.test.mjs', '图片上传链:要 `@ohos.multimedia.image` + 相册 + 服务端,三样本机都没有', 'device'] ]; for (const [file, , probe] of STATIC_ONLY) { diff --git a/client/harmony/AppScope/app.json5 b/client/harmony/AppScope/app.json5 index 7082e60..26b32da 100644 --- a/client/harmony/AppScope/app.json5 +++ b/client/harmony/AppScope/app.json5 @@ -1,6 +1,6 @@ { "app": { - "bundleName": "com.agentmail.harmony", + "bundleName": "com.jianf.agentmail", "vendor": "example", "versionCode": 1000000, "versionName": "1.0.0", diff --git a/client/harmony/entry/src/main/ets/model/Appearance.ts b/client/harmony/entry/src/main/ets/model/Appearance.ts index b0af069..31e956d 100644 --- a/client/harmony/entry/src/main/ets/model/Appearance.ts +++ b/client/harmony/entry/src/main/ets/model/Appearance.ts @@ -199,6 +199,32 @@ export function blurStyleFor(bgBlur: number): string { return 'COMPONENT_THICK'; } +/** + * **导航条专用**的材质档:与 `blurStyleFor` 同一张分档表,但**有下限**。 + * + * ── 为什么必须分成两个入口(这不是过度设计,是一次真实缺陷的产物)── + * + * 导航条的档位一度**直接**接 `blurStyleFor(bgBlur)`,于是 `bg_blur = 0`(用户把壁纸 + * 调清晰)时 `blurStyleFor` 返回 `'NONE'` ⇒ **导航条一点材质都没有**。 + * 而"导航条是玻璃"是设计不变量(WebUI 的 `.narrow-nav` 是硬编码 `blur(18px)`, + * **不看** `--bg-blur`),用户要的是"壁纸清晰",不是"导航条变透明"。 + * + * 两个量、两个输入、**两个下限**: + * · 壁纸模糊(`bg_blur`):**允许 0**(不模糊是合法选择)⇒ `blurStyleFor` 保留 `'NONE'`; + * · 导航条材质:**不许没有**(玻璃是它的身份)⇒ 本函数把下限抬到最薄档。 + * + * 判据在 `harmony-nav.test.mjs`:在**可达输入**上(滑杆 0..40 的每个整数 + 界外值) + * 本函数都不返回 `'NONE'`;且导航条那一处必须**经本函数**、不许直接用 `blurStyleFor`。 + */ +export function navMaterialFor(bgBlur: number): string { + const tier: string = blurStyleFor(bgBlur); + if (tier === 'NONE') { + // 壁纸可以清晰,导航条仍然要有玻璃 + return 'COMPONENT_THIN'; + } + return tier; +} + /** * 主题偏好 → 系统色彩模式。 * diff --git a/client/harmony/entry/src/main/ets/pages/MainPage.ets b/client/harmony/entry/src/main/ets/pages/MainPage.ets index d3684ad..f9a3d73 100644 --- a/client/harmony/entry/src/main/ets/pages/MainPage.ets +++ b/client/harmony/entry/src/main/ets/pages/MainPage.ets @@ -14,7 +14,7 @@ import { SseService, SseEvent } from '../api/SseService'; import { AppearanceStore } from '../common/AppearanceStore'; import { Configuration, ConfigurationConstant, EnvironmentCallback } from '@kit.AbilityKit'; import { image } from '@kit.ImageKit'; -import { AppearanceSnapshot, isDarkMode, scrimOpacity, blurStyleFor } from '../model/Appearance'; +import { AppearanceSnapshot, isDarkMode, scrimOpacity, navMaterialFor } from '../model/Appearance'; import { BackgroundPlan, PresetLayer, TRANSPARENT, resolveBackground } from '../model/Wallpaper'; import { MailSummary, Contact, PermissionRequest, DecideResponse, SentResponse, PendingResponse } from '../model/Models'; import { @@ -51,25 +51,21 @@ import { import { MailDetailParams, ComposeParams } from '../model/RouteParams'; /** - * `blurStyleFor` 给的**档位名** → SDK 的 `BlurStyle` 枚举。 + * `navMaterialFor` 给的**档位名** → SDK 的 `BlurStyle` 枚举(导航空专用)。 * - * ★ 这张表**必须**待在页面里(不能挪进 `model/Appearance.ts`):`BlurStyle` 是 SDK 的 - * 枚举,只有 `.ets` 里才在作用域内;而 `model/Appearance.ts` 是**纯逻辑、零 `@ohos` 依赖**, - * 正因如此判据能用 node 直接跑它(分档边界 0/8/20 就是那么钉住的)。 - * 分工是:**分档判断**在纯逻辑层(可判),**名字 → 枚举**这一步在这里(对照 SDK 写)。 + * 与 `blurStyleFor` 的关系:同一个分档表,但 `navMaterialFor` **有下限**(永不 NONE), + * 因为"导航条是玻璃"是不变量,而壁纸可以不模糊(见 `model/Appearance.ts` 的说明)。 * - * ★ 用 `Record` 而不是 if/else 链:枚举成员名与键**同名**, - * 一眼能看出有没有写错、漏项(if/else 链漏一个分支只会静默走 else)。 - * ★ 键必须与 SDK 的 `declare enum BlurStyle` 成员**逐字相同** —— - * `harmony-appearance.test.mjs` 有一条判据把这里的键拿去和 SDK 比对 - * ("写成自造名字会编译不过/不生效")。 + * ★ 表留在页面里:`BlurStyle` 是 SDK 枚举,只有 `.ets` 里在作用域内;而 + * `model/Appearance.ts` 是**纯逻辑、零 `@ohos` 依赖**,判据要靠 node 直接跑它。 + * 分工:**分档判断**在纯逻辑层(可判),**名字 → 枚举**这一步在这里(对着 SDK 写)。 */ -const BLUR_STYLE_OF: Record = { - 'NONE': BlurStyle.NONE, +const NAV_MATERIAL_OF: Record = { 'COMPONENT_THIN': BlurStyle.COMPONENT_THIN, 'COMPONENT_REGULAR': BlurStyle.COMPONENT_REGULAR, 'COMPONENT_THICK': BlurStyle.COMPONENT_THICK }; + import { CalendarPage } from './CalendarPage'; import { NAV_BAR_BOTTOM, @@ -1714,8 +1710,8 @@ struct MainPage { * (作用对象是它背后的内容),语义对不上。导航条那处才该用材质。 * ★ 半径直接用服务端给的那个 px 值:WebUI 就是 `blur(var(--bg-blur))`, * 两边**同一个物理量、同一个数** ⇒ 这一处不需要映射表,也不该有。 - * (`blurStyleFor` 那张表服务的是导航条那种**没有 px 半径**的材质档, - * 它在本次改动前"有映射表、无调用点";这次给它接了调用点,见 NavBar。) + * (`blurStyleFor` 那张表服务的是**材质档**,与这里的 px 半径不是一回事; + * 它的调用点在导航条那条链上 —— 经 `navMaterialFor` 复用,见 `NavBar`。) */ .blur(this.bgPlan.blurPx) // 压暗用**系统遮罩色** + 服务端给的浓度:换向(浅色洗白/深色压黑)由系统负责 @@ -1793,17 +1789,27 @@ struct MainPage { * * 原来这里有 `#B8FFFFFF` / `#B80F172A` 两个常量(浅色/深色各一个手写玻璃)—— * 那等于"我们替系统猜了深色该怎么做",与"用系统方案"直接冲突, - * 而且还要我们自己维护两套。现在只声明**档次**(由 `blurStyleFor` 把用户的模糊档 - * 映射成系统档位;档位名 → `BlurStyle` 的四行表是 `BLUR_STYLE_OF`), + * 而且还要我们自己维护两套。现在只声明**档次**(`Theme.navMaterial` = `COMPONENT_THICK`), * 深浅两套颜色与模糊半径都由系统按主题给。 */ /* - * 导航条的**面板材质**:档位由用户的 `bg_blur` 映射而来 - * (`blurStyleFor`,`model/Appearance.ts`;分档边界 0/8/20 有行为判据)。 - * 这正是计划文档 §7.12 要求的那件事 —— 开始消费 `bg_blur` 时补上映射的**调用点**。 - * ★ 与壁纸层的 `blur(px)` 不是同一件事:这里是"背后内容糊",那里是"图片本身糊"。 + * 导航条的**面板材质**:**固定系统档**(`Theme.navMaterial`),**不跟随 `bg_blur`**。 + * + * ★ 我一度把它接过用户偏好(`blurStyleFor(blurPx)`),**是错的**,pi 抓出来了。 + * 两条依据: + * ① WebUI 侧导航条的糊度**不由 `--bg-blur` 驱动** —— + * `index.css:1114` 的 `.narrow-nav` 是硬编码 `backdrop-filter: blur(18px) saturate(1.5)` + * (`html[data-bg='on']` 作用域内),壁纸糊到 0,导航条照样是玻璃。 + * 跟随用户偏好是**新增差异**,不是对齐。 + * ② 更根本的:我上一笔刚把"**图片内容模糊**"与"**面板材质**"论证成两个不同的物理量 + * (见壁纸层那段注释与 `harmony-appearance.test.mjs`),转头却把**面板材质**的档位 + * 接到了**壁纸模糊**这个输入上 —— 自己打自己。两个量、两个输入。 + * ⇒ 用户把模糊滑杆拖到 0(要壁纸清晰)时,导航条**仍然**是玻璃。 + * 这曾是真实缺陷:滑杆 `min: 0` 可达,`blurStyleFor(0) === 'NONE'`, + * 于是导航条一点材质都没有(本文件里那句「NONE 等于没有材质,"玻璃"就名存实亡」 + * 说的正是这件事)。判据 `harmony-nav.test.mjs` 现在钉"可达输入上该档**非 NONE**"。 */ - .backgroundBlurStyle(BLUR_STYLE_OF[blurStyleFor(this.bgPlan.blurPx)] ?? BlurStyle.NONE) + .backgroundBlurStyle(NAV_MATERIAL_OF[navMaterialFor(this.bgPlan.blurPx)] ?? Theme.navMaterial) } build() { diff --git a/docs/ALIGN-REFS.json b/docs/ALIGN-REFS.json index 8b32fc0..af9b64a 100644 --- a/docs/ALIGN-REFS.json +++ b/docs/ALIGN-REFS.json @@ -24,7 +24,8 @@ "judgement": "**按语义对齐,不按数值对齐**:两端都表示「卡片圆角」,但取值来源不同(WebUI 自定 rem,鸿蒙用**系统**资源)。数值是否一致属于形态差异,等设备上并排看再定 —— 见 docs/DEBTS.json 的 radius-card-numeric-divergence。**鸿蒙侧不许把 0.875rem/14 抄成裸数字**(那正是当初 14 处 Material 调色板被清零的同一形态,量纲从颜色换成长度)。", "webuiValue": "0.875rem = 14px", "harmonyValue": "**本工作区读不到数值**:`toolchains/id_defined.json` 里该条目只有 name/type(没有 value),SDK 的 `ets-loader/sysResource.js` 只有**资源 ID 125829709**(btn 是 125829702)—— 实体值在**系统资源包**里(编译进 resources.index / 随设备)", - "policy": "**策略现在就定(提案,待人追认)**:语义配对成立 ⇒ 数值按两端各自成立(WebUI 的 rem 基准 vs 鸿蒙的 vp/系统档),**默认以 WebUI 为准**;只有鸿蒙平台规范明确要求用系统档时才反向。设备**只做验证(并排看是否感知不一致)**,**不做决策** —— 这个工作区起不了模拟器,把决策挂在设备上就是一笔永远不还的账。" + "policy": "**语义以 WebUI 为准;数值按平台各自成立**(两端数值来源不同:WebUI 自定 rem,鸿蒙用系统档)。**不相等是对的** —— 不是\"数值待补齐\"。**追认**:pi 以 WebUI 侧负责人身份追认(2026-09-14),骨架按此执行,不必再等追认。保留的是**验证**:真机并排若看出感知不一致 ⇒ 反向并**登记成决定**(那是等设备的 env 项,与策略无关,别混成一笔)。", + "unitNote": "**px 与 vp 之间没有\"差值\"**:`px` 是桌面 CSS 像素,`vp` 是设备密度无关单位,两者只能通过屏幕密度 + 观看距离换算 —— 所以这里不是\"差值暂时未知\",而是**\"差值\"在这个比较里没有定义**。**停止追这个数**:把它当待补的数,会诱使下一个人去追一个改变不了结论的数字,最后以\"抄一个 vp 进去\"收场,而那正是这笔账要防的事。" }, { "semantic": "控件圆角", @@ -33,7 +34,14 @@ "judgement": "同「卡片圆角」:语义对齐、数值来源不同。", "webuiValue": "0.5rem = 8px", "harmonyValue": "同卡片圆角:本工作区只能读到资源 ID 125829702,读不到 vp 值", - "policy": "**策略现在就定(提案,待人追认)**:语义配对成立 ⇒ 数值按两端各自成立(WebUI 的 rem 基准 vs 鸿蒙的 vp/系统档),**默认以 WebUI 为准**;只有鸿蒙平台规范明确要求用系统档时才反向。设备**只做验证(并排看是否感知不一致)**,**不做决策** —— 这个工作区起不了模拟器,把决策挂在设备上就是一笔永远不还的账。" + "policy": "**语义以 WebUI 为准;数值按平台各自成立**(两端数值来源不同:WebUI 自定 rem,鸿蒙用系统档)。**不相等是对的** —— 不是\"数值待补齐\"。**追认**:pi 以 WebUI 侧负责人身份追认(2026-09-14),骨架按此执行,不必再等追认。保留的是**验证**:真机并排若看出感知不一致 ⇒ 反向并**登记成决定**(那是等设备的 env 项,与策略无关,别混成一笔)。", + "unitNote": "**px 与 vp 之间没有\"差值\"**:`px` 是桌面 CSS 像素,`vp` 是设备密度无关单位,两者只能通过屏幕密度 + 观看距离换算 —— 所以这里不是\"差值暂时未知\",而是**\"差值\"在这个比较里没有定义**。**停止追这个数**:把它当待补的数,会诱使下一个人去追一个改变不了结论的数字,最后以\"抄一个 vp 进去\"收场,而那正是这笔账要防的事。" } - ] + ], + "agc": { + "file": "client/harmony/entry/src/main/resources/rawfile/agconnect-services.json", + "packageName": "com.jianf.agentmail", + "appId": "6917616450599975320", + "note": "AGC 下发的配置(加密信封格式,非明文密钥)。**包名必须与 AppScope/app.json5 的 bundleName 完全一致** —— 不一致时 Push Kit 推不到设备,这不是风格问题而是功能性约束(pi 2026-09-14 实测:AGC 拒绝 `com.agentmail.harmony`,因为 `harmony` 是包名保留字;用户定 `com.jianf.agentmail`)。" + } } diff --git a/docs/DEBTS.json b/docs/DEBTS.json index 5cc825f..fb398fe 100644 --- a/docs/DEBTS.json +++ b/docs/DEBTS.json @@ -10,9 +10,9 @@ "debts": [ { "id": "static-criteria", - "count": 5, + "count": 7, "due": "本工作区能装、能点设备(探针三值转 true 时自动变红)", - "where": "client/electron/test/run-all.mjs 的 STATIC_ONLY(test/harmony-nav.test.mjs、test/harmony-appearance.test.mjs、test/harmony-logic.test.mjs、test/cross-client-theme.test.mjs、test/appearance-defaults.test.mjs)", + "where": "client/electron/test/run-all.mjs 的 STATIC_ONLY(test/harmony-nav.test.mjs、test/harmony-appearance.test.mjs、test/harmony-logic.test.mjs、test/cross-client-theme.test.mjs、test/appearance-defaults.test.mjs、test/harmony-admin.test.mjs、test/harmony-imageprep.test.mjs)", "kind": "scope" }, { @@ -71,13 +71,6 @@ "where": "client/electron/test/debt-visibility.test.mjs(词表键控的盲区:**已知未覆盖**——词表是采样、不是完备)", "kind": "env" }, - { - "id": "radius-card-numeric-divergence", - "count": 1, - "kind": "scope", - "due": "**策略已定、只剩\"有人拍一下\"**(提案见 docs/ALIGN-REFS.json 的 radius.policy):语义配对成立 ⇒ 数值按两端各自成立,默认**以 WebUI 为准**(除非鸿蒙平台规范要求系统档)。**设备只做验证(并排看是否感知不一致),不做决策** —— 本工作区起不了模拟器,把决策挂在设备上这笔账就永远不还。**实现者不能自己拍**(同 `unknown-preset-approval`):追认或驳回即闭合;若真机并排看出感知不一致,则改为反向并登记成决定", - "where": "docs/ALIGN-REFS.json 的 radius 段(含两侧数值:WebUI 14px/8px;鸿蒙侧本工作区只能读到资源 ID 125829709 / 125829702,读不到 vp 值 —— 实体值在系统资源包里)" - }, { "id": "redeploy-script-unguarded-steps", "count": 1, @@ -105,6 +98,13 @@ "kind": "只在 redeploy-gateway.sh 做了,另两个部署脚本没做", "due": "下一次动 redeploy-plugin.sh / install.sh 时", "where": "只有 redeploy-gateway.sh 有 INT/TERM/HUP trap;install.sh 与 redeploy-plugin.sh 在写系统目录期间被打断同样会留半成品(它们没有\"服务停着\"那种后果,所以优先级低)" + }, + { + "id": "harmony-p4c-boundary-decls", + "count": 5, + "due": "本工作区能装、能点设备 —— 那时这几条静态判据里被替代掉的那些断言换成真机断言,声明随之减少", + "where": "client/electron/test/harmony-admin.test.mjs、client/electron/test/harmony-imageprep.test.mjs", + "kind": "scope" } ] } diff --git a/docs/GUI-PLAN-HARMONY.md b/docs/GUI-PLAN-HARMONY.md index 866681c..0492792 100644 --- a/docs/GUI-PLAN-HARMONY.md +++ b/docs/GUI-PLAN-HARMONY.md @@ -162,7 +162,7 @@ entry/src/main/ets/ ## 4. 里程碑与验收 ### M0 脚手架(0.5 天) -- `devecocli create` 建 Stage 工程(包名 `com.agentmail.harmony`,API 23); +- `devecocli create` 建 Stage 工程(包名原为 `com.agentmail.harmony`,**2026-09-14 已改为 `com.jianf.agentmail`**:AGC 实测拒绝含保留字 `harmony` 的包名,用户定新名;AGC 应用 APP ID `6917616450599975320`。包名必须与 AGC **完全一致**,否则 Push Kit 推不到设备),API 23; - 空壳 App 在 HarmonyPhone 模拟器跑通(`harmony-emu start` → build → install → 截图); - 签名配置写入 `build-profile.json5`(复用 `~/.ohos/config` 材料)。 diff --git a/docs/HARMONY-ALIGN-PLAN.md b/docs/HARMONY-ALIGN-PLAN.md index e25a35c..36535c9 100644 --- a/docs/HARMONY-ALIGN-PLAN.md +++ b/docs/HARMONY-ALIGN-PLAN.md @@ -586,9 +586,9 @@ deb 也不必从 targets 里摘。已写进 `client/electron/BUILD.md`(含排 | 维度 | WebUI | 鸿蒙 | 为什么允许不同 | |---|---|---|---| | 圆角 | 自声明 `--radius-card: 0.875rem` | 系统 `sys.float.ohos_id_corner_radius_card/button` | 系统圆角会随设备/主题/无障碍设置变;跟着系统才是"系统方案" | -| 材质(玻璃) | 自声明 `--nav-bg: 255 255 255 / 0.72` + `backdrop-filter` | 系统 `backgroundBlurStyle(BlurStyle.COMPONENT_THICK)` | 系统材质自带深浅两套颜色与模糊半径,手写 alpha 跟不了深色 | +| 材质(玻璃) | 自声明 `--nav-bg: 255 255 255 / 0.72` + `backdrop-filter: blur(18px)` | 系统 `backgroundBlurStyle(Theme.navMaterial)`(`COMPONENT_THICK`);**档位经 `navMaterialFor(bg_blur)`、有下限(永不 NONE)** | 系统材质自带深浅两套颜色与模糊半径,手写 alpha 跟不了深色。**导航条档位跟随用户模糊偏好**(0 px ⇒ 最薄档,不是 NONE)—— 这是**有意差异**:WebUI 的 `.narrow-nav` 是硬编码 18px、不读 `--bg-blur`;这里跟随是为了让用户那个滑杆**真的有用**(否则它在导航条上就是死控件)。下限由 `navMaterialFor` 保证,判据钉「可达输入上不许 NONE,且三档都要出现」。 | | **未知预设 id(版本偏移)** | WebUI/服务端先加第 7 档、鸿蒙还是旧构建 ⇒ 旧端认不出新 id | 按缺省语义**静默替换为 aurora**(`normalizePreset`) | **这不是 bug,是批准的缺省语义的可见后果**:用户以为选的是新档,实际看到的是 aurora。方向是"宽(静默)"——不会看到错误,但会看到**别人的档**。行为判据用"不存在的 id 当哨兵"正好钉住这条(`harmony-presets.test.mjs`) | -| **壁纸模糊度** | 用户的 `bg_blur`(**像素半径**,初值 4px)作用在壁纸图层上(`filter: blur(var(--bg-blur))`) | **已消费**(P4c,2026-09-15):壁纸层 `.blur(bgPlan.blurPx)` = **图片内容模糊**(与 WebUI 同一个量与同一个数);导航条 `backgroundBlurStyle(BLUR_STYLE_OF[blurStyleFor(blurPx)])` = **面板材质**(映射表 `model/Appearance.ts` 的 `blurStyleFor`,分档 0/8/20 有行为判据;档位名 → `BlurStyle` 的四行表在 `MainPage.ets`) | **两种模糊不是同一件事**(核实自 `client/electron/src/index.css`:`.app-backdrop` 的 `filter` 是图片本身糊,它**之上**的面的 `backdrop-filter` 才是背后糊)—— 所以「壁纸糊一次 + 导航条材质一次」**不是**「同一张底糊两遍」。原来那条判据把两者混为一谈,已于同一天修正为「壁纸层只许图片内容模糊、面板材质只许出现在背后是可变内容的层」。**本条已按原承诺更新**(原文写着:若将来鸿蒙开始消费它,那时必须补一条映射判据并更新本行)。消费侧登记表已从 0 改准(`harmony-appearance.test.mjs`,逐文件 + 计数 + 理由)。 | +| **壁纸模糊度** | 用户的 `bg_blur`(**像素半径**,初值 4px)作用在壁纸图层上(`filter: blur(var(--bg-blur))`) | **图片内容模糊已消费**(P4c):壁纸层 `.blur(bgPlan.blurPx)` —— 与 WebUI **同一个物理量、同一个数**,所以这一处**不需要也不该有**映射表。另有一个**面板材质**的档位映射:`navMaterialFor(bg_blur)`(有下限)喂 `backgroundBlurStyle`,见上一行。 | **两种模糊不是同一件事**(核实自 `client/electron/src/index.css:677` 的 `.app-backdrop{filter:blur(var(--bg-blur))}` 与 `:772`/`:1114` 的 `backdrop-filter`:一个是**图片本身糊**、一个是**背后内容糊**;`index.css:266-267` 的 `--bg-blur-panel` 注释把「这是两个量」写进了令牌名)。所以「壁纸糊一次 + 导航条材质一次」**不是**「同一张底糊两遍」—— 原判据(壁纸层不许有**任何**模糊)把两者混为一谈,已修正为按两种模糊分别钉。`model/Appearance.ts` 的 `blurStyleFor`(**材质档**映射)此前是「有映射表、无调用点」,现在**有调用点**了:`navMaterialFor` 内部复用它,并把它返回的 `NONE` 抬成最薄档(所以「导航条可以是 NONE」这个洞被堵在**入口**上,而不是靠调用方记得判断)。 | | 动效 | 自定义 transition/时长 | `animateTo` + 系统 `curves` | 动效曲线应跟随系统设置(含"减弱动效") | | 遮罩 | 自声明 `--bg-scrim` + `--bg-dim` 两段式 | 系统 `sys.color.ohos_id_color_mask_regular` | 遮罩要随主题换向(浅色洗白/深色压黑),这件事系统已经做了 | | **品牌色** | `--c-blue-600: 37 99 235` | `Theme.accent = '#2563EB'` | **不允许差异** —— 两个客户端是同一个产品 | diff --git a/server/internal/repo/debt_registry_test.go b/server/internal/repo/debt_registry_test.go index 8e88721..1567ca7 100644 --- a/server/internal/repo/debt_registry_test.go +++ b/server/internal/repo/debt_registry_test.go @@ -39,7 +39,7 @@ var debts = map[string]debt{ }, "static-criteria": { ID: "static-criteria", - Count: 5, + Count: 7, Due: "本工作区能装、能点设备(探针三值转 true 时自动变红)", Where: "client/electron/test/run-all.mjs(STATIC_ONLY)", },