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;