跨端: 抽出基础面/玻璃组件(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:
2026-09-20 07:37:16 +08:00
parent a7896a7e3a
commit 5103e0aee3
11 changed files with 740 additions and 46 deletions

View File

@ -426,7 +426,8 @@ struct InboxTab {
}
.width('100%').height(46)
.padding({ left: 14, right: 12 })
.backgroundColor(Theme.surface)
.backgroundColor(this.bgActive ? Color.Transparent : Theme.surface)
.backgroundBlurStyle(this.bgActive ? Theme.cardMaterial : BlurStyle.NONE)
.borderRadius(Theme.radiusCard)
.border({ width: 1, color: Theme.border })
}
@ -472,8 +473,25 @@ struct InboxTab {
}, (acct: AccountInfo) => acct.id)
}
.width('100%')
.backgroundColor(Theme.surface)
.backgroundColor(this.bgActive ? Color.Transparent : Theme.surface)
.backgroundBlurStyle(this.bgActive ? Theme.cardMaterial : BlurStyle.NONE)
.border({ width: { bottom: 1 }, color: Theme.border })
/*
* ★ 2026-09-19 补(用户:「元素的出现消失动画呢?」)。
*
* 这个下拉原来是**硬弹**出来的:它挂在 `if (this.showAccountPicker)` 上,
* 条件一变就整块出现/消失,中间一帧过渡都没有 —— 而它正是
* "出现/消失"最典型的一处(用户点一下按钮,东西凭空冒出来)。
*
* 用 `menuIn()`:下移 4vp + 缩到 0.985 + 淡入,与 WebUI
* `@keyframes menu-in` 逐值对齐(`Theme.menuIn` 的注释里有出处)。
* 方向是**从上往下**(`translateY(-4px)` → 0)—— 下拉从触发点下方展开,
* 从上方"落"下来;与 `paneRiseIn` 的**上浮**方向相反,别混。
*
* `.transition()` 在这里**会播**(与日历那个常驻窗格不同):
* 它是 `if` 包着的,条件为真时才挂载 —— 正是 transition 的触发条件。
*/
.transition(Theme.menuIn())
}
if (this.loading) {
@ -609,7 +627,8 @@ struct InboxTab {
.width('100%').height(78)
.padding({ left: 10, right: 12 })
.alignItems(VerticalAlign.Center)
.backgroundColor(Theme.surface)
.backgroundColor(this.bgActive ? Color.Transparent : Theme.surface)
.backgroundBlurStyle(this.bgActive ? Theme.cardMaterial : BlurStyle.NONE)
.borderRadius(Theme.radiusCard)
.border({ width: 1, color: Theme.border })
.clip(true)
@ -933,7 +952,8 @@ struct SentTab {
this.SentRow(m)
}
.width('100%')
.backgroundColor(Theme.surface)
.backgroundColor(this.bgActive ? Color.Transparent : Theme.surface)
.backgroundBlurStyle(this.bgActive ? Theme.cardMaterial : BlurStyle.NONE)
.borderRadius(Theme.radiusCard)
.margin({ bottom: 6 })
.onClick(() => { this.openMail(m); })
@ -1135,7 +1155,8 @@ struct PermissionTab {
}
.width('100%').alignItems(HorizontalAlign.Start)
.padding(12)
.backgroundColor(Theme.surface)
.backgroundColor(this.bgActive ? Color.Transparent : Theme.surface)
.backgroundBlurStyle(this.bgActive ? Theme.cardMaterial : BlurStyle.NONE)
.borderRadius(Theme.radiusCard)
.margin({ bottom: 8 })
}
@ -1462,6 +1483,26 @@ struct CommPage {
}
}
.width('100%').height('100%')
/*
* ★★ 2026-09-19 修(用户:「你这玻璃也不透明啊」)。
*
* 这一层是 `Navigation` 的 **navBar 内容**(列表那一栏的根容器),
* 原来**没有背景** ⇒ 由系统给的 `NavBar` 外壳显出不透明白。实测:
*
* NavBar [229,140,1149,2204] ← 255,255,255(整条列表栏)
* 右栏同高位置 ← 187,190,191(**壁纸透出来了**)
*
* 外层 `Navigation` 的壳修好之后,列表栏自己还盖着一块白。
* 内外两处都要透明,链路才通(Navigation → navBar 内容 → 卡片)。
*
* ★ 这里**不加**材质:WebUI 的同位置规则(`index.css:1641`)给列表容器
* 恰恰是 `background-color: transparent`,注释写明
* 「面板退成透明,**每一项自己是一张玻璃卡**」。
* 玻璃在**卡片**那一层,容器只负责让位 —— 若这里也铺材质,
* 卡片就成了"玻璃套玻璃",正是 WebUI 用
* `.bg-white .bg-white { backdrop-filter: none }` 明确禁止的事。
*/
.backgroundColor(this.bgActive ? Color.Transparent : Theme.pageBg)
/*
* 悬浮的圆形加号(用户点名要的形状):56 圆、品牌色、右下角。
@ -1495,6 +1536,20 @@ struct CommPage {
.minContentWidth(360)
.hideTitleBar(true)
.width('100%').height('100%')
/*
* ★★ 2026-09-19 修(用户:「你这玻璃也不透明啊」)。
*
* 这一行原来**没有** —— `Navigation` 不设背景时用系统默认(不透明白),
* 于是它把里面已经透明的窗格整片盖住了。实测(模拟器 3184×2232):
*
* Navigation [229,140,3156,2204] ← 255,255,255(整块内容区)
* y=2210(Navigation 之外那条缝) ← 226,225,235(**壁纸清楚可见**)
*
* 那条缝是决定性的证据:**壁纸层本身是好的**,白是因为它上面盖了
* 一个不透明的 `Navigation`。此前一直在调窗格自己的 `bgActive`,
* 而真正挡住的是外面这层壳。
*/
.backgroundColor(this.bgActive ? Color.Transparent : Theme.pageBg)
}
}
@ -1831,7 +1886,8 @@ struct ContactsTab {
* 不要把一个"按当时字号的估算值"钉成常量 ——
* 字号、行高、动作行的存在与否都会变,而常量不会跟着变。
*/
.backgroundColor(Theme.surface)
.backgroundColor(this.bgActive ? Color.Transparent : Theme.surface)
.backgroundBlurStyle(this.bgActive ? Theme.cardMaterial : BlurStyle.NONE)
.borderRadius(Theme.radiusCard)
.clip(true)
/* 点卡片 = 打开这条会话(WebUI `ContactPanel` 的 onOpen) */
@ -1860,6 +1916,9 @@ struct ContactsTab {
.minContentWidth(360)
.hideTitleBar(true)
.width('100%').height('100%')
/* 同 CommPage 的 Navigation:不设背景时系统默认不透明白,会把里面
已经透明的窗格盖住。联系人页是同一形状,同一修法。 */
.backgroundColor(this.bgActive ? Color.Transparent : Theme.pageBg)
}
/**
@ -1921,7 +1980,8 @@ struct ContactsTab {
}
.width('100%').alignItems(HorizontalAlign.Start)
.padding({ left: 12, right: 12, top: 10, bottom: 10 })
.backgroundColor(Theme.surface)
.backgroundColor(this.bgActive ? Color.Transparent : Theme.surface)
.backgroundBlurStyle(this.bgActive ? Theme.cardMaterial : BlurStyle.NONE)
.borderRadius(Theme.radiusCard)
.border({ width: 1, color: Theme.border })
}