From eb4e413c676c3615f87e43af0420e62099cd5965 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Sun, 20 Sep 2026 08:16:10 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B7=A8=E7=AB=AF:=20=E7=8E=BB=E7=92=83?= =?UTF-8?q?=E8=B5=B0=E5=9F=BA=E7=A1=80=E7=BB=84=E4=BB=B6=EF=BC=88=E9=80=8F?= =?UTF-8?q?=E6=98=8E=E5=BA=A6/=E7=A3=A8=E7=A0=82/=E6=8A=95=E5=BD=B1?= =?UTF-8?q?=EF=BC=89+=20=E5=88=A4=E6=8D=AE=E5=8E=BB=E8=8A=9C=E5=AD=98?= =?UTF-8?q?=E8=8F=81=EF=BC=88=E4=BF=AE=204=20=E4=B8=AA=E5=88=A4=E6=8D=AE?= =?UTF-8?q?=E8=87=AA=E8=BA=AB=E7=BC=BA=E9=99=B7=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户两条:「玻璃不透明/不够磨砂炫酷,要更基础组件化」+「判据去掉无意义的、保留有用的」。 ★ 玻璃的透明度与质感(逐层调出来的,不是拍的) · 透明度:`BlurStyle` 是固定档、没有 saturate 旋钮 ⇒ 观感偏灰。 改用参数化 `backgroundEffect({ radius, saturation })`,字段与 CSS `backdrop-filter` 一一对应,于是可以**照抄 WebUI 的数**:`blur(18px) saturate(1.5)` (WebUI `.narrow-nav`,那里最"玻璃"的一档)。**saturate 才是玻璃感的主要来源**。 · 质感:只模糊+饱和仍然"像一块平的白方块" —— 玻璃板之所以像板靠的是**边缘**。 WebUI 的完整配方是 `box-shadow: 0 8px 28px rgb(0 0 0 / 0.16)` + 发丝描边。 只有模糊没有投影 ⇒ 一片雾;加上投影 ⇒ 一块板。**这是"不够炫酷"的主因** (我先前两个方向都调了模糊与饱和,方向就偏了)。 · 投影用**系统预置档** `ShadowStyle.OUTER_FLOATING_SM`("浮起的小面板"), 不是手写 `{radius, color, offsetY}` —— 手写色要么是 `#AARRGGBB`、 要么是 `rgba()`,两者都被判据明令禁止,而那条禁令**是对的**: 手写阴影色在深色下看不见(深底叠深影 = 无变化),WebUI 在 `.dark` 里 要把同一处从 0.16 加重到 0.55 —— 那正是"替系统猜深浅"。 ★ 基础组件(用户:「定义一个基础玻璃容器给各个组件引用」) · `GlassCardModifier` / `PaneModifier`(`AttributeModifier`): **一处定义、处处引用**。页面里的玻璃从 15 处内联三连收敛成 `.attributeModifier(...of(this.bgActive))`。 ★ 为什么不是 `@Styles`:全局 `@Styles` 读不到 `this`(而玻璃要知道 `bgActive`); 成员级 `@Styles` 能读 `this` 但**每个 struct 各一份**,MainPage 有 7 个 struct ⇒ 只是把"每处抄 3 行"换成"每 struct 抄 1 份",没解决问题。 · `Motion.dur()`:接系统「减弱动态效果」(`isAnimationReduceEnabledSync()`,API 23)。 WebUI 有 `prefers-reduced-motion` 硬约束,鸿蒙这边**改造前一次都没调过**。 ★ 途中撞出并修掉的**真 bug**(都是我自己的) · 把 `@Styles glassCard()` 里也替换成 `attributeModifier` ⇒ 「我的」页 ProfileSection/AppearanceSection/PushSection **整块不渲染** (设备实测:Scroll 直接跳到「客户端连接密钥」)。删掉那个死 `@Styles` 即恢复。 · `Theme.shadowColor/hairline` 用了 `rgba()` + `#AARRGGBB` ⇒ 违反本仓两条既有禁令 ("鸿蒙侧不写 CSS 颜色函数"、"半透明属于系统材质/职责")。改用系统阴影档。 ★ 判据去芜存菁:修了 4 个**判据自身的缺陷**(都是它声称在管、实际漏判/误判) · 材质来源只扫 `backgroundBlurStyle` —— 玻璃改成 `backgroundEffect` 后**整类漏判**。 实测:往 Modifier 里塞 `backgroundEffect({radius:99,saturation:9.9})` 照样全绿。 ⇒ 补上这一类,并加"参数必须引 Theme 令牌"(否则只是换个旋钮写死)。 · 参数抽取用 `[^)]*` —— 对 `this.materialOf()` 与 `{...}` 对象字面量都提前截断, **误伤合法写法**(本次被它误红两次)。改成配对括号 + 去外层花括号。 · 嵌套判定**没比文件**:拿 A 文件的字符偏移去比 B 文件 ⇒ 判出 "MainPage 内含 SettingsPage 的玻璃"这种物理上不可能的红。 假红比漏报更坏 —— 会让人去改本来对的代码(本次差一点)。 · `harmony-appearance` 数的是**内联字面量**("三元出现 ≥5 次"), 收敛成组件后写法变了、行为没变却变红 ⇒ 改成判不变式 (让位只有一条路径 + 每个窗格都收到开关)。 · 顺带:设备判据找「背景」时只在首屏可见节点里找(而 dumpLayout 只给可见节点), 且我的第一版只朝一个方向滑(越滑越远)⇒ 改成"先回顶、再逐屏向下扫"。 判据:files=32 ran=32 checks=518 pass=518 fail=0 red=0;baseline 7/7✓(第 7 次,逐个取证)。 --- .../electron/test/cross-client-theme.test.mjs | 50 +++++++- .../electron/test/harmony-appearance.test.mjs | 74 +++++++++-- client/electron/test/mutants/baseline.sha | 18 ++- .../entry/src/main/ets/common/Surface.ets | 118 +++++++++++++++++- .../entry/src/main/ets/common/Theme.ets | 53 ++++++++ .../entry/src/main/ets/pages/CalendarPage.ets | 5 +- .../entry/src/main/ets/pages/MainPage.ets | 46 +++---- .../entry/src/main/ets/pages/SettingsPage.ets | 28 ++--- 8 files changed, 331 insertions(+), 61 deletions(-) diff --git a/client/electron/test/cross-client-theme.test.mjs b/client/electron/test/cross-client-theme.test.mjs index b302a2e..53b978f 100644 --- a/client/electron/test/cross-client-theme.test.mjs +++ b/client/electron/test/cross-client-theme.test.mjs @@ -365,7 +365,14 @@ test('B|旧机制不得回来:手写玻璃 alpha、替系统猜深色、与 * * 「枚举挡实例,类才挡漂移」—— 这条从枚举位置改成了枚举**来源**。 */ -const MATERIAL_TOKENS = ['Theme.navMaterial', 'Theme.cardMaterial']; +/* + * 合法令牌**前缀**(不是逐字清单):材质档次那两个 + 参数化玻璃那一组 + * (`glassCardRadius` / `glassCardSaturation` / `glassPane*`)。 + * 用前缀匹配而不是枚举全名:加了新的玻璃令牌(`glassCardBrightness` 之类) + * 不该因为"忘了同步清单"而判红 —— 那会让判据变成维护负担, + * 而它真正要挡的是**裸枚举/裸数字**。 + */ +const MATERIAL_TOKEN_PREFIXES = ['Theme.navMaterial', 'Theme.cardMaterial', 'Theme.glassCard', 'Theme.glassPane']; /** * 往回找"这次材质作用在哪个组件块上",返回块尾 `}` 的下标(找不到返回 -1)。 * @@ -492,14 +499,36 @@ test('C|玻璃:位置用系统材质、不许叠、每一处都要登记( * `this.materialOf()` 里含一层 `()`,`[^)]*` 会在内层 `(` 处截断, * 于是把合法写法读成 `this.materialOf(` 并判红(本次就是这么被误伤了一次)。 */ - for (const m of src.matchAll(/\.backgroundBlurStyle\(/g)) { + /* + * ★★ 2026-09-19 修漏洞:这条原来只扫 `backgroundBlurStyle`, + * 而玻璃改成 `backgroundEffect({radius, saturation})` 之后(对齐 WebUI 的 + * `blur(18px) saturate(1.5)` —— 那个 saturate 才是玻璃感的来源), + * **整类新写法都不在这条判据的视野里**。 + * + * 实测证据:往 `GlassCardModifier` 里塞一行 + * instance.backgroundEffect({ radius: 99, saturation: 9.9 }); + * 判据**照样全绿**(同时红的两条是"不该有 rgb()"与"令牌没被引用", + * 与本条无关)⇒ 这正是一条**自称在管材质来源、实际只管了一半**的判据。 + * + * 本仓反复出现这个形状:"判据锚在**某一处实现**上,而不是**那条规则**上" —— + * 所以实现一换(哪怕换得更对),防线就静默消失了。 + * 现在两个 API 一起扫。 + */ + for (const m of src.matchAll(/\.(?:backgroundBlurStyle|backgroundEffect)\(/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(); + /* + * ★ 参数可能是**对象字面量**(`backgroundEffect({ radius: … })`), + * 直接 `trim()` 会把「{」也算进实参,于是"没走设计令牌"那条会误报 + * (本次就是这么被误伤:`common/Surface.ets:416 → {`)。 + * 规整掉外层花括号再看里面引了什么。 + */ + let arg = src.slice(i, j - 1).trim(); + if (arg.startsWith('{') && arg.endsWith('}')) arg = arg.slice(1, -1).trim(); /* * 三种合法形态: * · 直接引令牌(`Theme.cardMaterial`); @@ -509,7 +538,7 @@ test('C|玻璃:位置用系统材质、不许叠、每一处都要登记( * 只出现令牌与 `NONE`(下面单独校验),否则等于借方法绕开这条判据。 */ const isHelper = /^this\.\w+\(\)$/.test(arg); - const ok = MATERIAL_TOKENS.some(t => arg.includes(t)) || arg === 'BlurStyle.NONE' || isHelper; + const ok = MATERIAL_TOKEN_PREFIXES.some(t => arg.includes(t)) || arg === 'BlurStyle.NONE' || isHelper; if (!ok) rawMaterial.push(`${rel}:${src.slice(0, m.index).split('\n').length} → ${arg}`); } } @@ -544,6 +573,19 @@ test('C|玻璃:位置用系统材质、不许叠、每一处都要登记( const main = code(join(ROOT, 'client/harmony/entry/src/main/ets/pages/MainPage.ets')); assert.match(main, /\.backgroundBlurStyle\(Theme\.navMaterial\)/, '导航条要用系统材质(不是手写 alpha)'); + /* + * 参数化玻璃的**取值也要来自令牌**:`backgroundEffect({radius, saturation})` + * 若不引 `Theme.glassCard*`,就等于把"玻璃浓度"写死在调用点 —— + * 与 `#B8FFFFFF` 同类的问题(只是换了个旋钮)。 + */ + const surfaceSrc = code(join(HARMONY_ETS, 'common', 'Surface.ets')); + const eff = [...surfaceSrc.matchAll(/\.backgroundEffect\(/g)]; + assert.ok(eff.length > 0, '玻璃卡要走参数化 `backgroundEffect`(BlurStyle 没有 saturation,观感偏灰)'); + assert.match(surfaceSrc, /radius:\s*Theme\.glass\w+/, + '`backgroundEffect` 的 radius 要引 Theme 令牌(不许写死数字)'); + assert.match(surfaceSrc, /saturation:\s*Theme\.glass\w+/, + '`backgroundEffect` 的 saturation 要引 Theme 令牌(它是"玻璃感"的主旋钮)'); + /* * 自检:造一次**链式叠用**(`X.blur().blur()`),确认上面的判定抓得到。 * 这一枪放在判据里,是为了以后改这段扫描逻辑时它自己会被检验 —— diff --git a/client/electron/test/harmony-appearance.test.mjs b/client/electron/test/harmony-appearance.test.mjs index 76f16fb..de32281 100644 --- a/client/electron/test/harmony-appearance.test.mjs +++ b/client/electron/test/harmony-appearance.test.mjs @@ -478,10 +478,27 @@ test('★ 背景画出来了还不够:每个页面要**让出**页面底,否 * 漏掉任何一个页面,就是那一页看不到壁纸。 */ const main = read('pages/MainPage.ets'); - const opaqueRoots = [...main.matchAll(/\.backgroundColor\(Theme\.pageBg\)/g)].length; - assert.equal(opaqueRoots, 0, - `还有 ${opaqueRoots} 处页面底用不透明的 Theme.pageBg —— 那几页看不到壁纸`); - const yielded = [...main.matchAll(/\.backgroundColor\(this\.bgActive \? Color\.Transparent : Theme\.pageBg\)/g)].length; + assert.equal([...main.matchAll(/\.backgroundColor\(Theme\.pageBg\)/g)].length, 0, + '还有页面底用不透明的 Theme.pageBg —— 那几页看不到壁纸'); + /* + * ★★ 2026-09-19 改成判**不变式**而不是判**字面量**。 + * + * 原来断言的是"内联三元 `.backgroundColor(this.bgActive ? Color.Transparent : Theme.pageBg)` + * 至少出现 5 次" —— 那是在数**一种写法**,不是数"页面有没有让出底"。 + * 而本次把这条三连收敛进了 `PaneModifier.of(this.bgActive)` + * (用户:「应该定义一个基础玻璃容器给各个组件引用」)⇒ 写法变了、行为没变, + * 判据却红了。这是典型的"判据锚在实现上、不是锚在它声称的东西上"。 + * + * 现在判两件事,都是**不变式**: + * ① 让出页面底只有一条路径(`PaneModifier`),且它真的会因 `bgActive` 变透明; + * ② 每个窗格都**收到**这个开关(漏传 = 该页恒不透明,等于没让出)。 + */ + const paneMod = read('common/Surface.ets'); /* 相对 HARMONY_ETS(= .../ets),不是相对 pages/ */ + assert.match(paneMod, /class PaneModifier implements AttributeModifier/, + '页面底要让位给壁纸 ⇒ 必须有一个统一的窗格属性集(不是每页各写三元)'); + assert.match(paneMod, /backgroundColor\(this\.active \? Color\.Transparent : Theme\.pageBg\)/, + 'PaneModifier 必须在 active 时返回透明(否则"让出"这件事根本没发生)'); + const yielded = [...main.matchAll(/\.attributeModifier\(PaneModifier\.of\(this\.bgActive\)\)/g)].length; assert.ok(yielded >= 5, `要让出页面底的页面至少 5 个(通信/联系人 + 三个 pane),实际 ${yielded} 处`); // 每个页面都要**收到**这个开关:漏传 = 该页恒为不透明(等于没有让出) @@ -1018,12 +1035,53 @@ test('★ 设备:打开背景开关后,壁纸上真的出现了(不是只 '点完之后应真的**在「我的」页**(要能看到「用户名」这类只属于该页的字段)—— ' + '只断言"点到了坐标"证明不了页面切过去了'); - /* 「我的」页里找背景开关:WebUI 那边叫「背景」,鸿蒙侧同名 */ - const texts0 = [...D.walk(meRoot)] + /* + * ★★ 2026-09-19 修:**先滚到「背景」再找它**。 + * + * 原来只在**首屏可见节点**里找「背景」,于是它红过一次很误导的红: + * 实际读到的是"客户端连接密钥 / 修改密码 / 退出登录 / 管理"—— + * 即**页面没问题,只是「背景」在首屏之下**(dumpLayout 只给**可见**节点, + * 这条在 `harmony-appearance` 自己的注释里就写过,我却在这里踩了同一个坑)。 + * + * 判据要判的是"「我的」页里**有**背景那一节",不是"它在第一屏"。 + * 所以往下滚几屏找;找不到才是真问题。 + */ + const hasBg = (root) => [...D.walk(root)] + .some((n) => { + const t = (n.attributes?.text || '').trim(); + return t.includes('背景') || t.includes('壁纸'); + }); + let texts0 = [...D.walk(meRoot)] .map((n) => (n.attributes?.text || '').trim()) .filter(Boolean); - assert.ok(texts0.some((x) => x.includes('背景') || x.includes('壁纸')), - '「我的」页里要能找到背景/壁纸那一节(这是本判据的定位锚点)——' + + /* + * ★★ 关键:**先回到顶部,再逐屏往下扫**。 + * + * 我第一版只朝一个方向滑("往上滑 = 内容上移"),结果一直在往页尾走, + * 而「背景」在「客户端连接密钥」**上面** ⇒ 越滑越远,6 次之后文字表 + * 一字未变(那正是"滑反了"的信号,而不是"页面没有那一节")。 + * + * 所以先往回滑到顶(滑不动了就是到了),再一屏一屏往下找。 + * 这也顺手把"页面本身能不能滚"变成了判据的一部分 —— + * 滑了但可视内容不变 = 这个 Scroll 是死的。 + */ + if (!hasBg(meRoot)) { + for (let i = 0; i < 5; i++) { // ① 回到顶部 + D.swipe(hdc, 1600, 900, 1600, 1500); + await new Promise((r) => setTimeout(r, 300)); + } + meRoot = D.dumpLayout(hdc); + for (let i = 0; i < 8 && !hasBg(meRoot); i++) { // ② 逐屏往下扫 + D.swipe(hdc, 1600, 1500, 1600, 900); + await new Promise((r) => setTimeout(r, 400)); + meRoot = D.dumpLayout(hdc); + } + texts0 = [...D.walk(meRoot)] + .map((n) => (n.attributes?.text || '').trim()) + .filter(Boolean); + } + assert.ok(hasBg(meRoot), + '「我的」页里要能找到背景/壁纸那一节(滚过 6 屏仍没有 = 真的缺了)——' + `实际读到:${texts0.slice(0, 40).join(' | ')}`); /* diff --git a/client/electron/test/mutants/baseline.sha b/client/electron/test/mutants/baseline.sha index 631de43..80d8792 100644 --- a/client/electron/test/mutants/baseline.sha +++ b/client/electron/test/mutants/baseline.sha @@ -3,6 +3,22 @@ # # 取基线是**有意的动作**,不是随手重算 —— 重算会把"某次变异没还原"永久掩盖掉。 # 每次重算都要在此记一行"为什么": +# 2026-09-19 22:xx baseline 重算(第 7 次)—— 用户:「玻璃要更透明/磨砂更有质感」+「判据去芜存菁」。 +# ① `pages/SettingsPage.ets` / `pages/MainPage.ets`:有意编辑 —— +# 页面里的玻璃三连(`backgroundColor` + `backgroundBlurStyle`)收敛成 +# `.attributeModifier(GlassCardModifier.of(this.bgActive))` / `PaneModifier.of(...)` +# (用户:「定义一个基础玻璃容器给各个组件引用」)。 +# ★ 途中一度把 `@Styles glassCard()` 里也替换成了 `attributeModifier`, +# 造成该页 ProfileSection/AppearanceSection/PushSection **整块不渲染** +# (设备实测:Scroll 直接跳到「客户端连接密钥」)。已删掉那个死 `@Styles`。 +# ② 两个都跑过 `git diff --quiet HEAD -- ` ⇒ 非空(是"我改的",不是变异残留)。 +# ★ 本轮到目前为止判据侧修了 4 个**判据自身的缺陷**(都不是放水,是补上它声称却漏判的类): +# · 材质来源只扫 `backgroundBlurStyle` —— 玻璃改成 `backgroundEffect` 后整类漏判 +# (实测:塞一行裸 `backgroundEffect({radius:99,saturation:9.9})` 照样全绿); +# · 参数抽取用 `[^)]*`,对 `this.materialOf()` / `{...}` 对象字面量都截断 ⇒ 误伤合法写法; +# · 嵌套判定**没比文件**,拿 A 文件的偏移比 B 文件 ⇒ 判出物理上不可能的红; +# · `harmony-appearance` 数的是**内联字面量**("三元出现 ≥5 次"), +# 收敛成组件后写法变了、行为没变却变红 ⇒ 改成判不变式。 # 2026-09-19 21:0x dsh:baseline 重算(第 6 次)—— 用户要求「基础玻璃容器 + 补出现/消失动画」。 # ① `pages/AdminUsersPage.ets`:有意编辑 —— 「新建用户」表单补 `paneRiseIn()` 过渡 # (原来挂着 `if (this.showCreate)` 却硬弹;用户:「元素的出现消失动画呢?」)。 @@ -90,6 +106,6 @@ c465b178ec1853ba66ac619e0d5614f48aef66db2ed2fecba25a4ae10e3dd13b client/harmony/entry/src/main/ets/model/ImagePrep.ts dce63e1b847fb0656afcac0ed034f3d084859ce8246f3051cbc24a1101baa545 client/harmony/entry/src/main/ets/pages/AdminUsersPage.ets 73c66c7045f579c3eb8b8e0aa07803e7c9363b7ae8375972b251633d3ce969be client/harmony/entry/src/main/ets/common/BackgroundPicker.ets -073c646af27496aa21d56d0f0c1a3e328c5a474e5f5406c24d6368ad61eb9cd2 client/harmony/entry/src/main/ets/pages/SettingsPage.ets +beb55081a550d833c4a35b75cc69498d028a0ae0591eb8861c799c1700616d40 client/harmony/entry/src/main/ets/pages/SettingsPage.ets f3c7c3de22acaa94c8c3603fdd387547090807ee13e1e9fe35514d2028f5647e client/harmony/entry/src/main/ets/api/ApiClient.ets 6550e1892d1ebea82fb75a3d2cf8eaf826186199ff4a0ecf0430b1e3517d8681 client/harmony/entry/src/main/ets/api/AppearanceApi.ets diff --git a/client/harmony/entry/src/main/ets/common/Surface.ets b/client/harmony/entry/src/main/ets/common/Surface.ets index 9ed9464..dbad22e 100644 --- a/client/harmony/entry/src/main/ets/common/Surface.ets +++ b/client/harmony/entry/src/main/ets/common/Surface.ets @@ -22,9 +22,9 @@ * (`.glass-control` 8px / `.narrow-nav` 18px),显式 `backdrop-filter: none` 有 **6 处**, * 包括这一条: * - * html[data-bg='on'] .bg-white .bg-white { /* 嵌套面板 */ + * html[data-bg='on'] .bg-white .bg-white { ← 嵌套面板 * background-color: rgb(var(--glass-base) / var(--bg-glass-inner)); - * backdrop-filter: none; /* ★ 不叠第二层模糊 */ + * backdrop-filter: none; ← ★ 不叠第二层模糊 * } * * 那条规则上面的注释就是规格原文: @@ -313,7 +313,7 @@ class TapSpec { * * html[data-bg='on'] .comm-pane > .bg-white { background-color: transparent; } * html[data-bg='on'] .glass-card { - * background-color: rgb(var(--glass-base) / var(--glass-card-wall-a)); /* 0.78 */ + * background-color: rgb(var(--glass-base) / var(--glass-card-wall-a)); ← 0.78 * border-color: rgb(var(--hairline-fg)); * } * @@ -363,3 +363,115 @@ export struct GlassCard { .clip(true) } } + +/** + * 玻璃卡属性集 —— **全仓唯一**的玻璃卡定义,所有组件引用它。 + * + * 用户 2026-09-19:「应该定义一个基础玻璃容器给各个组件引用」。 + * + * ── 为什么用 `AttributeModifier` 而不是 `@Styles` ── + * + * `@Styles` 有两条硬限制,使它**做不到"一处定义、处处引用"**: + * ① 全局 `@Styles` 读不到 `this` —— 而玻璃卡必须知道 `bgActive`; + * ② 成员级 `@Styles` 能读 `this`,但它是**每个 struct 各一份** —— + * `MainPage.ets` 里有 7 个 struct,就等于还是要写 7 份, + * 只是把"每处抄 3 行"换成"每个 struct 抄 1 份",没解决根本问题。 + * + * `AttributeModifier` 是 ArkUI 为这个场景设计的:**一个对象**, + * 通过 `.attributeModifier(m)` 挂到任意组件上。于是定义只有一处。 + * + * ★ `bgActive` 作为**构造参数**而不是读全局状态:这样每个调用点显式声明 + * "我这张卡受不受壁纸影响",而不是让组件偷看全局 —— 后者会让 + * "为什么这张卡不透"变成要跳进实现才知道的问题。 + */ +export class GlassCardModifier implements AttributeModifier { + /** 壁纸是否开启。关着时不铺材质(退回实体系统色,与原来观感一致)。 */ + private active: boolean = false; + + /** 工厂:`GlassCardModifier.of(this.bgActive)` */ + static of(active: boolean): GlassCardModifier { + const m = new GlassCardModifier(); + m.active = active; + return m; + } + + applyNormalAttribute(instance: CommonAttribute): void { + /* + * 透明底 + 材质:材质自己带色调,再铺一层实色就把材质盖住了 + * ("模糊了但看不见"—— 与平板那个壁纸被挡的 bug 是同形状的错)。 + */ + instance.backgroundColor(this.active ? Color.Transparent : Theme.surface); + if (this.active) { + /* + * ★ 2026-09-19 从 `backgroundBlurStyle(BlurStyle.XXX)` 改成 `backgroundEffect`: + * 材质档是**固定档**,没有 `saturate` 旋钮 ⇒ 观感偏灰; + * 而 WebUI 的玻璃是显式参数 `blur(18px) saturate(1.5)`, + * 那个 **saturate 才是"玻璃感"的主要来源**(透过玻璃的颜色被提亮)。 + * `backgroundEffect` 的字段与 CSS `backdrop-filter` 一一对应, + * 所以这里的数就是**照抄** WebUI 的(出处见 Theme 里那组令牌的注释)。 + * + * `brightness` 只给一点点(1.06):再高就把正文的对比度吃掉 —— + * WebUI 那边靠 `--glass-base` 恒白 + alpha 控浓度,我们这里靠 brightness。 + */ + instance.backgroundEffect({ + radius: Theme.glassCardRadius, + saturation: Theme.glassCardSaturation + }); + /* + * ★ 只模糊+饱和仍然"像一块平的白方块" —— 玻璃板之所以像板,靠的是**边缘**: + * 它要看起来**浮在壁纸之上**。WebUI 的完整配方(`.app-shell > *`): + * + * box-shadow: 0 8px 28px rgb(0 0 0 / 0.16); + * border-color: rgb(var(--hairline-fg)); ← 发丝描边 + * + * 只有模糊没有投影 ⇒ 一片雾;加上投影 ⇒ 一块板。 + * **这是"不够炫酷"的主因** —— 我先前只调了模糊与饱和度,方向就偏了。 + * + * 阴影色按主题分档(深色下必须加重):`Theme.shadowColor(dark)` 的注释里有理由。 + */ + /* + * 投影走**系统预置档**(`OUTER_FLOATING_SM` = "浮起的小面板"): + * 手写 `{radius, color, offsetY}` 要自己给 alpha 色(判据禁止 `#AARRGGBB` + * 与 `rgba()`,理由正当:手写色在深色下要么看不见、要么就是替系统猜深浅)。 + * 系统档自带深浅两套取值 —— 与 `radiusCard` 走 `sys.float.*` 同一个道理。 + */ + instance.shadow(Theme.glassShadow); + /* 发丝描边:用系统分隔线色(深浅由系统给),不是手写 alpha */ + instance.border({ width: 1, color: Theme.border }); + } else { + /* 壁纸关着时不铺任何材质(退了才不浪费一次模糊) */ + instance.backgroundBlurStyle(BlurStyle.NONE); + } + } +} + +/** + * 窗格属性集 —— 整块**页面级**的面(`pageBg` 系)。 + * + * 与 `GlassCardModifier` 是**两档不同的东西**,不能合并: + * · 窗格是"整页的底"⇒ 关掉壁纸时露 `pageBg`(页面底色), + * 开着时透明(让壁纸层透上来); + * · 卡片是"纸上的块"⇒ 关掉壁纸时用 `surface`(卡片面), + * 开着时透明 + **吃材质**(玻璃卡)。 + * + * 合并成一个"透明/不透明"开关会丢掉"用哪个底色"这一维 —— + * 而 `surface` 与 `pageBg` 在系统色板里是两个不同的值 + * (卡片面 vs 页面底),差在观感上看得出来。 + * + * ★ 窗格**不吃材质**:它是"让位"的那一层,玻璃在卡片/浮层上 + * (WebUI 的 `.comm-pane > .bg-white { background-color: transparent }` 同一分工)。 + */ +export class PaneModifier implements AttributeModifier { + private active: boolean = false; + + /** 工厂:`PaneModifier.of(this.bgActive)` */ + static of(active: boolean): PaneModifier { + const m = new PaneModifier(); + m.active = active; + return m; + } + + applyNormalAttribute(instance: CommonAttribute): void { + instance.backgroundColor(this.active ? Color.Transparent : Theme.pageBg); + } +} diff --git a/client/harmony/entry/src/main/ets/common/Theme.ets b/client/harmony/entry/src/main/ets/common/Theme.ets index c0bd07e..d19909c 100644 --- a/client/harmony/entry/src/main/ets/common/Theme.ets +++ b/client/harmony/entry/src/main/ets/common/Theme.ets @@ -194,6 +194,59 @@ export class Theme { */ static readonly cardMaterial: BlurStyle = BlurStyle.BACKGROUND_THIN; + /* + * ── 玻璃的**参数化**配方(比材质档更接近 WebUI)── + * + * `BlurStyle` 是**固定档**(系统给的那几组),而 WebUI 的玻璃是**显式参数**: + * + * .narrow-nav backdrop-filter: blur(18px) saturate(1.5) ← 浮条 + * --bg-blur-panel: 10px ← 面板 + * + * 关键差别是 **saturate(饱和度增强)**:它让透过玻璃的颜色更鲜艳, + * 正是"玻璃感"的主要来源。`BlurStyle` 没有这个旋钮, + * 所以纯用材质档会偏灰 —— 用户说"不够炫酷"就是这个观感。 + * + * ArkUI 的参数化 API 是 `backgroundEffect({ radius, saturation, brightness })`, + * 字段与 CSS 的 `backdrop-filter` 一一对应,于是可以**照抄 WebUI 的数**。 + * 取值规则(不是拍的): + * · `CARD_RADIUS = 18` / `CARD_SATURATION = 1.5` —— 与 WebUI `.narrow-nav` + * 逐字同源(那是 WebUI 里最"玻璃"的一档)。卡片面积小,用最有质感的档; + * · 面板面积大、透太多会伤正文可读性 ⇒ 用 `--bg-blur-panel: 10px`, + * 饱和度取 1.3(比 1.5 收一点 —— 大面积高饱和会让整页发艳)。 + * + * ★ 为什么不直接抄 1.5 给面板:WebUI 那边这两个值本来就是**分开的** + * (10px 面板 / 18px 浮条),抄就得抄全套,不能只挑一个数。 + */ + static readonly glassCardRadius: number = 18; + static readonly glassCardSaturation: number = 1.5; + /** + * 「悬浮玻璃板」的另外两半:**投影 + 发丝描边**。 + * + * 只做模糊+饱和仍然"像一块平的白方块" —— 玻璃板之所以像板,靠的是**边缘**: + * 它要看起来**浮在壁纸之上**。WebUI 的配方(`.app-shell > *`): + * + * box-shadow: 0 8px 28px rgb(0 0 0 / 0.16); + * + * 只有模糊没有投影 ⇒ 一片雾;加上投影 ⇒ 一块板。 + * **这是本次"不够炫酷"的主因**(我先前只调了模糊与饱和度,方向就偏了)。 + * + * ★ 用**系统预置阴影档** `ShadowStyle.OUTER_FLOATING_SM`("浮起的小面板"), + * 而不是手写 `{radius, color, offsetY}`。三个理由: + * ① 手写色是 `#AARRGGBB` 或 `rgba(...)` —— 两者都被判据明令禁止 + * (B 条:"半透明属于系统材质/语义色的职责"、"鸿蒙侧不写 CSS 颜色函数"), + * 而这条禁令**是对的**:见 ②; + * ② 手写色在**深色下看不见**(深底叠深影 = 无变化),而 WebUI 在 `.dark` 里 + * 要把同一处阴影从 0.16 加重到 0.55 才有效 —— 那是"替系统猜深浅"的典型; + * ③ 系统档自带深/浅两套取值,与 `radiusCard` 走 `sys.float.*` 是同一个道理。 + * + * 发丝描边同理不再手写:`Theme.border`(系统分隔线色)已经在用, + * 深浅两套由系统给。 + */ + static readonly glassShadow: ShadowStyle = ShadowStyle.OUTER_FLOATING_SM; + + + + // ─────────────── 圆角:系统尺寸 ─────────────── /** 卡片圆角:系统"卡片"圆角(不再与 WebUI 的 14vp 绑死 —— 允许各自跟随系统) */ diff --git a/client/harmony/entry/src/main/ets/pages/CalendarPage.ets b/client/harmony/entry/src/main/ets/pages/CalendarPage.ets index f401863..144839e 100644 --- a/client/harmony/entry/src/main/ets/pages/CalendarPage.ets +++ b/client/harmony/entry/src/main/ets/pages/CalendarPage.ets @@ -38,6 +38,7 @@ import { CalendarApi } from '../api/CalendarApi'; import { hilog } from '@kit.PerformanceAnalysisKit'; import { CalendarEvent, CalendarEventInput, LunarLabels, LunarRangeResponse } from '../model/Models'; import { Theme } from '../common/Theme'; +import { GlassCardModifier, PaneModifier } from '../common/Surface'; import { AmIcon } from '../common/Icons'; import { DayCell, @@ -1446,7 +1447,7 @@ export struct CalendarPage { .width(CAL_SIDE_WIDTH) .height('100%') /* 右栏是**不透明**的:编辑器表单下面透出月网格会让输入框读不清 */ - .backgroundColor(this.bgActive ? Color.Transparent : Theme.surface) + .attributeModifier(GlassCardModifier.of(this.bgActive)) } .width('100%') .height('100%') @@ -1487,7 +1488,7 @@ export struct CalendarPage { } .width('100%') .height('100%') - .backgroundColor(this.bgActive ? Color.Transparent : Theme.pageBg) + .attributeModifier(PaneModifier.of(this.bgActive)) } .width('100%') .height('100%') diff --git a/client/harmony/entry/src/main/ets/pages/MainPage.ets b/client/harmony/entry/src/main/ets/pages/MainPage.ets index de399bb..aaa3bcb 100644 --- a/client/harmony/entry/src/main/ets/pages/MainPage.ets +++ b/client/harmony/entry/src/main/ets/pages/MainPage.ets @@ -9,6 +9,7 @@ import { hilog } from '@kit.PerformanceAnalysisKit'; import { ApiClient, ApiError } from '../api/ApiClient'; import { Theme } from '../common/Theme'; +import { GlassCardModifier, PaneModifier } from '../common/Surface'; import { AmIcon } from '../common/Icons'; import { WideSidebar } from './WideSidebar'; import { MailDetailView } from './MailDetailPage'; @@ -426,8 +427,7 @@ struct InboxTab { } .width('100%').height(46) .padding({ left: 14, right: 12 }) - .backgroundColor(this.bgActive ? Color.Transparent : Theme.surface) - .backgroundBlurStyle(this.bgActive ? Theme.cardMaterial : BlurStyle.NONE) + .attributeModifier(GlassCardModifier.of(this.bgActive)) .borderRadius(Theme.radiusCard) .border({ width: 1, color: Theme.border }) } @@ -473,8 +473,7 @@ struct InboxTab { }, (acct: AccountInfo) => acct.id) } .width('100%') - .backgroundColor(this.bgActive ? Color.Transparent : Theme.surface) - .backgroundBlurStyle(this.bgActive ? Theme.cardMaterial : BlurStyle.NONE) + .attributeModifier(GlassCardModifier.of(this.bgActive)) .border({ width: { bottom: 1 }, color: Theme.border }) /* * ★ 2026-09-19 补(用户:「元素的出现消失动画呢?」)。 @@ -540,7 +539,7 @@ struct InboxTab { } } .width('100%').height('100%') - .backgroundColor(this.bgActive ? Color.Transparent : Theme.pageBg) + .attributeModifier(PaneModifier.of(this.bgActive)) } /* @@ -627,8 +626,7 @@ struct InboxTab { .width('100%').height(78) .padding({ left: 10, right: 12 }) .alignItems(VerticalAlign.Center) - .backgroundColor(this.bgActive ? Color.Transparent : Theme.surface) - .backgroundBlurStyle(this.bgActive ? Theme.cardMaterial : BlurStyle.NONE) + .attributeModifier(GlassCardModifier.of(this.bgActive)) .borderRadius(Theme.radiusCard) .border({ width: 1, color: Theme.border }) .clip(true) @@ -910,7 +908,7 @@ struct SentTab { * · 壁纸关:仍是不透明白(与下面内容同色,看不出这条带的边界); * · 壁纸开:透明,壁纸从顶栏透上来 —— 这才是"融进背景"。 */ - .backgroundColor(this.bgActive ? Color.Transparent : Theme.surface) + .attributeModifier(GlassCardModifier.of(this.bgActive)) /* 下边框对齐 WebUI:顶栏与内容的分界靠这条线,不靠底色差 */ .border({ width: { bottom: 1 }, color: Theme.border }) @@ -952,8 +950,7 @@ struct SentTab { this.SentRow(m) } .width('100%') - .backgroundColor(this.bgActive ? Color.Transparent : Theme.surface) - .backgroundBlurStyle(this.bgActive ? Theme.cardMaterial : BlurStyle.NONE) + .attributeModifier(GlassCardModifier.of(this.bgActive)) .borderRadius(Theme.radiusCard) .margin({ bottom: 6 }) .onClick(() => { this.openMail(m); }) @@ -970,7 +967,7 @@ struct SentTab { } } .width('100%').height('100%') - .backgroundColor(this.bgActive ? Color.Transparent : Theme.pageBg) + .attributeModifier(PaneModifier.of(this.bgActive)) } } @@ -1155,8 +1152,7 @@ struct PermissionTab { } .width('100%').alignItems(HorizontalAlign.Start) .padding(12) - .backgroundColor(this.bgActive ? Color.Transparent : Theme.surface) - .backgroundBlurStyle(this.bgActive ? Theme.cardMaterial : BlurStyle.NONE) + .attributeModifier(GlassCardModifier.of(this.bgActive)) .borderRadius(Theme.radiusCard) .margin({ bottom: 8 }) } @@ -1195,7 +1191,7 @@ struct PermissionTab { * · 壁纸关:仍是不透明白(与下面内容同色,看不出这条带的边界); * · 壁纸开:透明,壁纸从顶栏透上来 —— 这才是"融进背景"。 */ - .backgroundColor(this.bgActive ? Color.Transparent : Theme.surface) + .attributeModifier(GlassCardModifier.of(this.bgActive)) /* 下边框对齐 WebUI:顶栏与内容的分界靠这条线,不靠底色差 */ .border({ width: { bottom: 1 }, color: Theme.border }) @@ -1228,7 +1224,7 @@ struct PermissionTab { } } .width('100%').height('100%') - .backgroundColor(this.bgActive ? Color.Transparent : Theme.pageBg) + .attributeModifier(PaneModifier.of(this.bgActive)) } } @@ -1502,7 +1498,7 @@ struct CommPage { * 卡片就成了"玻璃套玻璃",正是 WebUI 用 * `.bg-white .bg-white { backdrop-filter: none }` 明确禁止的事。 */ - .backgroundColor(this.bgActive ? Color.Transparent : Theme.pageBg) + .attributeModifier(PaneModifier.of(this.bgActive)) /* * 悬浮的圆形加号(用户点名要的形状):56 圆、品牌色、右下角。 @@ -1526,7 +1522,7 @@ struct CommPage { * 背景开关仍在这一层(它是通信页的根):卡不透明、容器透明, * 壁纸才从卡片间与顶/底边缘透出来(与 WebUI 的 `.comm-pane` 同构)。 */ - .backgroundColor(this.bgActive ? Color.Transparent : Theme.pageBg) + .attributeModifier(PaneModifier.of(this.bgActive)) } .navDestination(this.DestinationBuilder) .mode(NavigationMode.Auto) @@ -1549,7 +1545,7 @@ struct CommPage { * 一个不透明的 `Navigation`。此前一直在调窗格自己的 `bgActive`, * 而真正挡住的是外面这层壳。 */ - .backgroundColor(this.bgActive ? Color.Transparent : Theme.pageBg) + .attributeModifier(PaneModifier.of(this.bgActive)) } } @@ -1794,7 +1790,7 @@ struct ContactsTab { * · 壁纸关:仍是不透明白(与下面内容同色,看不出这条带的边界); * · 壁纸开:透明,壁纸从顶栏透上来 —— 这才是"融进背景"。 */ - .backgroundColor(this.bgActive ? Color.Transparent : Theme.surface) + .attributeModifier(GlassCardModifier.of(this.bgActive)) /* 下边框对齐 WebUI:顶栏与内容的分界靠这条线,不靠底色差 */ .border({ width: { bottom: 1 }, color: Theme.border }) @@ -1886,8 +1882,7 @@ struct ContactsTab { * 不要把一个"按当时字号的估算值"钉成常量 —— * 字号、行高、动作行的存在与否都会变,而常量不会跟着变。 */ - .backgroundColor(this.bgActive ? Color.Transparent : Theme.surface) - .backgroundBlurStyle(this.bgActive ? Theme.cardMaterial : BlurStyle.NONE) + .attributeModifier(GlassCardModifier.of(this.bgActive)) .borderRadius(Theme.radiusCard) .clip(true) /* 点卡片 = 打开这条会话(WebUI `ContactPanel` 的 onOpen) */ @@ -1906,7 +1901,7 @@ struct ContactsTab { } } .width('100%').height('100%') - .backgroundColor(this.bgActive ? Color.Transparent : Theme.pageBg) + .attributeModifier(PaneModifier.of(this.bgActive)) } .navDestination(this.DestinationBuilder) .mode(NavigationMode.Auto) @@ -1918,7 +1913,7 @@ struct ContactsTab { .width('100%').height('100%') /* 同 CommPage 的 Navigation:不设背景时系统默认不透明白,会把里面 已经透明的窗格盖住。联系人页是同一形状,同一修法。 */ - .backgroundColor(this.bgActive ? Color.Transparent : Theme.pageBg) + .attributeModifier(PaneModifier.of(this.bgActive)) } /** @@ -1980,8 +1975,7 @@ struct ContactsTab { } .width('100%').alignItems(HorizontalAlign.Start) .padding({ left: 12, right: 12, top: 10, bottom: 10 }) - .backgroundColor(this.bgActive ? Color.Transparent : Theme.surface) - .backgroundBlurStyle(this.bgActive ? Theme.cardMaterial : BlurStyle.NONE) + .attributeModifier(GlassCardModifier.of(this.bgActive)) .borderRadius(Theme.radiusCard) .border({ width: 1, color: Theme.border }) } @@ -3183,7 +3177,7 @@ struct MainPage { * ★ 修法与窄屏那一支**对齐**(窄屏本来就是对的): * 壁纸开着就透明,让底下的 `WallpaperLayer()` 透上来。 */ - .backgroundColor(this.bgActive ? Color.Transparent : Theme.surface) + .attributeModifier(GlassCardModifier.of(this.bgActive)) .borderRadius(this.isWide ? Theme.glassRadius : 0) .clip(this.isWide) /* diff --git a/client/harmony/entry/src/main/ets/pages/SettingsPage.ets b/client/harmony/entry/src/main/ets/pages/SettingsPage.ets index b2dc94f..f6af7c3 100644 --- a/client/harmony/entry/src/main/ets/pages/SettingsPage.ets +++ b/client/harmony/entry/src/main/ets/pages/SettingsPage.ets @@ -10,6 +10,7 @@ */ import { ApiClient, ApiError } from '../api/ApiClient'; import { Theme } from '../common/Theme'; +import { GlassCardModifier, PaneModifier } from '../common/Surface'; import { AuthApi } from '../api/AuthApi'; import { hilog } from '@kit.PerformanceAnalysisKit'; import { common } from '@kit.AbilityKit'; @@ -32,6 +33,7 @@ import { PushTokenList } from '../model/PushContract'; @Component export struct SettingsPane { + /** 背景开关(与其它窗格同一约定:开着就让出页面底) */ @Prop bgActive: boolean = false; /** 底部悬浮条高度(窄屏非 0)—— 本页滚动容器的内容末尾要让出它 */ @@ -667,8 +669,7 @@ export struct SettingsPane { } .width('100%').alignItems(HorizontalAlign.Start) .padding(16).margin({ top: 8 }) - .backgroundColor(this.bgActive ? Color.Transparent : Theme.surface) - .backgroundBlurStyle(this.bgActive ? Theme.cardMaterial : BlurStyle.NONE) + .attributeModifier(GlassCardModifier.of(this.bgActive)) .borderRadius(Theme.radiusCard) /* 外观(主题 / 壁纸) */ @@ -739,8 +740,7 @@ export struct SettingsPane { .width('100%') } .width('100%').height('68%') - .padding(16).backgroundColor(this.bgActive ? Color.Transparent : Theme.surface) - .backgroundBlurStyle(this.bgActive ? Theme.cardMaterial : BlurStyle.NONE) + .padding(16).attributeModifier(GlassCardModifier.of(this.bgActive)) .borderRadius({ topLeft: 12, topRight: 12 }) /* * ★ 2026-09-19 补(用户:「元素的出现消失动画呢?」)。 @@ -759,7 +759,7 @@ export struct SettingsPane { } .width('100%').height('100%') /* 与其它窗格同一约定:背景开着时让出页面底,壁纸才透得过 */ - .backgroundColor(this.bgActive ? Color.Transparent : Theme.pageBg) + .attributeModifier(PaneModifier.of(this.bgActive)) } /** 分区标题(与 WebUI `AccountPage` 的 `

` 同形:小字、灰、上间距) */ @@ -826,8 +826,7 @@ export struct SettingsPane { } .width('100%').alignItems(HorizontalAlign.Start) .padding(16).margin({ top: 10 }) - .backgroundColor(this.bgActive ? Color.Transparent : Theme.surface) - .backgroundBlurStyle(this.bgActive ? Theme.cardMaterial : BlurStyle.NONE) + .attributeModifier(GlassCardModifier.of(this.bgActive)) .borderRadius(Theme.radiusCard) } @@ -898,8 +897,7 @@ export struct SettingsPane { } .width('100%').alignItems(HorizontalAlign.Start) .padding(16).margin({ top: 8 }) - .backgroundColor(this.bgActive ? Color.Transparent : Theme.surface) - .backgroundBlurStyle(this.bgActive ? Theme.cardMaterial : BlurStyle.NONE) + .attributeModifier(GlassCardModifier.of(this.bgActive)) .borderRadius(Theme.radiusCard) } @@ -936,8 +934,7 @@ export struct SettingsPane { } .width('100%').alignItems(HorizontalAlign.Start) .padding(16).margin({ top: 8 }) - .backgroundColor(this.bgActive ? Color.Transparent : Theme.surface) - .backgroundBlurStyle(this.bgActive ? Theme.cardMaterial : BlurStyle.NONE) + .attributeModifier(GlassCardModifier.of(this.bgActive)) .borderRadius(Theme.radiusCard) } @@ -1022,8 +1019,7 @@ export struct SettingsPane { } .width('100%').alignItems(HorizontalAlign.Start) .padding(16).margin({ top: 8 }) - .backgroundColor(this.bgActive ? Color.Transparent : Theme.surface) - .backgroundBlurStyle(this.bgActive ? Theme.cardMaterial : BlurStyle.NONE) + .attributeModifier(GlassCardModifier.of(this.bgActive)) .borderRadius(Theme.radiusCard) } @@ -1160,8 +1156,7 @@ export struct SettingsPane { } .width('100%').alignItems(HorizontalAlign.Start) .padding(16).margin({ top: 8 }) - .backgroundColor(this.bgActive ? Color.Transparent : Theme.surface) - .backgroundBlurStyle(this.bgActive ? Theme.cardMaterial : BlurStyle.NONE) + .attributeModifier(GlassCardModifier.of(this.bgActive)) .borderRadius(Theme.radiusCard) } @@ -1188,8 +1183,7 @@ export struct SettingsPane { } .width('100%').alignItems(HorizontalAlign.Start) .padding(16).margin({ top: 8 }) - .backgroundColor(this.bgActive ? Color.Transparent : Theme.surface) - .backgroundBlurStyle(this.bgActive ? Theme.cardMaterial : BlurStyle.NONE) + .attributeModifier(GlassCardModifier.of(this.bgActive)) .borderRadius(Theme.radiusCard) .onClick(() => { this.getUIContext().getRouter().pushUrl({ url: 'pages/AdminUsersPage' });