上一提交(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 断言,
都改成剥注释后验**形状与不变量**,而不是写死字面表达式。改完变异仍咬得住。
229 lines
7.8 KiB
Plaintext
229 lines
7.8 KiB
Plaintext
/*
|
||
* AgentMail 鸿蒙客户端 — 收件箱页
|
||
* GET /me/inbox 分页,列表展示邮件摘要
|
||
*/
|
||
import { ApiClient, ApiError } from '../api/ApiClient';
|
||
import { Theme } from '../common/Theme';
|
||
import { MailApi } from '../api/MailApi';
|
||
import { MailSummary, Me } from '../model/Models';
|
||
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;
|
||
@State unread: number = 0;
|
||
@State me: Me = new Me();
|
||
@State error: string = '';
|
||
|
||
private client: ApiClient | null = null;
|
||
private mailApi: MailApi | null = null;
|
||
|
||
aboutToAppear(): void {
|
||
const ctx = this.getUIContext().getHostContext();
|
||
if (ctx !== undefined) {
|
||
const client: ApiClient = ApiClient.getInstance(ctx);
|
||
this.client = client;
|
||
this.mailApi = new MailApi(client);
|
||
this.loadInbox();
|
||
}
|
||
}
|
||
|
||
async loadInbox(): Promise<void> {
|
||
const c: ApiClient | null = this.client;
|
||
const m: MailApi | null = this.mailApi;
|
||
if (c === null || m === null) {
|
||
return;
|
||
}
|
||
this.loading = true;
|
||
this.error = '';
|
||
try {
|
||
const resp = await m.inbox('all', this.limit);
|
||
this.mails = resp.mails;
|
||
this.total = resp.total;
|
||
// 客户端统计未读(API 未提供顶层 unread)
|
||
let unreadCount: number = 0;
|
||
for (let i = 0; i < this.mails.length; i++) {
|
||
if (this.mails[i].status === 'unread') {
|
||
unreadCount++;
|
||
}
|
||
}
|
||
this.unread = unreadCount;
|
||
} catch (e) {
|
||
const ae = e as ApiError;
|
||
this.error = ae.message.length > 0 ? ae.message : '加载失败';
|
||
} finally {
|
||
this.loading = false;
|
||
}
|
||
}
|
||
|
||
build() {
|
||
Column() {
|
||
// 顶栏
|
||
Row() {
|
||
Text('收件箱')
|
||
.fontSize(20).fontWeight(FontWeight.Bold).fontColor(Theme.textPrimary)
|
||
Blank()
|
||
if (this.unread > 0) {
|
||
Text(this.unread + ' 未读')
|
||
.fontSize(13).fontColor(Theme.surface)
|
||
.backgroundColor(Theme.danger)
|
||
.borderRadius(10)
|
||
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
|
||
}
|
||
}
|
||
/* 高度与 padding-top 一起加避让 —— 理由见 `ComposePage` 顶栏(同形状) */
|
||
.width('100%')
|
||
.height(56 + topInset(this.windowInsets))
|
||
.padding({ left: 16, right: 16, top: topInset(this.windowInsets) })
|
||
.backgroundColor(Theme.surface)
|
||
|
||
// 列表
|
||
if (this.loading) {
|
||
Column() {
|
||
LoadingProgress().width(40).height(40)
|
||
Text('加载中…').fontSize(14).fontColor(Theme.textSubtle).margin({ top: 8 })
|
||
}
|
||
.width('100%').height('80%')
|
||
.justifyContent(FlexAlign.Center)
|
||
} else if (this.error.length > 0) {
|
||
Column() {
|
||
Text(this.error).fontSize(14).fontColor(Theme.danger)
|
||
Button('重试').margin({ top: 12 }).onClick(() => { this.loadInbox(); })
|
||
}
|
||
.width('100%').height('80%')
|
||
.justifyContent(FlexAlign.Center)
|
||
} else if (this.mails.length === 0) {
|
||
Column() {
|
||
Text('收件箱为空').fontSize(16).fontColor(Theme.textSubtle)
|
||
}
|
||
.width('100%').height('80%')
|
||
.justifyContent(FlexAlign.Center)
|
||
} else {
|
||
List({ space: 1 }) {
|
||
ForEach(this.mails, (mail: MailSummary) => {
|
||
ListItem() {
|
||
this.MailItem(mail)
|
||
}
|
||
.height(80)
|
||
.backgroundColor(mail.status === 'unread' ? Theme.accentSoft : Theme.surface)
|
||
.onClick(() => {
|
||
this.getUIContext().getRouter().pushUrl({
|
||
url: 'pages/MailDetailPage',
|
||
params: { mail_id: mail.mail_id }
|
||
});
|
||
})
|
||
}, (mail: MailSummary) => mail.mail_id)
|
||
}
|
||
.width('100%')
|
||
.layoutWeight(1)
|
||
/* 上下边缘渐隐(2026-09-17 补:原先漏了,同一次滚动里只有部分是渐隐) */
|
||
.fadingEdge(true, { fadingEdgeLength: LengthMetrics.vp(LIST_FADE_LENGTH) })
|
||
.divider({ strokeWidth: 1, color: Theme.border, startMargin: 16, endMargin: 16 })
|
||
}
|
||
|
||
// 底部 Tab 栏(预留系统导航栏空间)
|
||
Column() {
|
||
Row() {
|
||
Column() {
|
||
AmIcon({ iconName: 'inbox', iconSize: 20, iconColor: Theme.accent })
|
||
Text('收件箱').fontSize(11).fontColor(Theme.accent)
|
||
}
|
||
.layoutWeight(1).height('100%')
|
||
.justifyContent(FlexAlign.Center)
|
||
.backgroundColor(Theme.accentSoft)
|
||
|
||
Column() {
|
||
AmIcon({ iconName: 'chatBubble', iconSize: 20, iconColor: Theme.textMuted })
|
||
Text('会话').fontSize(11).fontColor(Theme.textMuted)
|
||
}
|
||
.layoutWeight(1).height('100%')
|
||
.justifyContent(FlexAlign.Center)
|
||
.onClick(() => {
|
||
this.getUIContext().getRouter().pushUrl({ url: 'pages/SessionsPage' });
|
||
})
|
||
|
||
Column() {
|
||
AmIcon({ iconName: 'contacts', iconSize: 20, iconColor: Theme.textMuted })
|
||
Text('联系人').fontSize(11).fontColor(Theme.textMuted)
|
||
}
|
||
.layoutWeight(1).height('100%')
|
||
.justifyContent(FlexAlign.Center)
|
||
}
|
||
.width('100%').height(56)
|
||
.backgroundColor(Theme.surface)
|
||
}
|
||
.width('100%')
|
||
}
|
||
.width('100%').height('100%')
|
||
.backgroundColor(Theme.pageBg)
|
||
}
|
||
|
||
@Builder
|
||
MailItem(mail: MailSummary) {
|
||
Row() {
|
||
// 未读圆点
|
||
Column() {
|
||
if (mail.status === 'unread') {
|
||
Circle({ width: 8, height: 8 }).fill(Theme.accent)
|
||
}
|
||
}
|
||
.width(20).height('100%')
|
||
.justifyContent(FlexAlign.Center)
|
||
|
||
// 正文
|
||
Column() {
|
||
Row() {
|
||
Text(mail.from_name).fontSize(14).fontWeight(mail.is_read ? FontWeight.Normal : FontWeight.Bold).fontColor(Theme.textPrimary)
|
||
Blank()
|
||
Text(mail.created_at).fontSize(11).fontColor(Theme.textSubtle)
|
||
}
|
||
.width('100%')
|
||
|
||
Text(mail.subject).fontSize(13).fontColor(Theme.textPrimary)
|
||
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
|
||
.margin({ top: 2 })
|
||
Text(mail.body_preview).fontSize(12).fontColor(Theme.textSubtle)
|
||
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
|
||
.margin({ top: 2 })
|
||
}
|
||
.layoutWeight(1).height('100%')
|
||
.alignItems(HorizontalAlign.Start)
|
||
.padding({ left: 8 })
|
||
|
||
// 附件图标
|
||
if (mail.has_attachments) {
|
||
AmIcon({ iconName: 'paperclip', iconSize: 14, iconColor: Theme.textMuted }).margin({ right: 8 })
|
||
}
|
||
|
||
// 权限档位徽标
|
||
if (mail.permission_mode.length > 0) {
|
||
Text(mail.permission_mode)
|
||
.fontSize(10).fontColor(Theme.textMuted)
|
||
.backgroundColor(Theme.border).borderRadius(4)
|
||
.padding({ left: 4, right: 4, top: 1, bottom: 1 })
|
||
}
|
||
}
|
||
.width('100%').height('100%')
|
||
.padding({ left: 12, right: 12 })
|
||
.alignItems(VerticalAlign.Center)
|
||
}
|
||
} |