harmony(ui): 登录页 1:1 对齐 WebUI 卡片规格 + 通信页改用系统 Navigation Auto

登录页(LoginPage.ets):对照 WebUI LoginPage.tsx 同规格重写 build()
- maxWidth 380vp 居中卡片(与 WebUI 380px 同档)
- 14vp 圆角(Theme.glassRadius)
- 48vp 品牌图标(Envelope 浅蓝底 accentSoft)
- 显式字段标签(服务器/用户名/密码/用户密钥)
- 40vp 输入框与登录按钮(WebUI 控件同高)
- 分段式登录方式切换(账号密码 / 用户密钥)
- 窄屏安全边距 16vp + Scroll 可滚

通信页(MainPage.ets CommPage):改用鸿蒙系统 Navigation 组件
- Navigation(navPathStack) 包住列表,navDestination 注册邮件详情目标页
- mode(NavigationMode.Auto):phone 自动 Stack、tablet/2in1 自动 Split
- navBarWidth(320) 对齐 WebUI MailList 320px;navBarWidthRange([280,360])
- minContentWidth(360) 参与 Auto 断点计算
- Inbox/Sent 选中邮件改走 navPathStack.pushPath,不再 router.pushUrl
- 切收件箱/发件箱/授权时清空详情栈避免跨栏残留

详情页(MailDetailPage.ets):抽出可复用 MailDetailView
- 既有详情实现导出为 @Component struct MailDetailView
- 新增 initialMailId/initialAccountId/embedded/onBack 输入
- 无 initialMailId 时才读路由参数(兼容旧 router 入口)
- embedded=true 时返回走 onBack 回调(NavPathStack.pop)
- 薄壳 @Entry MailDetailPage 用 Column 包裹(@Entry 根节点须容器)

module.json5:deviceTypes 扩展为 phone/tablet/2in1 三形态

判据:harmony-widescreen.test.mjs 新增 ⑥ 钉住 Navigation Auto 不许退回手搓分栏

验证:ArkTS 编译通过;harmony-arkts/widescreen/nav/cross-client-theme 35/35 通过
This commit is contained in:
2026-09-17 10:58:31 +08:00
parent f44b15684b
commit a5f680a147
5 changed files with 332 additions and 122 deletions

View File

@ -10,6 +10,7 @@ import { ApiClient, ApiError } from '../api/ApiClient';
import { Theme } from '../common/Theme';
import { AmIcon } from '../common/Icons';
import { WideSidebar } from './WideSidebar';
import { MailDetailView } from './MailDetailPage';
import { MailApi, InboxResponse } from '../api/MailApi';
import { AccountManager, AccountInfo } from '../api/AccountManager';
import { SseService, SseEvent } from '../api/SseService';
@ -81,11 +82,52 @@ import {
/** 一页取多少封。取满了就要如实提示"可能还有更多"(服务端 total 是未读数,不是总封数)。 */
const INBOX_PAGE_SIZE: number = 50;
const MAIL_DETAIL_ROUTE: string = 'mail-detail';
/**
* Navigation 目标页:按官方示例在 NavDestination.onReady 读取路径参数。
* 系统 Navigation 自动决定 Stack / Split;这里不读窗口宽度、不手搓分栏。
*/
@Component
struct MailDetailDestination {
@State mailId: string = '';
@State accountId: string = '';
private pathStack: NavPathStack = new NavPathStack();
handleReady(ctx: NavDestinationContext): void {
const params: MailDetailParams = ctx.pathInfo.param as MailDetailParams;
this.mailId = params?.mail_id ?? '';
this.accountId = params?.account_id ?? '';
this.pathStack = ctx.pathStack;
}
build() {
NavDestination() {
if (this.mailId.length > 0) {
MailDetailView({
initialMailId: this.mailId,
initialAccountId: this.accountId,
embedded: true,
onBack: (): void => { this.pathStack.pop(); }
})
} else {
Column() {
LoadingProgress().width(32).height(32)
}
.width('100%').height('100%').justifyContent(FlexAlign.Center)
}
}
.hideTitleBar(true)
.onReady((ctx: NavDestinationContext) => { this.handleReady(ctx); })
}
}
@Component
struct InboxTab {
/** 背景是否开启:开着就让出页面底(WebUI 的做法是页面底完全透明) */
@Prop bgActive: boolean = false;
/** 选中邮件回调:宽屏内联到 Navigation 右栏,窄屏 push 到目标页 */
onOpenMail: (mailId: string, accountId: string) => void = (): void => {};
@State mails: MailSummary[] = [];
/** 按会话折叠后的列表(单封的组平铺渲染) */
@State groups: SessionGroup[] = [];
@ -273,14 +315,7 @@ struct InboxTab {
}
openMail(mail: MailLike): void {
const params: MailDetailParams = {
mail_id: mail.mail_id,
account_id: mail.source_account_id
};
this.getUIContext().getRouter().pushUrl({
url: 'pages/MailDetailPage',
params: params
});
this.onOpenMail(mail.mail_id, mail.source_account_id);
}
openCompose(): void {
@ -611,6 +646,8 @@ struct InboxTab {
struct SentTab {
/** 背景是否开启:开着就让出页面底(WebUI 的做法是页面底完全透明) */
@Prop bgActive: boolean = false;
/** 选中邮件回调 */
onOpenMail: (mailId: string, accountId: string) => void = (): void => {};
@State groups: SessionGroup[] = [];
@State expandedKeys: string[] = [];
@State loading: boolean = false;
@ -681,11 +718,7 @@ struct SentTab {
}
openMail(mail: MailLike): void {
const params: MailDetailParams = {
mail_id: mail.mail_id,
account_id: mail.source_account_id
};
this.getUIContext().getRouter().pushUrl({ url: 'pages/MailDetailPage', params: params });
this.onOpenMail(mail.mail_id, mail.source_account_id);
}
@Builder
@ -1074,6 +1107,8 @@ struct CommPage {
@State commTab: string = 'inbox';
@State unreadCount: number = 0;
@State pendingCount: number = 0;
/** Navigation 路由栈:宽屏 Split 时列表+详情并排,窄屏 Stack 时详情 push 覆盖 */
private navPathStack: NavPathStack = new NavPathStack();
aboutToAppear(): void {
this.refreshCounts();
@ -1177,6 +1212,7 @@ struct CommPage {
.border({ width: { bottom: this.commTab === key ? 2 : 0 }, color: Theme.accent })
.onClick(() => {
this.commTab = normalizeCommTab(key);
this.navPathStack.clear();
this.refreshCounts();
})
}, (key: string) => key)
@ -1185,37 +1221,68 @@ struct CommPage {
.backgroundColor(Theme.surface)
}
openMail(mailId: string, accountId: string): void {
const params: MailDetailParams = {
mail_id: mailId,
account_id: accountId
};
this.navPathStack.pushPath({ name: MAIL_DETAIL_ROUTE, param: params });
}
@Builder
DestinationBuilder(name: string, param: Object) {
if (name === MAIL_DETAIL_ROUTE) {
MailDetailDestination()
}
}
build() {
Stack({ alignContent: Alignment.BottomEnd }) {
Column() {
Row() {
Text('通信').fontSize(20).fontWeight(FontWeight.Bold).fontColor(Theme.textPrimary)
Blank()
/*
* 设置入口。
* 图标用 `person`:与 WebUI 的「我的」(`NarrowNav.tsx` 的 `PersonIcon`)同一枚,
* 而设置页里正是账号 / 外观 / 密钥这些“属于这个人的东西”。
* 2026-09-15 之前这里是 emoji `⚙`(理由与替换过程见 `model/NavItems.ts`)。
*/
AmIcon({ iconName: 'person', iconSize: 20, iconColor: Theme.textMuted })
.width(36).height(36)
.onClick(() => {
this.getUIContext().getRouter().pushUrl({ url: 'pages/SettingsPage' });
})
}
.width('100%').height(52).padding({ left: 16, right: 8 })
.backgroundColor(Theme.surface)
Navigation(this.navPathStack) {
Column() {
Row() {
Text('通信').fontSize(20).fontWeight(FontWeight.Bold).fontColor(Theme.textPrimary)
Blank()
/*
* 设置入口。
* 图标用 `person`:与 WebUI 的「我的」(`NarrowNav.tsx` 的 `PersonIcon`)同一枚,
* 而设置页里正是账号 / 外观 / 密钥这些“属于这个人的东西”。
* 2026-09-15 之前这里是 emoji `⚙`(理由与替换过程见 `model/NavItems.ts`)。
*/
AmIcon({ iconName: 'person', iconSize: 20, iconColor: Theme.textMuted })
.width(36).height(36)
.onClick(() => {
this.getUIContext().getRouter().pushUrl({ url: 'pages/SettingsPage' });
})
}
.width('100%').height(52).padding({ left: 16, right: 8 })
.backgroundColor(Theme.surface)
this.CommTabBar()
this.CommTabBar()
if (this.commTab === 'sent') {
SentTab({ bgActive: this.bgActive })
} else if (this.commTab === 'permissions') {
PermissionTab({ bgActive: this.bgActive })
} else {
InboxTab({ bgActive: this.bgActive })
if (this.commTab === 'sent') {
SentTab({
bgActive: this.bgActive,
onOpenMail: (mailId: string, accountId: string): void => { this.openMail(mailId, accountId); }
})
} else if (this.commTab === 'permissions') {
PermissionTab({ bgActive: this.bgActive })
} else {
InboxTab({
bgActive: this.bgActive,
onOpenMail: (mailId: string, accountId: string): void => { this.openMail(mailId, accountId); }
})
}
}
.width('100%').height('100%')
}
.navDestination(this.DestinationBuilder)
.mode(NavigationMode.Auto)
/* WebUI 邮件列表栏固定约 320px;Auto 的断点 = 列表最小宽 + 详情最小宽。 */
.navBarWidth(320)
.navBarWidthRange([280, 360])
.minContentWidth(360)
.hideTitleBar(true)
.width('100%').height('100%')
/*