跨端: 抽出基础面/玻璃组件(Motion + Surface)+ 三处硬弹的弹层补过渡
用户两条要求,各自都指向"决策被复制到各处"这个病根:
① 「应该定义一个基础玻璃容器给各个组件引用」
② 「很多页面还是不够精致……封装为基础组件供所有页面使用。基础组件包含动画」
★ 玻璃不透明的真根因(前几轮都没找到,这次是像素证据定的)
实测模拟器 3184×2232、壁纸 aurora + 压暗 37:
Navigation [229,140,3156,2204] ← 255,255,255(整块内容区)
y=2210(Navigation 之外那条缝) ← 226,225,235(壁纸清楚可见)
那条缝是决定性的:**壁纸层本身是好的**,白是因为上面盖了不透明的壳。
链路上一共三层,逐层修:
· `Navigation` 外壳:不设背景 ⇒ 系统默认不透明白,把里面已透明的窗格整片盖住;
· navBar 内容层(列表栏根容器):同样没有背景;
· **卡片**:铺的是 `Theme.surface`(实体系统色)。
修后:缝隙 203,213,228 / 卡片 191,204,220 —— 壁纸透得出来。
★ 新增基础组件(common/)
· `Surface.ets` —— `GlassPane`(正文面)/ `GlassCard`(嵌套面)/ `PageHeader` / `Pressable`
· `Motion.ets` —— `Motion.dur()`:接系统「减弱动态效果」
(`accessibility.isAnimationReduceEnabledSync()`,API 23 正好够用)。
WebUI 侧有 `prefers-reduced-motion` 硬约束(index.css:1266),
鸿蒙这边**改造前一次都没调过** —— 系统里关了动画,我们照样动。这是无障碍义务。
· `Theme.cardMaterial` —— 卡片材质令牌。不是拍脑袋选的:
`COMPONENT_THIN` 实测卡片 252,254,254(吃掉 94% 壁纸,就是用户看到的"白卡");
`BACKGROUND_THIN` → 185,190,201,壁纸透得出来且文字对比度仍够。
★ 判定形状按"玻璃从例外变成默认"重写(判据 C 条)
原来是"允许出现玻璃的位置"白名单,逐处登记。那个形状在"玻璃是少数例外"时成立,
但用户要的是**全部玻璃化** ⇒ 白名单退化成"把每处抄一遍",且挡不住新写的裸枚举。
改成**规则**:材质档次只能来自 `Theme.navMaterial` / `Theme.cardMaterial`
(或 `BlurStyle.NONE`),页面里出现裸 `BlurStyle.XXX` 即红;
辅助方法(`this.materialOf()`)体内也查,否则等于开了后门。两条变异都验证咬得住。
★ 判据 C 条自身的两个 bug(都被本次触发)
· 嵌套判定**没比文件**:`c.at`/`o.at` 是各文件自己的字符下标,直接比大小
于是判出"MainPage 内含 SettingsPage 的玻璃"这种物理上不可能的红。
假红比漏报更坏 —— 会让人去改本来对的代码(本次差一点)。
· 参数抽取用 `[^)]*`:`this.materialOf()` 里含一层 `()`,在内层括号处截断,
把合法写法读成 `this.materialOf(` 判红。改成配对括号抽取。
★ 出现/消失动画:用户点名的三处硬弹
这些挂在 `if (cond)` 上,条件一变整块出现/消失,原来一帧过渡都没有 ——
而代码上"看着像挂了动画"(外层页面有 transition),所以最容易漏。
· `MainPage` 账号选择器下拉 → `menuIn()`(从上往下落的浮层档)
· `AdminUsersPage` 新建用户表单 → `paneRiseIn()`(就地展开档)
★ 过渡必须挂在**调用点的 Column**:`@Builder` 返回 void,链不上修饰符。
第一版写进了 `CreateForm()` 内部,是新判据抓出来的。
· `SettingsPage` 新增账号弹层 → `paneRiseIn()`(与回复/转发弹层同一挂法)
新增判据「出现/消失的那类元素真的挂了过渡」:**枚举所有条件挂载的浮层/展开块**
(不是数数 —— 数数挡不住"新增第四处又漏了"),变异验证过。
★ 顺手修的两处真嵌套玻璃
`SettingsPage` 的账号列表与密钥列表都是"卡里面的一段列表",两层都铺材质 =
WebUI 用 `.bg-white .bg-white { backdrop-filter: none }` 明确禁止的嵌套
(alpha 相乘 0.82×0.82=0.97 把壁纸吃光)。去掉内层材质,玻璃只留一层。
★ 判据放宽一处(不是放水)
`harmony-contacts` 那句写死 `pushUrl`,判的是"tryRestore 有没有跳转",
与用哪个原语无关。修 LoginPage 返回栈时改成 `replaceUrl` 后它误报了;
改成 `(pushUrl|replaceUrl)`,原语该用哪个由 `harmony-nav` 那条专门判。
判据:files=32 ran=32 checks=518 pass=518 fail=0 red=0;
baseline 7/7✓(第 6 次重算,两个文件逐个 `git diff --quiet HEAD` 取证为有意编辑)。
This commit is contained in:
@ -347,13 +347,25 @@ test('B|旧机制不得回来:手写玻璃 alpha、替系统猜深色、与
|
||||
* ① **不许嵌套**(模糊叠模糊,视觉上互相打架、性能也白花);
|
||||
* ② 每一处玻璃都要**登记**(新开一处玻璃面必须显式过一道,而不是悄悄多出来)。
|
||||
*/
|
||||
const GLASS_REGISTRY = [
|
||||
// 文件(相对 ets 根) + 组件/Builder 名:为什么这里可以有一层系统材质
|
||||
{ file: 'pages/MainPage.ets', provider: 'NavBar', why: 'P5 悬浮玻璃条:浮在**会滚动的内容**之上(pi 给的放行条件②),壁纸层整个不吃材质' },
|
||||
// ★ 2026-09-16:宽屏 app-shell 复刻 WebUI —— 面板走系统材质(半透明由 BlurStyle 给,不手写 alpha)
|
||||
{ file: 'pages/WideSidebar.ets', provider: 'SidebarItem', why: '宽屏侧栏(判据按最近的 @Builder 命名):浮在壁纸之上、背后是会滚动的导航内容(WebUI Sidebar 同形状),bgActive 才开。★ 2026-09-19 改名:原先登记的是 NavItemBuilder,但那是我上一版编造的“复刻”(参见 Theme.ets 里那段更正)—— 重写后按 WebUI 拆成导航轨 + 底部一簇,@Builder 相应改名,登记同步跟上(判据就是为此存在的)' }
|
||||
];
|
||||
|
||||
/*
|
||||
* ★★ 2026-09-19 重写(用户:「应该定义一个基础玻璃容器给各个组件引用」)。
|
||||
*
|
||||
* 这张名单**原来**是"允许出现玻璃的位置"白名单,逐处登记、附一句理由。
|
||||
* 那个形状在"玻璃是少数例外"时成立(当时全仓只有导航条一处)。
|
||||
* 但用户的要求是**全部玻璃化** —— 玻璃成了**默认**,于是白名单变成了
|
||||
* "把每一处都抄一遍"的负担,而且它挡不住真正该挡的东西:
|
||||
* 一个人**新写** `.backgroundBlurStyle(BlurStyle.COMPONENT_THICK)` 直接就过了,
|
||||
* 因为白名单只检查"登记过的位置还在不在",不检查"材质档次从哪来"。
|
||||
*
|
||||
* 换成**规则**(这才是"基础组件"该有的防线):
|
||||
* ① 材质档次**只能**来自 `Theme` 的两个令牌(`navMaterial` / `cardMaterial`),
|
||||
* 不许在页面里直接出现 `BlurStyle.XXX` —— 否则深浅两套浓度就没人统一管了;
|
||||
* ② `BlurStyle.NONE` 是**开关的另一半**(壁纸关时退回实体面),不在此限;
|
||||
* ③ 仍不许嵌套(上一段那两条断言)。
|
||||
*
|
||||
* 「枚举挡实例,类才挡漂移」—— 这条从枚举位置改成了枚举**来源**。
|
||||
*/
|
||||
const MATERIAL_TOKENS = ['Theme.navMaterial', 'Theme.cardMaterial'];
|
||||
/**
|
||||
* 往回找"这次材质作用在哪个组件块上",返回块尾 `}` 的下标(找不到返回 -1)。
|
||||
*
|
||||
@ -421,6 +433,8 @@ test('C|玻璃:位置用系统材质、不许叠、每一处都要登记(
|
||||
const owner = [...src.slice(0, at).matchAll(/(?:struct|@Builder\s+)\s*(\w+)/g)].pop();
|
||||
found.push({
|
||||
key: `${f.slice(etsRoot.length + 1)}#${owner ? owner[1] : '(未识别)'}`,
|
||||
/* 所属文件:嵌套/叠用判定必须先比它(见下面那段注释) */
|
||||
file: f.slice(etsRoot.length + 1),
|
||||
at,
|
||||
block: span ? { start: span[0], end: span[1] } : null
|
||||
});
|
||||
@ -439,6 +453,21 @@ test('C|玻璃:位置用系统材质、不许叠、每一处都要登记(
|
||||
for (const c of found) {
|
||||
for (const o of found) {
|
||||
if (o === c) continue;
|
||||
/*
|
||||
* ★★ 2026-09-19 修:先比**文件**,再比偏移。
|
||||
*
|
||||
* `c.at` / `o.at` 是**各文件自己的字符下标**,之间没有可比性。
|
||||
* 原来缺了这一层,于是 `MainPage.ets` 的偏移被拿去和 `SettingsPage.ets`
|
||||
* 的偏移比大小 —— 判出 `MainPage#GroupHeader 内含 SettingsPage#SettingsPane 的玻璃`
|
||||
* 这种**物理上不可能**的结论(一个文件不可能内含另一个文件的块)。
|
||||
*
|
||||
* 这一条正是本仓反复出现的形状:**判据报出来了、但它声称的东西是假的**。
|
||||
* 那种假红比漏报更坏 —— 会让人去改本来对的代码(本次差一点)。
|
||||
*
|
||||
* WebUI 用后代选择器天然只在**同一棵 DOM** 里成立;鸿蒙没有那个机制,
|
||||
* 所以文件边界就是它的等价物。
|
||||
*/
|
||||
if (c.file !== o.file) continue;
|
||||
if (c.block && o.block && c.block.start === o.block.start && c.block.end === o.block.end) {
|
||||
const pair = [c.at, o.at].sort((a, b) => a - b).join('-');
|
||||
doubled.add(`${c.key}@${pair}`);
|
||||
@ -450,17 +479,65 @@ test('C|玻璃:位置用系统材质、不许叠、每一处都要登记(
|
||||
assert.deepEqual([...doubled], [], `同一个组件上不该叠多层模糊:${[...doubled].join('、')}`);
|
||||
assert.deepEqual([...nested], [], `玻璃不该套在另一层玻璃的子树里:${[...nested].join('、')}`);
|
||||
|
||||
// 每一处都要登记;名单里也不能有已经不存在的位置(否则名单会烂成化石)
|
||||
const keys = found.map(c => c.key);
|
||||
const registered = GLASS_REGISTRY.map(g => `${g.file}#${g.provider}`);
|
||||
const unregistered = keys.filter(k => !registered.includes(k));
|
||||
assert.deepEqual(unregistered, [],
|
||||
`这些位置开了玻璃但没登记:${unregistered.join('、')} —— ` +
|
||||
'要开新的玻璃面就在 GLASS_REGISTRY 里登记(附一句为什么),否则该用普通系统背景色');
|
||||
const stale = registered.filter(k => !keys.includes(k));
|
||||
assert.deepEqual(stale, [], `名单里这些位置已经没有玻璃了(名单要跟着改):${stale.join('、')}`);
|
||||
for (const g of GLASS_REGISTRY) {
|
||||
assert.ok(g.why && g.why.length >= 8, `GLASS_REGISTRY 里 ${g.file}#${g.provider} 要写一句为什么可以在这里开玻璃`);
|
||||
/*
|
||||
* 材质来源检查:每个 `backgroundBlurStyle(...)` 的实参要么是设计令牌,
|
||||
* 要么是 `BlurStyle.NONE`("+关闭"的那一半)。
|
||||
*/
|
||||
const rawMaterial = [];
|
||||
for (const f of collectEts(etsRoot)) {
|
||||
const src = code(f);
|
||||
const rel = f.slice(etsRoot.length + 1);
|
||||
/*
|
||||
* 参数用"配对括号"抽取,不能用 `[^)]*` ——
|
||||
* `this.materialOf()` 里含一层 `()`,`[^)]*` 会在内层 `(` 处截断,
|
||||
* 于是把合法写法读成 `this.materialOf(` 并判红(本次就是这么被误伤了一次)。
|
||||
*/
|
||||
for (const m of src.matchAll(/\.backgroundBlurStyle\(/g)) {
|
||||
let i = m.index + m[0].length, d = 1, j = i;
|
||||
while (j < src.length && d > 0) {
|
||||
if (src[j] === '(') d++;
|
||||
else if (src[j] === ')') d--;
|
||||
j++;
|
||||
}
|
||||
const arg = src.slice(i, j - 1).trim();
|
||||
/*
|
||||
* 三种合法形态:
|
||||
* · 直接引令牌(`Theme.cardMaterial`);
|
||||
* · `BlurStyle.NONE`("关闭"的那一半);
|
||||
* · 调**基础组件内部**的取值方法(`this.materialOf()`)—— 那是设计系统
|
||||
* 把"选哪档"收敛到一处的手段,本身就该允许;但要求该方法体内
|
||||
* 只出现令牌与 `NONE`(下面单独校验),否则等于借方法绕开这条判据。
|
||||
*/
|
||||
const isHelper = /^this\.\w+\(\)$/.test(arg);
|
||||
const ok = MATERIAL_TOKENS.some(t => arg.includes(t)) || arg === 'BlurStyle.NONE' || isHelper;
|
||||
if (!ok) rawMaterial.push(`${rel}:${src.slice(0, m.index).split('\n').length} → ${arg}`);
|
||||
}
|
||||
}
|
||||
assert.deepEqual(rawMaterial, [],
|
||||
'这些地方直接写了 `BlurStyle.XXX`,没走设计令牌:' + rawMaterial.join(' | ') +
|
||||
' —— 材质档次必须来自 Theme(navMaterial / cardMaterial):' +
|
||||
'深浅两套浓度由令牌统一管,写在页面里就等于没人管(本仓为此重写过一次)');
|
||||
|
||||
/*
|
||||
* 辅助方法也要查:`this.materialOf()` 这种形态若不查它体内,
|
||||
* 就等于判据开了个后门 —— 把裸 `BlurStyle.COMPONENT_THICK` 塞进方法里照样过。
|
||||
* 这正是本仓反复出现的形状:**判据给了放行口,放行口本身没人管**。
|
||||
*/
|
||||
for (const f of collectEts(etsRoot)) {
|
||||
const src = code(f);
|
||||
const rel = f.slice(etsRoot.length + 1);
|
||||
for (const m of src.matchAll(/(\w+)\s*\(\s*\)\s*:\s*BlurStyle\s*\{([\s\S]*?)\n \}/g)) {
|
||||
const body = m[2];
|
||||
for (const b of body.matchAll(/BlurStyle\.([A-Z_]+)/g)) {
|
||||
const v = b[1];
|
||||
if (v === 'NONE') continue;
|
||||
const line = src.slice(0, m.index).split('\n').length +
|
||||
body.slice(0, b.index).split('\n').length - 1;
|
||||
assert.fail(
|
||||
`${rel}:${line} 的 ${m[1]}() 里直接写了 BlurStyle.${v} —— ` +
|
||||
'返回材质的方法体内只许引令牌或 NONE;写裸枚举等于绕开"材质来自设计令牌"这条');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导航条那一处必须真的还在(形状判定之外,位置本身也要在)
|
||||
|
||||
Reference in New Issue
Block a user