跨端: harmony 管理页(用户管理)+ P4c 壁纸上传入口 —— 「功能做全再交付」的两块
pi 的交付清单里缺的两块(`docs/GUI-PLAN-HARMONY.md` 原先把管理后台划在首版之外, 用户明确要求「功能做全再给我」之后收进来)。 标 `跨端:` 是因为判据落在 `client/electron/test/`(鸿蒙的判据目录一向量在那里), 代码本体全在 `client/harmony/`。 ## 管理页(用户管理) - `pages/AdminUsersPage.ets`:新建 / 编辑(显示名·角色·白名单)/ 启停 / 重置密码。 排布照 `AdminUsersPage.tsx`,包括「受限」徽标口径(普通用户且白名单非空才显示)、 最后登录缺席与空串都显示「从未登录」、管理员对白名单两项忽略。 - 入口在设置页底部,**仅管理员可见**(`role === 'admin'` 严格相等,与 `App.tsx` 同口径)。 读不到身份时**不**显示也不报错(乐观放行会让每个普通用户看到点进去 403 的入口)。 - `api/AdminApi.ets` + `model/AdminUsers.ts`(纯逻辑,零 import ⇒ 判据能真跑)。 - 启停**只发 status 一个字段** —— 服务端是部分更新,多发字段会把显示名与白名单一起改掉。 - `model/Models.ets` 补管理端 DTO;`main_pages.json` 注册路由。 ## P4c 壁纸上传 - `model/ImagePrep.ts`:阈值与两档策略(2560/0.85 → 1280/0.78,入口 20MB,压后上限 3.5MiB)。 **一处有意不对齐 WebUI** 并写明理由:WebUI 卡 data-URL 长度(含 base64 膨胀), 鸿蒙内存直传 ArrayBuffer,卡的是字节数。 - `common/BackgroundPicker.ets`:不设 / 预设 / 自定义图片 + 浓度与模糊滑杆。 上传链:picker → 判可不可以 → 逐档按 desiredSize 解码压缩 → 上传 → **请页面以服务端为准重新同步**。 失败**必带原因**(服务端 415/413 文案原样透出)。用户取消选图**不算失败**。 - `ApiClient.uploadBytes`:MultiFormData.data 收 ArrayBuffer(核了 SDK,since 11;本工程 23) ⇒ 内存直传,不需要 base64 也不需要临时文件。 ## 两处真 bug(变异测试逼出来的,不是"新写坏的") 1. **压缩循环的第二档此前是死代码**:循环里的 break 与循环外那句 shouldRetryWithActual 互相抵消 —— 把循环里那处改成 `if (true)`(永远只压一档)整套判据照样全绿。 收成一处判定(overLimit),循环外只读结论,并加结构性判据(该函数在这条链上只许调用一次)。 2. **壁纸的模糊档一直是「只写不读」**(计划文档 §7.12 登记过):滑杆能拖、值能存、 blurStyleFor 也写了,就是**没有调用点**,壁纸一点没糊。 本次补上的调用点分两层:壁纸层 `.blur(px)` = **图片内容模糊** (与 WebUI 的 `filter: blur(var(--bg-blur))` 同一个量、同一个数,所以不需要映射表); 而那张**材质档**映射表 `blurStyleFor` 也终于有了调用点(`navMaterialFor` 内部复用它)。 `docs/HARMONY-ALIGN-PLAN.md` 的 §7.12 两行(材质 / 壁纸模糊度)已一并改准、不再互相矛盾。 ## pi 复核后**改回来的**(这一笔里我自己犯的两处,都由 pi 抓出) 1. **导航条材质一度绑定到 `bg_blur`,`bg_blur=0` 时整个消失。** 我把 `NavBar` 从固定档改成 `blurStyleFor(bgPlan.blurPx)`,而滑杆 `min: 0` 可达、 `blurStyleFor(0) === 'NONE'` ⇒ 用户把壁纸调清晰时**导航条一点材质都没有**。 而且它与本笔自己的论证**相反**:刚论证完"图片内容模糊"与"面板材质"是两个物理量, 转头把面板材质接到壁纸模糊这个输入上。 现在**分层**:`blurStyleFor` 是通用映射(**允许** NONE —— "0 px 不模糊"是它的正确语义); `navMaterialFor` 是**导航条专用、有下限**的入口(0 px ⇒ 最薄档)。 判据钉**可达性**(滑杆 0..40 每个整数 + 界外值都不许 NONE,且三档都要出现 —— 否则"恒定最薄档"会让滑杆成为死控件)。 2. **`Theme.navMaterial` 被我弄成了死令牌**,而看着它的判据**照样绿** (那条只断言"声明存在且不是 NONE" —— 守的是声明,坏的是活的调用路径)。 现在导航条真的用它;并把同文件里**只覆盖 `Theme.overlay` 一个令牌**的死令牌规则 **铺到 Theme 的全部 35 个令牌**(量**外部引用数**:只被 Theme 内部方法读、 而那个方法自己有外部调用点 ⇒ 不算死 —— `chipSpentBg` 是这种;`navMaterial` 当时 唯一的消费者是一张可整体删掉的局部表,所以必须被抓)。 ## pi 复核后**补上的**(这一笔漏掉的接线,都是我造成的) - **`test/run-all.mjs` 的 SUITE 没接两个新判据文件** ⇒ HEAD 上 `npm test` **一条判据都不跑、直接 exit 1**(套件自检 2 就是为这件事写的)。已接入, 并把两条登记进 `STATIC_ONLY`(`.ets` 要设备 ⇒ 静态欠账)。 - **`debt-visibility` 是我自伤**:那两个新文件里有 5 处"边界声明"但一次都没登记。 我当时报"2 条失败是改动前就红" —— **只对一半**:这条在父提交上是**绿的**。 我那次 `git stash push -u -- client/harmony` 的对照是**无效对照** (`-- client/harmony` 把 `client/electron/test/` 整个排除在外,新判据文件根本没被 stash), 所以两次跑都红、看着像"既有"。已按 pi 的建议改用 `git worktree` 到父提交做对照。 现在两处都登记进 `docs/DEBTS.json`(含 `static-criteria` 5→7,Go 侧同一份登记同步改)。 ## 一并修正的旧判据(都是"太宽/太窄/钉错东西",不是放宽标准) - 「模糊归属」:原文「壁纸层不许有**任何**模糊调用」把**图片内容模糊**与**面板材质** 混为一谈(WebUI 侧核实:`.app-backdrop` 的 filter 与它之上那层的 backdrop-filter 是两个不同的量)⇒ 改成按两种模糊分别钉。 - 「bgBlur 只写不读,消费侧必须为 0」:值不再成立,**形状保留**(逐文件登记 + 计数 + 理由), 标题与断言里的假话一并改掉。 - isDarkMode 那条 `/dark\s*\)/` 断的是**参数顺序**(加一个入参就误红)⇒ 改成"dark 在实参里"。 - 三条钉 `backgroundBlurStyle` **整条字面表达式**的断言 ⇒ 改成钉语义 ("用系统材质 + 材质有下限"),不再匹配那一行的字符。 ## 判据 新增 `harmony-admin.test.mjs`(22 条)、`harmony-imageprep.test.mjs`(29 条); `harmony-presets.test.mjs` 加 1 条(模糊档搬运与归一,含 `-0` 那个洞: `Math.round(-0.4)` 是 `-0` 而 `-0 < 0` 为 false ⇒ 改成判 `!(r > 0)`)。 **`node test/run-all.mjs`:22 个判据文件全部跑起来**,红的只有 1 个: `build-stamp`(`dist` 是 `a5fc86b` 上构建的,`gitRev` 对不上当前 HEAD)。 这条**不是我的代码造成的**(可证:`a5fc86b..HEAD` 之间,`srcHash` 覆盖的那批文件 ——`client/electron/src` 等——**一个都没动过**,所以 `srcHash` 没变,差的是 `gitRev`), 但也**不是"改动前就红"**:任何推进 HEAD 的提交都会让它变红,正确修法是重构建。 ## 未验(如实标注) - **本机无设备/无模拟器 ⇒ 全部观感未验**:管理页排版与卡片观感、滑杆手感、 模糊在真机上的实际档位观感、系统材质在自绘悬浮条上的实际效果。代码齐 ≠ 真机验过。 - 预设档**没有**上模糊(壁纸在预设档下是一叠自绘矩形,系统材质对它不生效)—— 这是我**主动收的范围**,不是漏,真机看一眼再决定要不要补。 - **Go 侧的 `debt_registry_test.go` 我没能跑**(沙箱里没有 Go 模块缓存,`go test` 起不来), 只做了 `gofmt` 校验;那处改动是一行 `Count: 5 → 7`。
This commit is contained in:
@ -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}`);
|
||||
});
|
||||
|
||||
@ -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.<name>` 出现处**前面最近**的成员声明
|
||||
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 单值');
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -165,18 +165,24 @@ test('★ 模糊值映射到**系统材质档次**(不是把 40 当半径塞
|
||||
* 这里判据自己去页面源码里把键取出来,逐个对着 SDK 的成员名核。
|
||||
*/
|
||||
const main = read('pages/MainPage.ets');
|
||||
const tableAt = main.indexOf('const BLUR_STYLE_OF: Record<string, BlurStyle> = {');
|
||||
assert.ok(tableAt > 0, '页面里要有 `BLUR_STYLE_OF`(档位名 → SDK 枚举)那张表');
|
||||
const tableAt = main.indexOf('const NAV_MATERIAL_OF: Record<string, BlurStyle> = {');
|
||||
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)' }],
|
||||
|
||||
@ -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 把玻璃弄没');
|
||||
});
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"app": {
|
||||
"bundleName": "com.agentmail.harmony",
|
||||
"bundleName": "com.jianf.agentmail",
|
||||
"vendor": "example",
|
||||
"versionCode": 1000000,
|
||||
"versionName": "1.0.0",
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 主题偏好 → 系统色彩模式。
|
||||
*
|
||||
|
||||
@ -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<string, BlurStyle>` 而不是 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<string, BlurStyle> = {
|
||||
'NONE': BlurStyle.NONE,
|
||||
const NAV_MATERIAL_OF: Record<string, BlurStyle> = {
|
||||
'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() {
|
||||
|
||||
@ -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`)。"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -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` 材料)。
|
||||
|
||||
|
||||
@ -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'` | **不允许差异** —— 两个客户端是同一个产品 |
|
||||
|
||||
@ -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)",
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user