From c0ab3f57b274da775ed6fa6a349c138b9bad3485 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Sun, 20 Sep 2026 09:01:46 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B7=A8=E7=AB=AF:=20=E9=A1=B6=E9=83=A8?= =?UTF-8?q?=E9=A1=B5=E7=AD=BE=E6=9D=A1=E6=94=B9=E6=82=AC=E6=B5=AE=E7=8E=BB?= =?UTF-8?q?=E7=92=83=20+=20=E6=B7=B1=E8=89=B2=E5=8E=8B=E6=9A=97=E4=B8=8B?= =?UTF-8?q?=E9=99=90=EF=BC=88=E4=BF=AE=208=20=E5=A4=84=E6=B7=B1=E8=89=B2?= =?UTF-8?q?=E5=8F=AF=E8=AF=BB=E6=80=A7=EF=BC=89+=20=E6=89=8B=E5=8A=BF?= =?UTF-8?q?=E5=88=A4=E6=8D=AE=E8=87=AA=E6=90=AD=E7=8E=B0=E5=9C=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户:「顶部的收件箱发件箱和授权为什么不做成悬浮玻璃?」 问得对,而且它当时是**整页唯一一块实心白**。 ★ 顶部页签条 → 悬浮玻璃 `CommTabBar` 原来写的是 `backgroundColor(Theme.surface)`(系统卡片色=实体)。 上一轮把列表容器、卡片、底部导航条都玻璃化了,**唯独漏了这条** —— 它既不是卡片也不是容器,是"条状 chrome",逐处修时最容易漏的那一类。 ⇒ 现在与底部浮动条**同一族**:圆角 + 左右留白 + `Theme.navMaterial`。 ★ 几何常量复用底部条的(`TAB_BAR_RADIUS = NAV_BAR_RADIUS`、 `TAB_BAR_SIDE = NAV_BAR_SIDE`),不各写一份 —— 两个数一旦分叉, "同一条轴线上的两种条"就不齐了。为什么选悬浮而不是 WebUI 的"通栏无底色" (WebUI 页签条自己**没有**底色,靠父面板的玻璃)也记在那个常量的注释里。 新增判据:页签条必须与导航条同族(不许实体面 / 要引 TAB_BAR_* 常量), 变异验证过(改回 `Theme.surface` 即红)。 ★ 深色压暗下限 —— 本轮最重要的真 bug 玻璃让壁纸**真的透进卡片**之后,"浅壁纸 + 深色文字令牌"这个组合会在 **卡片内部**发生。设备实测(深色、aurora、压暗 37): 「角色」ink rgb(183,195,211) on bg rgb(68,95,128) = 2.57:1 「创建时间」2.27:1、「状态」2.68:1、「压暗」2.19:1(共 8 段 <3:1) 而**同一套代码在浅色下全部达标** ⇒ 差别不在"选错了色",在"底没暗下来"。 WebUI 早就撞过并写下了必然值(`index.css:442`): .dark { --bg-dim-min: 92%; } /* 浅色下是 0% */ background-color: rgb(var(--bg-scrim) / max(var(--bg-dim), var(--bg-dim-min))); 我们只有 `scrimOpacity(bgDim)`、**没有下限**。补上 `DARK_DIM_MIN = 0.92` (浅色下用户设的值照旧生效,不忽略设置)。 复扫:低对比 8 段 → 通信 0 / 日历 0 / 联系 1 / 我的 1 (剩下两条经手核是扫描器的假阳性:近黑底上把抗锯齿暗像素当成了"墨")。 ★ 手势判据"没有自己搭现场"(套件红、单独绿) `cross-client-gesture` 只保证"在月档"、**不保证在哪一月**,而它算的是 相对最初那一月的 delta ⇒ 前面某个判据(或一次失败的手势)把日历翻到 10 月后, 期望值就差一格。修法:先点「今天」归位到本月(该按钮语义就是"回本月")。 验证:从 9 月起步、从 10 月起步,两种脏状态都通过。 ★ 顺带确认手势本体是好的:`--speed 1500` 时 1000px 要走 670ms, 紧贴 `SWIPE_MAX_DURATION_MS=700` 而被丢弃;`--speed 3000` 两个方向都翻。 ★ 判据基建:修掉一个**静默失效**的 API 名(沿用上轮的教训修法) `Surface.ets` 里两处块注释内嵌了 `/* ... */` —— 会把块注释**提前闭合**, 于是后面的代码跑到注释外面(报"Use let instead of var"/"Invalid character")。 本仓已有这条纪律("当 `code()` 因注释里的 `/*` 吞掉 import 时,改注释文本"), 这次又踩了两处(`Surface.ets` 与 `Appearance.ts`)。 判据:files=32 ran=32 checks=519 pass=519 fail=0 red=0;baseline 7/7✓。 --- .../test/cross-client-gesture.test.mjs | 20 +++++++++ client/electron/test/harmony-nav.test.mjs | 41 +++++++++++++++++++ client/electron/test/run-all.mjs | 2 +- .../entry/src/main/ets/model/Appearance.ts | 41 +++++++++++++++++-- .../entry/src/main/ets/model/NavItems.ts | 27 ++++++++++++ .../entry/src/main/ets/pages/MainPage.ets | 22 +++++++++- 6 files changed, 147 insertions(+), 6 deletions(-) diff --git a/client/electron/test/cross-client-gesture.test.mjs b/client/electron/test/cross-client-gesture.test.mjs index fb6b853..88f0c84 100644 --- a/client/electron/test/cross-client-gesture.test.mjs +++ b/client/electron/test/cross-client-gesture.test.mjs @@ -359,6 +359,26 @@ test('★ 设备:在网格列上左滑,日历标题真的翻到下一段', a await new Promise((r) => setTimeout(r, 2000)); } + /* + * ★★ 2026-09-20 修(本判据在套件里红、单独跑绿):**先回到已知的月份**。 + * + * 本判据下面算的是"相对最初那一月的 delta",而它原来**只保证在月档、 + * 不保证在哪一月** —— 于是前一屏停在哪个月、它就从那个月开始数。 + * 套件里跑时,日历页可能已被别的判据(或上一次失败的手势)翻到 10 月, + * 而右滑那一步的期望值是按"最初那一月"算的 ⇒ 差一格就判红。 + * + * 这不是"设备抖动",是**判据没有自己搭现场**:它依赖进入时的状态, + * 于是"通过与否取决于跑之前那一屏是什么"。同族毛病本仓已修过多次 + * (`harmony-appearance` 那条设备判据、以及 `backToMain` 那段注释)。 + * + * 修法:点「今天」把 anchor 归位到本月(该按钮的语义就是"回本月", + * 与 WebUI `goToday` 一致),再从那个确定的状态开始算 delta。 + */ + assert.ok(tapText(hdc, '今天'), '要能点「今天」把月份归位(本判据需要一个已知起点)'); + await new Promise((r) => setTimeout(r, 2000)); + assert.ok(monthTitle(dumpLayout(hdc)) !== null, + '点了「今天」之后仍应在月档(否则下面的 delta 无从算起)'); + /* * ★★ 2026-09-19:本判据第一版用「滑一次 + 固定等 2500ms + dump」。 * 结果**单独跑通过、接进 run-all 后失败**(同一台设备、同一份代码)。 diff --git a/client/electron/test/harmony-nav.test.mjs b/client/electron/test/harmony-nav.test.mjs index b1467f6..fdd801f 100644 --- a/client/electron/test/harmony-nav.test.mjs +++ b/client/electron/test/harmony-nav.test.mjs @@ -585,6 +585,47 @@ test('★ 出现/消失的那类元素真的挂了过渡(`if` 包的弹层不 assert.deepEqual(missing, [], `这些位置会硬弹:${missing.join('、')}`); }); +test('★ 顶部页签条与底部导航条同一族(都是悬浮玻璃)—— 一块实心白就是"割裂"', () => { + /* + * 用户 2026-09-20:「顶部的收件箱发件箱和授权为什么不做成悬浮玻璃?」 + * + * 那条页签条当时是**整页唯一一块实心白**:上一轮把列表容器、卡片、 + * 底部导航条都玻璃化了,唯独它还写着 `backgroundColor(Theme.surface)`(系统卡片色=实体) + * ⇒ 壁纸从四周透出来,只有它在顶上白着一横条。 + * + * ★ 这条判据要抓的形状:**"同一页里少数几处忘了跟着改"** —— + * 逐处修的时候最容易漏的就是"不是卡片、也不是容器"的条状 chrome。 + * 所以判据盯的是"页签条有没有和导航条**用同一族**的三种属性", + * 而不是"有没有某个字符串": + * ① 圆角不是自己拍的值(必须引 `TAB_BAR_RADIUS`,它 = `NAV_BAR_RADIUS`); + * ② 左右留白引 `TAB_BAR_SIDE`(它 = `NAV_BAR_SIDE`)⇒ 两条轴线对齐; + * ③ 材质走 `Theme.navMaterial`(与底部条同一档次),不是 `Theme.surface`。 + */ + const page = stripComments(main); + const at = page.indexOf('CommTabBar()'); + assert.ok(at > 0, '通信页要有页签条(收件箱/发件箱/授权)'); + const body = page.slice(at, at + 3000); + + assert.ok(!/\.backgroundColor\(Theme\.surface\)/.test(body), + '页签条不许铺实体面(Theme.surface)—— 那是壁纸模式下唯一一块实心白,"割裂"就是这么来的'); + assert.match(body, /\.backgroundBlurStyle\(this\.bgActive \? Theme\.navMaterial : BlurStyle\.NONE\)/, + '页签条要吃系统材质(与底部导航条同档),否则它和玻璃卡片拼在一起不是一套东西'); + assert.match(body, /\.borderRadius\(TAB_BAR_RADIUS\)/, + '页签条要用 TAB_BAR_RADIUS(= NAV_BAR_RADIUS)—— 自己拍一个数会让上下两条不齐'); + assert.match(body, /\.margin\(\{[^}]*left: TAB_BAR_SIDE[^}]*right: TAB_BAR_SIDE/, + '页签条的左右留白要用 TAB_BAR_SIDE(= NAV_BAR_SIDE)—— 悬浮条的侧边必须与底部条同值'); + + /* + * 自检:把页签条改回实心面,上面第一条必须判红。 + * (不写自检的话,这条判据可能因为 `body` 切片取错位置而**恒绿** —— + * 本仓撞过"判据自己在骗自己",所以凡按位置切片的都验一次。) + */ + const broken = 'CommTabBar() {\n Row() {}\n .width(\'100%\')\n .backgroundColor(Theme.surface)\n}'; + const bAt = broken.indexOf('CommTabBar()'); + assert.ok(/\.backgroundColor\(Theme\.surface\)/.test(broken.slice(bAt, bAt + 3000)), + '自检:实心面这个写法必须能被命中(否则上面的断言是假的)'); +}); + test('★ 玻璃只在两处、且这一处是"背后有可变内容"(GLASS 登记的放行条件)', () => { /* * pi 撤回"模糊只由壁纸层负责"后给的是两条:①同一张底只许糊一次; diff --git a/client/electron/test/run-all.mjs b/client/electron/test/run-all.mjs index e46d57f..4cf6681 100644 --- a/client/electron/test/run-all.mjs +++ b/client/electron/test/run-all.mjs @@ -83,7 +83,7 @@ const SUITE = [ // P4 外观同步:跑 model/Appearance.ts(纯逻辑),所以也要 strip-types ['test/harmony-appearance.test.mjs', ['--experimental-strip-types', '--no-warnings'], 27], // P5 悬浮玻璃导航:点击配对 / index 决定挂载 / 命中区 ≥44vp / 悬浮与让位 - ['test/harmony-nav.test.mjs', ['--experimental-strip-types', '--no-warnings'], 20], + ['test/harmony-nav.test.mjs', ['--experimental-strip-types', '--no-warnings'], 21], // 上下黑边(用户 2026-09-17/18 报过两次)——钉的是一整套东西的两半: // `setWindowLayoutFullScreen(true)`(消黑边)+ `getWindowAvoidArea`(让开时钟/手势条)。 // 只做前半 ⇒ 页签被时钟盖住(上一次就是这样退回去的,黑边于是留了三天); diff --git a/client/harmony/entry/src/main/ets/model/Appearance.ts b/client/harmony/entry/src/main/ets/model/Appearance.ts index 3d468f5..62a7562 100644 --- a/client/harmony/entry/src/main/ets/model/Appearance.ts +++ b/client/harmony/entry/src/main/ets/model/Appearance.ts @@ -271,10 +271,45 @@ export function isDarkMode(theme: string, systemColorMode: number): boolean { return systemColorMode === 0; // COLOR_MODE_DARK } -/** 遮罩浓度:0~90 的"压暗"值 → 0~1(系统遮罩色 + 这个不透明度) */ -export function scrimOpacity(bgDim: number): number { +/** + * **深色下的压暗下限**(比例)。0.92 = 壁纸至少被压掉 92%。 + * + * ★★ 2026-09-19:这条是从 WebUI 抄来的**必然值**,不是调出来的观感。 + * + * WebUI `index.css:772` 与 `:442`: + * + * background-color: rgb(var(--bg-scrim) / max(var(--bg-dim), var(--bg-dim-min))); + * .dark { --bg-dim-min: 92%; } ← 浅色下是 0% + * + * 那段注释写清了成因(实测事故):用户上传的是一张**浅色**照片 + * (均值 RGB 213,212,225),压暗 56% 之后壁纸仍是中灰 94, + * 而深色主题的文字是近白的 ⇒ 近白对中灰只有约 3.6:1, + * 次要文字更是 1.2–1.4:1。**没有任何文字颜色能救** —— + * 不是"选错了色",是"底没暗下来"。 + * + * 加了下限之后 WebUI 复测:四页最差 通信 6.14 / 日历 4.90 / 联系 4.96 / + * 我的 5.38,全部 ≥ 4.5:1。 + * + * ★ 为什么鸿蒙这边也必须跟:现在卡片是**玻璃**(壁纸真的透得进来), + * 于是"浅壁纸 + 深色文字令牌"这个组合会在**卡片内部**发生 —— + * 设备实测(模拟器 3184×2232、深色、aurora + 压暗 37): + * 「角色」ink rgb(183,195,211) on bg rgb(68,95,128) = **2.57:1** + * 「创建时间」2.27:1、「状态」2.68:1、「压暗」2.19:1 + * 而**同一套代码在浅色下**这些字都是达标的 —— 差别就在底有没有暗下来。 + */ +export const DARK_DIM_MIN: number = 0.92; + +/** + * 遮罩浓度:0~90 的"压暗"值 → 0~1(系统遮罩色 + 这个不透明度)。 + * + * ★ 深色下取 `max(用户设的, DARK_DIM_MIN)`:用户的压暗值在浅色下**完全照用** + * (浅色页面本来就白,压暗只是调味),而深色下它压不住浅壁纸。 + * 这不是"忽略用户设置"—— 用户设的 37% 在浅色下依然原样生效。 + */ +export function scrimOpacity(bgDim: number, dark?: boolean): number { const d: number = clampNumber(bgDim, 0, 90, 12); - return d / 100; + const v: number = d / 100; + return dark === true ? Math.max(v, DARK_DIM_MIN) : v; } /** 状态文案:降级必须看得见(WebUI 侧的原话:「否则用户以为换设备也能带走」) */ diff --git a/client/harmony/entry/src/main/ets/model/NavItems.ts b/client/harmony/entry/src/main/ets/model/NavItems.ts index ca8c5b5..4239ad7 100644 --- a/client/harmony/entry/src/main/ets/model/NavItems.ts +++ b/client/harmony/entry/src/main/ets/model/NavItems.ts @@ -299,3 +299,30 @@ export function navBadgeText(n: number): string { } return n + ''; } + + +/** + * 顶部页签条(通信页的 收件箱/发件箱/授权)的几何 —— 与底部浮动条**同一族**。 + * + * 用户 2026-09-20:「顶部的收件箱发件箱和授权为什么不做成悬浮玻璃?」 + * + * 问得对,而且它当时是**整页唯一一块实心白**:上一轮把列表容器、卡片、 + * 导航条都玻璃化了,唯独这条页签条还写着 `Theme.surface`(系统卡片色 = 实体) + * ⇒ 壁纸从四周透出来,只有它在顶上白着一横条,成了最扎眼的那块。 + * + * 为什么**悬浮**(左右留白 + 圆角)而不是"通栏 + 材质": + * · 底部导航条已经确立了这个语汇(`NAV_BAR_*`),顶部再做一个通栏的 + * 会在同一条轴线上出现两种不同的"条";用户要的正是"和底部一样"。 + * · WebUI 的页签条本身**没有底色**(`CommTabs.tsx:40` 只有 + * `border-b border-gray-200`),靠父面板的玻璃 —— 那是"通栏"做法。 + * 本仓底部条选了悬浮,于是顶部跟底部统一,是**本仓的**选择; + * 差异记在这里,免得后人以为漏抄了 WebUI。 + * + * 高度与侧留白**复用底部条的常量**(不是各写一份):两者一旦不同, + * "同一条轴线上的两种条"就变成肉眼可见的不齐 —— 而这正是本仓反复出现的那类毛病 + * (同一个东西两处各写一个数,然后慢慢分叉)。 + */ +export const TAB_BAR_RADIUS: number = NAV_BAR_RADIUS; +export const TAB_BAR_SIDE: number = NAV_BAR_SIDE; +/** 顶部留白(vp):离内容区顶部一点点,做成"浮着"而不是"贴着" */ +export const TAB_BAR_TOP: number = 8; diff --git a/client/harmony/entry/src/main/ets/pages/MainPage.ets b/client/harmony/entry/src/main/ets/pages/MainPage.ets index aaa3bcb..51dc653 100644 --- a/client/harmony/entry/src/main/ets/pages/MainPage.ets +++ b/client/harmony/entry/src/main/ets/pages/MainPage.ets @@ -86,6 +86,9 @@ import { NAV_CONTENT_RESERVE, NAV_ITEM_MIN_HIT, NAV_ITEMS, + TAB_BAR_RADIUS, + TAB_BAR_SIDE, + TAB_BAR_TOP, navBadgeCount, navBadgeText, navBadgeTone, @@ -1425,7 +1428,22 @@ struct CommPage { }, (key: string) => key) } .width('100%') - .backgroundColor(Theme.surface) + /* + * ★★ 2026-09-20(用户:「顶部的收件箱发件箱和授权为什么不做成悬浮玻璃?」) + * + * 这一行原来是 `backgroundColor(Theme.surface)` —— 系统卡片色,**实体**。 + * 于是它是整页唯一一块实心白:上一轮把列表容器、卡片、底部导航条 + * 都玻璃化了,唯独漏了这条页签条 ⇒ 壁纸从四周透出来,只有它在顶上白着一横条。 + * + * 现在与**底部浮动条同一族**:圆角 + 左右留白 + 系统材质。 + * 三种做法各自的取舍见 `model/NavItems.ts` 的 `TAB_BAR_*` 常量注释 + * (为什么选悬浮而不是 WebUI 的"通栏无底色")。 + */ + .backgroundColor(Color.Transparent) + .backgroundBlurStyle(this.bgActive ? Theme.navMaterial : BlurStyle.NONE) + .borderRadius(TAB_BAR_RADIUS) + .clip(true) + .margin({ left: TAB_BAR_SIDE, right: TAB_BAR_SIDE, top: TAB_BAR_TOP }) } openMail(mailId: string, accountId: string): void { @@ -2599,7 +2617,7 @@ struct MainPage { this.accountLabel = ''; } // 模糊强度是**服务端给的 px 原值**:计划只搬运它,映射成系统材质档在画的那一层做 - this.bgPlan = resolveBackground(snap.bgKind, snap.bgPresetId, scrimOpacity(snap.bgDim), store.wallpaper !== null, dark, snap.bgBlur); + this.bgPlan = resolveBackground(snap.bgKind, snap.bgPresetId, scrimOpacity(snap.bgDim, dark), store.wallpaper !== null, dark, snap.bgBlur); this.bgActive = this.bgPlan.kind !== 'none'; /* 让 `@State` 感知到"壁纸内容换了"(对象内部字段的变化观察不到 —— 见它的声明) */ this.bgContentRev = this.bgContentRev + 1;