From a87a88ea2a341bb5d30394a8c54fa948c0bd70ab Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Fri, 18 Sep 2026 10:34:00 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B7=A8=E7=AB=AF:=20=E5=85=A8=E5=B1=8F?= =?UTF-8?q?=E6=98=AF**=E7=AA=97=E5=8F=A3=E7=BA=A7**=E7=9A=84=20=E2=80=94?= =?UTF-8?q?=E2=80=94=20=E5=85=89=E7=BB=99=20MainPage=20=E8=AE=A9=E4=BD=8D?= =?UTF-8?q?=EF=BC=8C=E7=AD=89=E4=BA=8E=E6=8A=8A=E9=BB=91=E8=BE=B9=E6=8D=A2?= =?UTF-8?q?=E6=88=90=E9=A1=B6=E6=A0=8F=E5=8E=8B=E5=AD=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上一提交(cac026e)把黑边消掉了,但**只给 `MainPage` 加了避让**。 实测截图硬证:写邮件页的「取消」与时钟「09:49」重叠、「发送」与 wifi/电量图标重叠。 ## 根因:`setWindowLayoutFullScreen(true)` 不只作用于当前页 它是**窗口级**的:一旦设上,这个窗口里**所有**用 `router.pushUrl` 推上来的页 (写邮件/会话/收件箱/邮件详情/用户管理)都从 y=0 开始画。 所以上次那个错与更早那次(6861934 只删全屏不留避让)**同源**: 都是"同一件事只做了一半"。上次少的是**步骤**,这次少的是**页面**。 ## 改法:把"消费避让"变成每个 @Entry 页都得做的事 - `model/WindowInsets.ts` 加 `topInset(insets)`:取 0 时(未全屏/取不到) 表达式的值与旧代码**逐字相同** ⇒ 没全屏的环境行为不变,不会把谁顶下去。 - 五个页各按自己的形状让位: - 固定 56vp 顶栏(写邮件/会话/收件箱/用户管理): `height(56 + topInset(...))` **与** `padding(… top: topInset(...))` 一起加 —— 只加 padding 会把固定的 56 切掉 39(按钮压扁),只加 height 则内容仍贴 y=0。 - 满高容器(邮件详情):`padding({ top })` 加在 `@Entry` 包装层。 ★ **不能加在 `MailDetailView` 里面**:它同时被 `MainPage` 的 Navigation 内嵌复用, 而那层已经让过位了 —— 加在里面就变成让两次(39vp 变 78vp)。 这类"同一组件两种入口"的坑与"悬浮加号要放在 Navigation 内部"同源: **让位的量取决于它被挂在哪一层**。 ## 判据(harmony-window 8 → 9 条) 接线⑤ 枚举**所有** `@Entry` 页并要求它们消费避让 —— 口径是 "有人在窗口上开了全屏 ⇒ 每个 @Entry 页都得让",而不是"检查 MainPage 做了没有"。 后者在新增一个推上来的页时会静默逃掉,而"新增一个页"正是最常发生的事。 豁免要带**可机器复核**的理由(不再是"这个页先不管"): - `Index.ets`:DevEco 模板欢迎页,不在 `main_pages.json` 流程里; - `LoginPage.ets`:根容器 `.align(Alignment.Center)` ⇒ 结构上碰不到 y=0。 但"居中"是可能被改掉的性质 ⇒ 判据**断言那个居中写法仍然存在**, 谁把它改成贴顶,这条先红,逼他回来重新想这个页要不要避让。 **变异自检两个方向都跑过**: - 把 ComposePage 避让整个拿掉(重演"只给 MainPage 加")⇒ 红 ✓ - 只加 padding 不加 height(会压扁按钮)⇒ 红 ✓ ★ 期间还修掉两处"判据锚在当时的字符串上"(不是放宽,是它把"加一个正当的避让" 与"真犯那个错"判得一模一样): - `harmony-nav` ④ 的留白断言、`harmony-widescreen` ④ 的 navReserve 断言, 都改成剥注释后验**形状与不变量**,而不是写死字面表达式。改完变异仍咬得住。 --- client/electron/test/harmony-window.test.mjs | 113 ++++++++++++++++++ client/electron/test/run-all.mjs | 2 +- .../entry/src/main/ets/model/WindowInsets.ts | 24 ++++ .../src/main/ets/pages/AdminUsersPage.ets | 15 ++- .../entry/src/main/ets/pages/ComposePage.ets | 23 +++- .../entry/src/main/ets/pages/InboxPage.ets | 17 ++- .../src/main/ets/pages/MailDetailPage.ets | 15 +++ .../entry/src/main/ets/pages/SessionsPage.ets | 17 ++- 8 files changed, 218 insertions(+), 8 deletions(-) diff --git a/client/electron/test/harmony-window.test.mjs b/client/electron/test/harmony-window.test.mjs index cf5003d..b68bf24 100644 --- a/client/electron/test/harmony-window.test.mjs +++ b/client/electron/test/harmony-window.test.mjs @@ -30,6 +30,7 @@ import test from 'node:test'; import assert from 'node:assert/strict'; +import { readdirSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { prose } from './lib/read.mjs'; @@ -165,6 +166,118 @@ test('★ 接线④:`.ets` 不许 import `WindowInsets` 的 SDK 类型(判 'WindowInsets.ts 必须保持纯净(无 SDK 依赖),否则判据连编都编不过'); }); +test('★ 接线⑤:**每一个 `@Entry` 页**都要消费避让(全屏是窗口级的,不止 MainPage)', () => { + /* + * ── 这条为什么是全文件第二重要(2026-09-18 实测踩到)── + * + * `setWindowLayoutFullScreen(true)` 是**窗口级**的:一旦设上,窗口里**所有** + * 用 `router.pushUrl` 推上来的页都从 y=0 开始画,而不只是 `MainPage`。 + * + * 我第一版只给 `MainPage` 加了避让 ⇒ 写邮件页实拍:「取消」与时钟「09:49」重叠、 + * 「发送」与 wifi/电量图标重叠。**把黑边换成了另一种坏** —— 而且两个错同源: + * 都是"同一件事只做了一半"(上次少了避让,这次少了页面)。 + * + * ★ 所以口径是「**有人在窗口上开了全屏 ⇒ 每个 `@Entry` 页都得让**」, + * 而不是「检查 MainPage 做了没有」。后者下新增一个推上来的页会静默逃掉 —— + * 而"新增一个页"正是最常发生的事。 + */ + const pageDir = join(HARMONY, 'entry', 'src', 'main', 'ets', 'pages'); + const entries = readdirSync(pageDir) + .filter(f => f.endsWith('.ets')) + .filter(f => /^@Entry\b/m.test(prose(join(pageDir, f)))); + /* + * 自检:扫到的 `@Entry` 页数要 ≥ 5(`main_pages.json` 里登记 7 个)。 + * 防的是"目录改名/遍历写错 ⇒ 这条变成空判据而依然全绿"—— + * 与 `harmony-system-api` 里那条"扫到的文件数 ≥ N"同形。 + */ + assert.ok(entries.length >= 5, `要扫到全部 @Entry 页(实际 ${entries.length}:${entries.join('、')})`); + + /* + * 两种合法的"消费"写法: + * · `topInset(this.windowInsets)` —— 直白的(顶栏页用这个); + * · 直接读 `this.windowInsets.statusBar` —— MainPage 要的,它得把避让嵌进 + * `isWide ? paneGap : statusBar` 这个条件表达式里,套不进 `topInset()`。 + * 两者都算"把值用起来了";形状不同而要求相同(与 `harmony-nav` 那条同口径: + * 判的是"读到的东西有没有落到布局上",不是"用的哪个函数")。 + */ + const consumes = (src) => /topInset\(\s*this\.windowInsets\s*\)/.test(src) + || /this\.windowInsets\.statusBar/.test(src); + + /* + * 豁免名单:每条都必须是**可机器复核的理由**,不是"这个页先不管"。 + * + * `LoginPage` 的根容器是 `.align(Alignment.Center)` 的满高容器 —— 内容是**居中**的, + * 最顶上的那个像素也远在状态栏之下,结构上碰不到 y=0。所以它不需要避让。 + * + * ★ 但"居中"是个可能被改掉的性质(哪天有人给它加个贴顶的标题栏,理由就没了)。 + * 所以这里不只看一次理由字符串 —— 下面**断言那个居中写法仍然存在**: + * 谁把它改成贴顶,这条先红,逼他回来重新想这个页要不要避让。 + * 这正是本仓那条"豁免得带可复核的理由"(`harmony-system-api` 的 ALLOW 同形)。 + */ + const EXEMPT = new Map([ + ['Index.ets', { why: 'DevEco 模板欢迎页,不在 main_pages.json 的流程里,从不被路由到', check: null }], + ['LoginPage.ets', { + why: '根容器 .align(Alignment.Center):内容居中,结构上碰不到 y=0', + check: (src) => /\.align\(Alignment\.Center\)/.test(stripped(src)) + && !/Alignment\.Top/.test(stripped(src)) + }] + ]); + + const missing = []; + for (const f of entries) { + const src = prose(join(pageDir, f)); + const exempt = EXEMPT.get(f); + if (exempt) { + if (exempt.check) { + assert.ok(exempt.check(src), + `${f} 被豁免的理由已不存在(原理由:${exempt.why})—— ` + + '要么恢复那个形状,要么把它当普通页消费避让,不要默默留着豁免'); + } + continue; + } + if (!consumes(src)) missing.push(f); + } + assert.deepEqual(missing, [], + `这些 @Entry 页没有消费窗口避让:${missing.join('、')} —— ` + + '全屏是窗口级的,漏一个页就是一处「顶栏被时钟压住」(实测过:写邮件页的取消/发送压着 09:49 与电量)'); + + /* + * `\u2605` 顶栏那对"高度 + padding-top"只对**固定高度**的顶栏成立(`height(56)`)。 + * + * `MailDetailPage` 是另一种形状:它的 `@Entry` 包装层是 `height('100%')` 的**满高列**, + * 避让用 `padding({ top: … })` 加在满高容器上(内容自然往下,没有"从 56 里切"的问题)—— + * 这是满高容器的正确写法,不该被要求写成 `56 + topInset(...)`。 + * + * 所以这里只对**真的写了固定 height(56) 顶栏**的页判那对同现, + * 其余页(满高容器)只要求"避让落到了布局上"(上面已经判过)。 + */ + const halfDone = []; + for (const f of entries) { + if (EXEMPT.has(f)) continue; + const code_ = stripped(prose(join(pageDir, f))); + if (!/topInset\(\s*this\.windowInsets\s*\)/.test(code_)) continue; // 不是顶栏形状(MainPage) + /* + * 只看**同一个修饰符链**:包含 `padding(top: topInset(...))` 的那一段往前找它的 `.height(...)`。 + * + * 不能满文件搜 `height(56)` —— `MailDetailPage` 里的悬浮回复球是 `.width(56).height(56)`, + * 与顶栏毫无关系(我第一版就是这么误报的:把一个 56×56 的圆球当成了"固定 56 顶栏")。 + * 判据要锚在"这一段的形状"上,而不是"文件里有没有出现过这个数"。 + */ + const pad = /\.padding\(\{[^}]*top:\s*topInset\(\s*this\.windowInsets\s*\)[^}]*\}\)/.exec(code_); + if (!pad) continue; + const chain = code_.slice(Math.max(0, pad.index - 300), pad.index + pad[0].length); + const hasFixed56 = /\.height\(\s*56\s*\)/.test(chain); + if (!hasFixed56) continue; // 满高容器形状(MailDetailPage):没有从 56 里切的问题 + const heightGrew = /\.height\(56 \+ topInset\(\s*this\.windowInsets\s*\)\)/.test(chain); + if (!heightGrew) { + halfDone.push(f); + } + } + assert.deepEqual(halfDone, [], + `这些页的顶栏没把"高度 + padding-top"一起加:${halfDone.join('、')} —— ` + + '只加一个会把固定 56vp 切掉一截(按钮压扁)或让位没生效(内容仍贴 y=0)'); +}); + /* ───────────────────────── ③ 设备行为(有设备才跑)───────────────────────── */ /* diff --git a/client/electron/test/run-all.mjs b/client/electron/test/run-all.mjs index 5bc53e2..15ac45f 100644 --- a/client/electron/test/run-all.mjs +++ b/client/electron/test/run-all.mjs @@ -82,7 +82,7 @@ const SUITE = [ // `setWindowLayoutFullScreen(true)`(消黑边)+ `getWindowAvoidArea`(让开时钟/手势条)。 // 只做前半 ⇒ 页签被时钟盖住(上一次就是这样退回去的,黑边于是留了三天); // 只做后半 ⇒ 黑边照旧。**两半缺哪一半都要判红**,所以变异自检两个方向都跑过。 - ['test/harmony-window.test.mjs', ['--experimental-strip-types', '--no-warnings'], 8], + ['test/harmony-window.test.mjs', ['--experimental-strip-types', '--no-warnings'], 9], // 外观契约:默认值去 Go 源码里读(服务端 DefaultAppearance 是权威)+ 缓存键按账号 ['test/appearance-defaults.test.mjs', [], 4], ['test/build-stamp.test.mjs', [], 7], diff --git a/client/harmony/entry/src/main/ets/model/WindowInsets.ts b/client/harmony/entry/src/main/ets/model/WindowInsets.ts index 27570f1..a1fadef 100644 --- a/client/harmony/entry/src/main/ets/model/WindowInsets.ts +++ b/client/harmony/entry/src/main/ets/model/WindowInsets.ts @@ -120,3 +120,27 @@ export interface AvoidAreaLike { topRect?: InsetsRect; bottomRect?: InsetsRect; } + +/** + * 顶栏该往下让多少 —— **每一个 `@Entry` 页都要用这个值**。 + * + * ── 为什么这不能只是 `MainPage` 的事(2026-09-18 实测踩到)── + * + * `setWindowLayoutFullScreen(true)` 是**窗口级**的:一旦设上,窗口里**所有** + * 用 `router.pushUrl`/`replaceUrl` 推上来的页(`ComposePage`/`SessionsPage`/ + * `MailDetailPage`/`AdminUsersPage`)都从 y=0 开始画,而不只是 `MainPage`。 + * + * 所以只给 `MainPage` 加避让 = **把黑边换成了另一种坏**:同一次实测截图里, + * 写邮件页的「取消」与时钟「09:49」重叠、「发送」与 wifi/电量图标重叠。 + * 这正是上次退回的那种错(只做一半)—— 只是这次"一半"是**页面**而不是步骤。 + * + * ★ 所以判定口径是:**只要有人在窗口上开了全屏,每个 `@Entry` 页就必须消费它**。 + * 判据 `harmony-window.test.mjs` 按这个口径枚举所有 `@Entry` 页 —— + * 不是"检查 MainPage 做了没有"(那样新增一个推上来的页就静默逃掉)。 + * + * ★ 取 0 时(未全屏/取不到避让)这个函数就是 0,页面表现与修之前完全一样 —— + * 即"没用上就退回到旧行为",不会把一个不知情的页顶下去。 + */ +export function topInset(insets: Insets | undefined): number { + return insets !== undefined && insets.statusBar > 0 ? insets.statusBar : 0; +} diff --git a/client/harmony/entry/src/main/ets/pages/AdminUsersPage.ets b/client/harmony/entry/src/main/ets/pages/AdminUsersPage.ets index a16530c..f62bbeb 100644 --- a/client/harmony/entry/src/main/ets/pages/AdminUsersPage.ets +++ b/client/harmony/entry/src/main/ets/pages/AdminUsersPage.ets @@ -36,11 +36,23 @@ import { toggled, lastLoginLabel, messageOfApiError, isAdminRole, isRestricted } import { AmIcon } from '../common/Icons'; import { LIST_FADE_LENGTH } from '../model/NavItems'; import { LengthMetrics } from '@kit.ArkUI'; +import { Insets, KEY_WINDOW_INSETS, topInset } from '../model/WindowInsets'; @Entry @Component struct AdminUsersPage { @State users: AdminUser[] = []; + /** + * 窗口避让区 —— 由 `EntryAbility.setupFullScreenWindow` 写进 AppStorage。 + * + * ★ 推上来的页**也要**消费它:`setWindowLayoutFullScreen(true)` 是**窗口级**的, + * 一旦设上,这个窗口里**所有**用 `router.pushUrl` 推上来的页都从 y=0 开始画。 + * 本页顶栏原来贴 y=0 ⇒ 全屏后「取消/发送」会与时钟、wifi/电量图标重叠 + * (2026-09-18 实测截图硬证)。只给 `MainPage` 加避让就是把"黑边"换成 + * "顶栏被状态栏压住",同属只做一半。 + */ + @StorageLink(KEY_WINDOW_INSETS) windowInsets: Insets = new Insets(); + @State scopes: AdminScopes = new AdminScopes(); @State loading: boolean = false; @State errorText: string = ''; @@ -337,7 +349,8 @@ struct AdminUsersPage { AmIcon({ iconName: 'repeat', iconSize: 20, iconColor: Theme.accent }).width(40).height(40) .onClick(() => { this.load(); }) } - .width('100%').height(56).padding({ left: 8, right: 8 }) + /* 高度与 padding-top 一起加避让 —— 理由见 `ComposePage` 顶栏(同形状) */ + .width('100%').height(56 + topInset(this.windowInsets)).padding({ left: 8, right: 8, top: topInset(this.windowInsets) }) .backgroundColor(Theme.surface) } diff --git a/client/harmony/entry/src/main/ets/pages/ComposePage.ets b/client/harmony/entry/src/main/ets/pages/ComposePage.ets index 5adcd5d..f0fcba3 100644 --- a/client/harmony/entry/src/main/ets/pages/ComposePage.ets +++ b/client/harmony/entry/src/main/ets/pages/ComposePage.ets @@ -12,11 +12,23 @@ import { ComposeParams } from '../model/RouteParams'; import { hilog } from '@kit.PerformanceAnalysisKit'; import { picker } from '@kit.CoreFileKit'; import { AmIcon } from '../common/Icons'; +import { Insets, KEY_WINDOW_INSETS, topInset } from '../model/WindowInsets'; @Entry @Component struct ComposePage { @State to: string = ''; + /** + * 窗口避让区 —— 由 `EntryAbility.setupFullScreenWindow` 写进 AppStorage。 + * + * ★ 推上来的页**也要**消费它:`setWindowLayoutFullScreen(true)` 是**窗口级**的, + * 一旦设上,这个窗口里**所有**用 `router.pushUrl` 推上来的页都从 y=0 开始画。 + * 本页顶栏原来贴 y=0 ⇒ 全屏后「取消/发送」会与时钟、wifi/电量图标重叠 + * (2026-09-18 实测截图硬证)。只给 `MainPage` 加避让就是把"黑边"换成 + * "顶栏被状态栏压住",同属只做一半。 + */ + @StorageLink(KEY_WINDOW_INSETS) windowInsets: Insets = new Insets(); + @State subject: string = ''; @State body: string = ''; @State replyTo: string = ''; @@ -198,8 +210,15 @@ struct ComposePage { .enabled(!this.sending && !this.uploading) .onClick(() => { this.doSend(); }) } - .width('100%').height(56) - .padding({ left: 12, right: 12 }) + /* + * ★ 高度与 padding-top 一起加避让:只加 padding 会把 56 里切掉 39, + * 按钮被压扁;只加 height 内容还是贴着状态栏。 + * 背景照常铺到 y=0(`backgroundColor` 画在 border box 上,含 padding 区), + * 所以黑边不会回来 —— 「背景铺满、内容让开」正是 `MainPage` 的同一口径。 + * 取 0 时(未全屏)表达式的值与旧代码**逐字相同**:height=56、无 top。 + */ + .width('100%').height(56 + topInset(this.windowInsets)) + .padding({ left: 12, right: 12, top: topInset(this.windowInsets) }) .backgroundColor(Theme.surface) Divider().color(Theme.border) diff --git a/client/harmony/entry/src/main/ets/pages/InboxPage.ets b/client/harmony/entry/src/main/ets/pages/InboxPage.ets index dda1994..9f41622 100644 --- a/client/harmony/entry/src/main/ets/pages/InboxPage.ets +++ b/client/harmony/entry/src/main/ets/pages/InboxPage.ets @@ -10,11 +10,23 @@ import { router } from '@kit.ArkUI'; import { AmIcon } from '../common/Icons'; import { LIST_FADE_LENGTH } from '../model/NavItems'; import { LengthMetrics } from '@kit.ArkUI'; +import { Insets, KEY_WINDOW_INSETS, topInset } from '../model/WindowInsets'; @Entry @Component struct InboxPage { @State mails: MailSummary[] = []; + /** + * 窗口避让区 —— 由 `EntryAbility.setupFullScreenWindow` 写进 AppStorage。 + * + * ★ 推上来的页**也要**消费它:`setWindowLayoutFullScreen(true)` 是**窗口级**的, + * 一旦设上,这个窗口里**所有**用 `router.pushUrl` 推上来的页都从 y=0 开始画。 + * 本页顶栏原来贴 y=0 ⇒ 全屏后「取消/发送」会与时钟、wifi/电量图标重叠 + * (2026-09-18 实测截图硬证)。只给 `MainPage` 加避让就是把"黑边"换成 + * "顶栏被状态栏压住",同属只做一半。 + */ + @StorageLink(KEY_WINDOW_INSETS) windowInsets: Insets = new Insets(); + @State loading: boolean = false; @State limit: number = 20; @State total: number = 0; @@ -78,9 +90,10 @@ struct InboxPage { .padding({ left: 8, right: 8, top: 2, bottom: 2 }) } } + /* 高度与 padding-top 一起加避让 —— 理由见 `ComposePage` 顶栏(同形状) */ .width('100%') - .height(56) - .padding({ left: 16, right: 16 }) + .height(56 + topInset(this.windowInsets)) + .padding({ left: 16, right: 16, top: topInset(this.windowInsets) }) .backgroundColor(Theme.surface) // 列表 diff --git a/client/harmony/entry/src/main/ets/pages/MailDetailPage.ets b/client/harmony/entry/src/main/ets/pages/MailDetailPage.ets index cfb3ba9..748cf51 100644 --- a/client/harmony/entry/src/main/ets/pages/MailDetailPage.ets +++ b/client/harmony/entry/src/main/ets/pages/MailDetailPage.ets @@ -20,6 +20,7 @@ import { mailReplyTarget, formatAddress } from '../model/ReplyTarget'; +import { Insets, KEY_WINDOW_INSETS, topInset } from '../model/WindowInsets'; /** * 本地时间:与 WebUI `new Date(x).toLocaleString('zh-CN')` 同一口径。 @@ -553,10 +554,24 @@ export struct MailDetailView { @Entry @Component struct MailDetailPage { + /** + * 窗口避让区 —— 由 `EntryAbility.setupFullScreenWindow` 写进 AppStorage。 + * + * ★ 避让加在**这个 `@Entry` 包装层**,**不在** `MailDetailView` 里面: + * 同一个 `MailDetailView` 还被 `MainPage` 的 `Navigation` 内嵌复用 + * (`MailDetailDestination`),而 `MainPage` 已经给内容层加过避让了 —— + * 加在里面就变成**让两次**(39vp 变 78vp,白白多一条空档)。 + * 这类"同一个组件两种入口"的坑与 `harmony-nav` 里那条"悬浮加号要放在 + * Navigation 内部、不能当它的兄弟"同源:**让位的量取决于它被挂在哪一层**。 + */ + @StorageLink(KEY_WINDOW_INSETS) windowInsets: Insets = new Insets(); + build() { Column() { MailDetailView() } .width('100%').height('100%') + .padding({ top: topInset(this.windowInsets) }) + .backgroundColor(Theme.surface) } } diff --git a/client/harmony/entry/src/main/ets/pages/SessionsPage.ets b/client/harmony/entry/src/main/ets/pages/SessionsPage.ets index 77e2d53..962b20a 100644 --- a/client/harmony/entry/src/main/ets/pages/SessionsPage.ets +++ b/client/harmony/entry/src/main/ets/pages/SessionsPage.ets @@ -8,11 +8,23 @@ import { MailApi } from '../api/MailApi'; import { Session } from '../model/Models'; import { LIST_FADE_LENGTH } from '../model/NavItems'; import { LengthMetrics } from '@kit.ArkUI'; +import { Insets, KEY_WINDOW_INSETS, topInset } from '../model/WindowInsets'; @Entry @Component struct SessionsPage { @State sessions: Session[] = []; + /** + * 窗口避让区 —— 由 `EntryAbility.setupFullScreenWindow` 写进 AppStorage。 + * + * ★ 推上来的页**也要**消费它:`setWindowLayoutFullScreen(true)` 是**窗口级**的, + * 一旦设上,这个窗口里**所有**用 `router.pushUrl` 推上来的页都从 y=0 开始画。 + * 本页顶栏原来贴 y=0 ⇒ 全屏后「取消/发送」会与时钟、wifi/电量图标重叠 + * (2026-09-18 实测截图硬证)。只给 `MainPage` 加避让就是把"黑边"换成 + * "顶栏被状态栏压住",同属只做一半。 + */ + @StorageLink(KEY_WINDOW_INSETS) windowInsets: Insets = new Insets(); + @State loading: boolean = false; @State error: string = ''; @@ -54,8 +66,9 @@ struct SessionsPage { Text('会话') .fontSize(20).fontWeight(FontWeight.Bold).fontColor(Theme.textPrimary) } - .width('100%').height(56) - .padding({ left: 16 }) + /* 高度与 padding-top 一起加避让 —— 理由见 `ComposePage` 顶栏(同形状) */ + .width('100%').height(56 + topInset(this.windowInsets)) + .padding({ left: 16, top: topInset(this.windowInsets) }) .backgroundColor(Theme.surface) if (this.loading) {