跨端: 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:
@ -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 单值');
|
||||
|
||||
Reference in New Issue
Block a user