跨端: 写邮件改在宽屏右栏打开(原来盖住全屏、把列表栏也带走)
用户:「写邮件 webui 的宽屏样式不是在右侧打开吗」—— 是,这是**结构性不符**。
WebUI `App.tsx:179` 宽屏下写信只是把 `main` 那一格换掉:
const main = composing ? <ComposePage /> : 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 宽屏):点悬浮加号 → 写信在**右栏**打开,
侧栏与列表栏都在;点「取消」→ 弹回右栏空态,列表与选中态不受影响。
This commit is contained in:
@ -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 ? <ComposePage/> : ...` ——
|
||||
* `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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 独立页包装(**窄屏**走这条:`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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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` 那一格换成 `<ComposePage/>`,
|
||||
* 侧栏与列表栏都还在。鸿蒙用 `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 ? <ComposePage/> : ...`),
|
||||
* 侧栏与列表栏都还在。
|
||||
*
|
||||
* 改走本页自己的 `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); }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user