跨端: 鸿蒙修收信/自动已读/发件箱点开 + 页签条通透(用户报的四个问题)
用户报了四个问题,逐个实测复现 → 定位根因 → 修 → 设备复验:
① 「邮件点进去自动已读的能力不正常」
根因:鸿蒙只有「标记已读」按钮(doMarkRead,对照 WebUI MailView.tsx:522
那个手动按钮),缺 WebUI 的**自动**路径(MailView.tsx:55-65 的 useEffect:
可见且 unread 就 markRead)。⇒ 点开邮件不变已读,必须再点按钮。
修:MailDetailPage.loadMail 成功尾端按 `status==='unread'` 触发 doMarkRead
(复用按钮那条路,因而天然带上「就地改 status」+「失败弹 toast」)。
★ 加 autoReadMailId 守卫:WebUI 靠 useEffect 依赖数组天然只跑一次,
鸿蒙 loadMail 是显式调用的(下拉刷新会重跑),不守会重复打接口。
② 「接收邮件的能力也有点不正常」
三个独立缺口,每个都会单独造成"收不到":
a) **SSE 监听寿命**:原挂在 InboxTab.aboutToAppear,而三个 tab 是
if/else 条件挂载的 ⇒ 切到发件箱/授权时 InboxTab 被销毁、监听跟着
移除 ⇒ 在那两栏时收不到任何新邮件。搬到 MainPage(@Entry,全程在)。
b) **只处理 new_mail**:WebUI 监听 5 种事件(sse.ts:7-13 的 EVENTS);
服务端权限决策后发的是 session_update(permission.go:387)⇒
"授权栏里处理过的申请,收件箱还是旧的样子"。补齐四种。
c) **UTF-8 解码错**:arrayBufferToString 逐字节 String.fromCharCode
(Latin-1 语义)⇒ 中文解成乱码。改用 util.TextDecoder + stream:true,
且**每连接一个实例**(stream 会把半截汉字存在解码器内部,
共享实例会把两个账号的半个字拼在一起)。
③ 「发件箱内容点不开」(第二轮;5483140 补了 onClick 仍点不开)
根因:发件箱对单封组**既画组头又画行**——
this.SentGroupHeader(g); if (isFlatGroup(g)) { this.SentRow(...) }
组头那半张卡没有任何点击处理 ⇒ 点卡片上半部完全无反应。
而 isFlatGroup 自己的注释写着「单封不成组:套一个可折叠的组头只是
多一次点击」—— 实现与注释**直接相反**。收件箱一直是正确形状
(if (isFlatGroup) { MailRow } else { 组头 + 子行 })。
修:与收件箱取同形,单封组只画 SentRow。
④ 「通信页面我觉得没有 webui 那么通透」
这是我自己上一轮改错的:把 WebUI 的「页签条无背景」实现成了
`backgroundColor(Theme.surface)` 实心白。WebUI 的真实层叠是
.comm-pane 是玻璃卡、CommTabs 在它内部且**自己无背景**(透出卡的白)。
铺实心白 = 把玻璃卡换成横条白 ⇒ 壁纸再也透不过来。
⇒ 改回玻璃族(Theme.navMaterial,与底部导航条同档)+ 通栏 +
只左上圆角(右上 0,与窗格那道弧重合)+ 底边线。
判据 harmony-nav.test.mjs:732 在我改错时当场判红,是它先抓到的。
判据(变异验证:还原 bug → 必须判红)
- 新增「单封组不许既画组头又画行」:两栏的 (header, row) 对必须在
else/三目里二选一。
★ 第一版写弱了(用 isFlatGroup(g) 作锚点往后切片,组头在切片之前
⇒ 变异测不红)。改成以**行调用**作锚点往两边开窗后,删掉修复
即判红(实测已验)。
- 新增「列表行必须把 onClick 挂在自己身上」(MailRow/SentRow)。
- harmony-logic 34→37、harmony-nav 22(新增后仍绿)、
harmony-appearance 27→28、animation-audit 12→15 的登记数同步。
设备复验(全部有实测凭据,不是推断)
- 自动已读:点开前 unread → 点开 4s 后服务端 read(连验两封)。
列表组头 4→2、侧栏徽标 4→2,三处数字一致。
- 实时收信:App 保持前台不重启,从 gateway 发信 ⇒ 8s 内自动出现
(新卡片 + 侧栏 2→3 + 页签 2→3 + 3 组 8 封)。
- 发件箱点开:点原先点不动的卡片上半区 ⇒ 右栏出正文 + 蓝色选中态。
- 页签条:截图确认为玻璃通透(不再是实心白横条)。
This commit is contained in:
@ -163,8 +163,24 @@ struct InboxTab {
|
||||
* ArkTS 的静态常量没有那层机制)。窗格自己算不出深浅色 ⇒ 走 AppStorage 读。
|
||||
*/
|
||||
@StorageProp('agentmail.appearance.isDark') isDarkNow: boolean = false;
|
||||
@State mails: MailSummary[] = [];
|
||||
/** 按会话折叠后的列表(单封的组平铺渲染) */
|
||||
/**
|
||||
* 邮件列表修订号(`MailStore.bump()` / `notifyRemoteChange()` 发布)。
|
||||
*
|
||||
* ★★ 2026-09-24 新增(用户:「接收邮件不正常」+「自动已读不正常」)。
|
||||
*
|
||||
* 本栏只在 `aboutToAppear` 拉一次;而 `Navigation` split 模式下列表常驻,
|
||||
* 从详情页返回、或从别的 tab 切回来时**都不会重拉**。
|
||||
* ⇒ 两种症状:
|
||||
* ① 在详情页标了已读,返回列表那行**还是蓝点**;
|
||||
* ② SSE 告知来了新邮件,但切过去看还是旧的(除非杀进程重进)。
|
||||
*
|
||||
* 现在监听这个键:详情页标已读、或 SSE 报新邮件 → store 自增 → 本栏重拉。
|
||||
*
|
||||
* ★ 用 `@StorageProp` 而不是与 `InboxTab` 的 `@State` 双向绑定:
|
||||
* 这里只需要“变化时醒一下”,值本身不用读(窗口的其它 `@StorageProp` 同形)。
|
||||
*/
|
||||
@StorageProp('agentmail.mail.revision') @Watch('onMailRevChanged') mailRev: number = 0;
|
||||
@State mails: MailSummary[] = []; /** 按会话折叠后的列表(单封的组平铺渲染) */
|
||||
@State groups: SessionGroup[] = [];
|
||||
/** 已展开的组(键 = SessionGroup.key) */
|
||||
@State expandedKeys: string[] = [];
|
||||
@ -198,12 +214,10 @@ struct InboxTab {
|
||||
* 两个 tab 各存一份必然会分叉。单点写、多点读。
|
||||
*/
|
||||
@Prop currentMailId: string = '';
|
||||
private sseService: SseService | null = null;
|
||||
|
||||
aboutToAppear(): void {
|
||||
const ctx = this.getUIContext().getHostContext();
|
||||
if (ctx !== undefined) {
|
||||
this.sseService = SseService.getInstance();
|
||||
// 加载账号列表
|
||||
const acctMgr: AccountManager = AccountManager.getInstance(ctx);
|
||||
acctMgr.load().then(() => {
|
||||
@ -217,30 +231,36 @@ struct InboxTab {
|
||||
}
|
||||
});
|
||||
this.loadData();
|
||||
// 全局 SSE 监听(所有账号的事件都会收到);保存同一函数引用以便页面退出时移除。
|
||||
this.sseService.addListener(this.onSseEvent);
|
||||
/*
|
||||
* ★★ 2026-09-24:SSE 监听**已搬到 `MainPage`**(App 级,全程在)——
|
||||
* 这里不再注册。
|
||||
*
|
||||
* 原处注册的致命问题:本栏是条件挂载的,切到发件箱/授权就被销毁,
|
||||
* 监听跟着没了。详细的因果写在 `MainPage.aboutToAppear` 里。
|
||||
*
|
||||
* ★ 留着会双重触发(两层都收同一事件)—— 必须只有一处注册。
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
aboutToDisappear(): void {
|
||||
if (this.sseService !== null) {
|
||||
this.sseService.removeListener(this.onSseEvent);
|
||||
}
|
||||
/* 本栏不再持有 SSE 监听,所以这里没有 `removeListener`;
|
||||
原先那两行已随注册一起移走。 */
|
||||
}
|
||||
|
||||
private onSseEvent = (event: SseEvent): void => {
|
||||
if (event.type === 'new_mail') {
|
||||
let sourceName: string = '';
|
||||
for (let i = 0; i < this.accountList.length; i++) {
|
||||
if (this.accountList[i].id === event.accountId) {
|
||||
sourceName = this.accountList[i].displayName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.getUIContext().getPromptAction().showToast({ message: sourceName.length > 0 ? '新邮件:' + sourceName : '新邮件到达' });
|
||||
this.loadData();
|
||||
}
|
||||
};
|
||||
/**
|
||||
* 邮件修订号变了 ⇒ 列表需要重拉。
|
||||
*
|
||||
* 两种来源都走这里(见字段上的注释):
|
||||
* · 详情页标了已读(`MailStore.markReadLocal`);
|
||||
* · SSE 报新邮件(`MailStore.notifyRemoteChange`)。
|
||||
*
|
||||
* ★ 只重拉、不做“就地改本栏 `@State`”—— `loadInbox` 内部已经处理了
|
||||
* “已有列表时用服务端结果覆盖”,重拉一次比本栏自己拼一份可靠。
|
||||
*/
|
||||
onMailRevChanged(): void {
|
||||
this.loadData();
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉收件箱。
|
||||
@ -499,13 +519,19 @@ struct InboxTab {
|
||||
.bindMenu($$this.showAccountPicker, this.AccountFilterMenu())
|
||||
.onClick(() => { this.showAccountPicker = !this.showAccountPicker; })
|
||||
}
|
||||
if (this.unread > 0) {
|
||||
Text(this.unread > 99 ? '99+' : this.unread.toString())
|
||||
.fontSize(10).fontWeight(FontWeight.Bold).fontColor(Theme.accentFg)
|
||||
.backgroundColor(Theme.danger).borderRadius(9)
|
||||
.constraintSize({ minWidth: 18 }).height(18)
|
||||
.textAlign(TextAlign.Center).margin({ left: 8 })
|
||||
}
|
||||
/*
|
||||
* ★★ 2026-09-24 **删掉列表头的未读红圈**(用户:「通信页面 webui 和 app
|
||||
* 存在很大的区别」)。
|
||||
*
|
||||
* WebUI 的列表头(`MailList.tsx:74-84`)只有三个东西:
|
||||
* ① `<h2>` 收件箱/发件箱;
|
||||
* ② `AccountSwitcher`(多账号才出现);
|
||||
* ③ 计数(`N 组 · M 封`)。
|
||||
* **没有未读徽标** —— 未读只在页签条上(`CommTabs` 的红色胶囊)。
|
||||
*
|
||||
* 这里多出来的红圈是鸿蒙自己加的:同一屏上就会看到两个未读数
|
||||
* (页签条一个、列表头一个),而它们是同一个数字,看着像两个指示。
|
||||
*/
|
||||
}
|
||||
.width('100%').height(46)
|
||||
.padding({ left: 14, right: 12 })
|
||||
@ -983,11 +1009,24 @@ struct SentTab {
|
||||
@State loading: boolean = false;
|
||||
@State error: string = '';
|
||||
@State loaded: number = 0;
|
||||
/*
|
||||
* 邮件修订号 —— 与 `InboxTab` 那一个**同键同形**(完整理由写在那里)。
|
||||
*
|
||||
* ★ 2026-09-24:发件箱同样只在 `aboutToAppear` 拉一次,
|
||||
* SSE 报新邮件 / 详情页改了状态时不会重拉。两栏是同一块
|
||||
* “切走了就不刷新”的病 ⇒ 同一个键、同一套写法,不许分叉。
|
||||
*/
|
||||
@StorageProp('agentmail.mail.revision') @Watch('onMailRevChanged') mailRev: number = 0;
|
||||
|
||||
aboutToAppear(): void {
|
||||
this.load();
|
||||
}
|
||||
|
||||
/** 修订号变了 ⇒ 重拉(与 `InboxTab.onMailRevChanged` 同形同理由) */
|
||||
onMailRevChanged(): void {
|
||||
this.load();
|
||||
}
|
||||
|
||||
/**
|
||||
* 取数**交给 `MailStore`**(与收件箱同一份实现)。
|
||||
*
|
||||
@ -1262,8 +1301,30 @@ struct SentTab {
|
||||
ForEach(this.groups, (g: SessionGroup) => {
|
||||
ListItem() {
|
||||
Column() {
|
||||
this.SentGroupHeader(g)
|
||||
if (isFlatGroup(g)) {
|
||||
/*
|
||||
* ★★ 2026-09-24 修(用户:「发件箱内容也点不开」——补了 `.onClick` 仍点不开)。
|
||||
*
|
||||
* ── 根因:单封组多渲染了一个**不可点的组头** ──
|
||||
* 这里原来是“先无条件渲染 `SentGroupHeader(g)`,单封组再叠一个 `SentRow`”。
|
||||
* 于是一张卡的上半部(组头:别名/主题/N 封)**没有任何点击处理**,
|
||||
* 只有下半部(`SentRow`:致 xxx/正文预览)能点。实测点 y=630
|
||||
* (组头区)完全无反应 —— 而人点卡片时很难正好落在下半部。
|
||||
*
|
||||
* 更矛盾的是:`isFlatGroup` 的注释自己写着
|
||||
* 「单封不成组:套一个可折叠的组头只是多一次点击」
|
||||
* —— 而这里恰恰就给它套了组头,**注释与实现直接相反**。
|
||||
*
|
||||
* 收件箱(`InboxTab`,L593)一直是正确形状:
|
||||
* if (isFlatGroup(g)) { this.MailRow(g.mails[0]) }
|
||||
* else { 组头 + 子行 }
|
||||
* ⇒ 与它取同形:**单封组只渲染 `SentRow`,不渲染组头**。
|
||||
*
|
||||
* ★ 两者取同形之后,上一轮补在 `SentRow` 自己身上的 `.onClick`
|
||||
* 才真正覆盖整张卡(原来只覆盖了半张)。
|
||||
*/
|
||||
if (!isFlatGroup(g)) {
|
||||
this.SentGroupHeader(g)
|
||||
} else {
|
||||
this.SentRow(g.mails[0])
|
||||
}
|
||||
}
|
||||
@ -1590,122 +1651,106 @@ struct CommPage {
|
||||
}, (key: string) => key)
|
||||
}
|
||||
/*
|
||||
* ★★ 2026-09-20(用户:「顶部的收件箱发件箱和授权为什么不做成悬浮玻璃?」)
|
||||
* ─────────────────────── 页签条造型:三次反转记在这里 ───────────────────────
|
||||
*
|
||||
* 这一行原来是 `backgroundColor(Theme.surface)` —— 系统卡片色,**实体**。
|
||||
* 于是它是整页唯一一块实心白:上一轮把列表容器、卡片、底部导航条
|
||||
* 都玻璃化了,唯独漏了这条页签条 ⇒ 壁纸从四周透出来,只有它在顶上白着一横条。
|
||||
* ① **09-20 之前**:`backgroundColor(Theme.surface)`(系统卡片色,实心白)。
|
||||
* 当时它是整页唯一一块实心白 —— 别的都玻璃化了、只有它白着一横条。
|
||||
* 用户:「顶部的收件箱发件箱和授权为什么不做成悬浮玻璃?」
|
||||
*
|
||||
* ★★ 2026-09-24 **推翻上述方向**(见下面 `跟 webui 同步` 那段):
|
||||
* 当时把它做成了“悬浮玻璃条”(材质 + 圆角),依据是“与底部条同一语汇”。
|
||||
* 但底部条是**圆角浮条**(四周都是边),页签条是**贴顶通栏 44vp** ——
|
||||
* 同一种系统材质在两种几何下表现不同:贴顶那条把材质自带的
|
||||
* 方向性明暗暴露成了下缘一条暗带(用户:「顶栏莫名其妙的底部阴影」)。
|
||||
* ⇒ 现在改成 WebUI 的形状:**无材质的通栏条 + 底边分隔线**。
|
||||
* ② **09-20 照做**:改成**悬浮玻璃条**(`Theme.navMaterial` + 圆角 + 左右留白),
|
||||
* 依据记在 `NavItems.ts`:
|
||||
* 「底部导航条已经确立了这个语汇,顶部再做一个通栏的,
|
||||
* 会在同一条轴线上出现两种不同的"条"」
|
||||
* ★ 那个推理**漏了一件事**:底部条是**独立悬浮在内容之上**的(该是浮条),
|
||||
* 而页签条是**列表栏的一部分** —— 两者不是同一类东西。
|
||||
* 代价当场就来了:贴顶通栏 44vp 的几何把系统材质**自带的方向性明暗**
|
||||
* 暴露成下缘一条暗带。用户:「顶栏莫名其妙的底部阴影」。
|
||||
*
|
||||
* 为什么"不做成实心白"仍然成立:WebUI 那条也是**透明的**
|
||||
* (`.comm-pane` 给它 `transparent`,`index.css:1641`)——
|
||||
* 它只是没有**材质**,不是有实心底。两者别混。
|
||||
*/
|
||||
/*
|
||||
* 宽度:**与窗格齐平**(`100%`)。
|
||||
* ③ **09-24 改回通栏**(用户并排看 WebUI/App 后:「通信页面 webui 和 app
|
||||
* 存在很大的区别」+「去吧」)。WebUI 的真实形状(`App.tsx:214-216`):
|
||||
* <div className="comm-pane relative flex-1 …">
|
||||
* <CommTabs /> ← 「shrink-0 … px-3 border-b border-gray-200」
|
||||
* {listBody}
|
||||
* 页签条与列表**同属一个 `.comm-pane`**,它是那张卡顶上的**一条边**、
|
||||
* 不是一块浮起来的板。
|
||||
*
|
||||
* 原来这里是 `calc(100% - 2×TAB_BAR_SIDE)`(左右各内缩 16vp)。
|
||||
* 实测 WebUI:`.comm-pane` 与 `tabstrip` 的 `x`/`w` **完全相同**(都是 80/320)
|
||||
* —— 它靠**窗格的 `overflow:hidden`** 把顶角裁圆,所以页签条自己直角、通栏。
|
||||
* 我们内缩 + 自己带角 ⇒ 右端出现两道弧(用户:「你又在内部套了一个胶囊」)。
|
||||
* ⇒ 改成与窗格齐平,右端只剩窗格那一道边。
|
||||
*/
|
||||
/*
|
||||
* ★★ 2026-09-24 改为**照 WebUI 同步**(用户:「跟 webui 同步」,
|
||||
* 上下文是他刚指出「顶栏莫名其妙的底部阴影」)。
|
||||
* ── 现在的形状(逐条对齐 WebUI)──
|
||||
* · **通栏**:`width('100%')`,无左右留白、无圆角
|
||||
* (`TAB_BAR_SIDE`/`TAB_BAR_RADIUS` 已在 09-21 删除,理由见 `NavItems.ts`);
|
||||
* · **无材质**:WebUI 的页签条没有 `backdrop-filter`,只有一条底边线;
|
||||
* · **有底色**:`Theme.surface`(= WebUI 列表栏的 `bg-white`)。
|
||||
* ★ 这与"不做成悬浮玻璃"**不冲突** —— 那是"材质 vs 实色",
|
||||
* 这是"有底色 vs 透明"。WebUI `index.css:1648` 写得很明确:
|
||||
* 「列表/详情的外框在壁纸模式下让位给"每项一张卡",
|
||||
* 但**工具条与头部**仍要有底色,否则会直接压在壁纸上读不清」
|
||||
* · **底边线**:`Theme.border`(WebUI 的 `border-b border-gray-200`)。
|
||||
*
|
||||
* ── 那条暗带是什么(像素实测,不是猜)──
|
||||
* 详情页左栏 x=500 逐像素:
|
||||
* y=142 lum=244 ↓ 单调递减(没有回弹)
|
||||
* y=262 lum=225 ← 落差 19
|
||||
* 对照证据:
|
||||
* · 同一高度的**纯壁纸区**(x=210 / x=3170)恒为 210-212,**没有任何渐变**
|
||||
* ⇒ 不是“壁纸透出来”;
|
||||
* · 全仓 `.shadow(` 只有两处(`PaneModifier` 与登录页),页签条自己没有
|
||||
* ⇒ 不是外部投影(我先前以为是,给它加了底边 —— **渐变照旧**,
|
||||
* 那个诊断当场被推翻)。
|
||||
* ⇒ 渐变来自**页签条自身的 `BlurStyle.COMPONENT_THICK`**:这类系统材质自带
|
||||
* 方向性明暗(顶部亮、底部暗)用来表达“浮起的立体条”。底部导航条因为是
|
||||
* **圆角浮条**(四周都是边)看不出,而页签条是**贴顶通栏 44vp**,
|
||||
* 就把这个渐变直接暴露成下缘一条暗带。
|
||||
*
|
||||
* ── 为什么“对齐 WebUI”等于**去掉材质** ──
|
||||
* WebUI 的页签条根本不是浮条,是**无材质的通栏条**:
|
||||
* className="shrink-0 flex items-stretch gap-1 px-3 **border-b border-gray-200**"
|
||||
* (`CommTabs.tsx:40`)。它自己的注释写了两条理由:
|
||||
* ① 「与**列表头**同一套观感(同内边距、同下边框)」;
|
||||
* ② 用户 2026-09-14:「通信页面的二级页面与其他位置极其割裂」——
|
||||
* 之前那个白胶囊带 shadow 浮在面板上,看着是硬贴上去的另一套控件。
|
||||
* ⇒ “悬浮玻璃页签条”是本仓 09-20 自己的发明(当时依据是“与底部条同一语汇”),
|
||||
* 而底部条是圆角浮条、页签条是贴顶通栏 —— **同材质在两种几何下表现不同**,
|
||||
* 这一点当时漏了。现在回到 WebUI 的形状。
|
||||
*
|
||||
* ── 同时去掉:圆角、以及上一轮为诊断加的底边 ──
|
||||
* WebUI 里页签条是**唯一不圆角的那一个**(其余面板各自 `radius-card`,
|
||||
* 见 `index.css:1371` 的 `.comm-pane > *:not([data-testid='comm-tabs'])`)。
|
||||
* 底边则**保留 WebUI 本来就有**的那条(`border-b border-gray-200`)——
|
||||
* 它不是为诊断加的,是 WebUI 的分界线,对应 `Theme.border`(同为分隔线语义)。
|
||||
* ── 不随本次反转回退的那次修复 ──
|
||||
* 页签条下方曾有一条**渐变暗带**。真凶不是页签条自己,而是 `InboxTab`
|
||||
* 根容器的投影向上扩散压住了它(几何取证:页签条 y142→271、
|
||||
* InboxTab y271→2202,观测到的渐变区 y240→268 完全吻合)。
|
||||
* 修在 `InboxTab` 上(`PaneModifier.plain`),与本次造型反转无关,保留。
|
||||
*/
|
||||
.width('100%')
|
||||
/*
|
||||
* ★★ 2026-09-24 **必须有底色** —— 这是“顶栏底部阴影”的真正修因。
|
||||
* ★★★ 2026-09-24 **改回通栏**(用户:「通信页面 webui 和 app 存在很大的区别」+「去吧」)。
|
||||
*
|
||||
* 上面那段说的是“不做成**实心白卡片**”,那是**实心 vs 玻璃**的区别;
|
||||
* 而这里是另一个问题:**有底色 vs 透明**。
|
||||
* 我把材质去掉之后写成了 `Color.Transparent`,于是页签条**直接压在壁纸上**
|
||||
* —— 实测那条暗带仍然存在(x 250→1150 贯通、亮度恒 224-227,
|
||||
* 且页签条内部从上到下 245→227 单调递减)。
|
||||
* ── 这次只是**几何上的反转**,不是材质上的 ──
|
||||
*
|
||||
* ── WebUI 怎么做的(关键那段注释)──
|
||||
* 它明确区分了“列表容器”与“工具条/头部”(`index.css:1648`):
|
||||
* 09-20 用户问:「顶部的收件箱发件箱和授权为什么不做成悬浮玻璃?」
|
||||
* 我把它做成了**带左右留白的浮条**(材质 + 圆角 + `TAB_BAR_SIDE`)。
|
||||
* 用户 09-21 又连指三次,最后裁定
|
||||
* 「你右边改成没圆角不就行了」「你又在内部套了一个胶囊」
|
||||
* ⇒ 那次改的是**几何**:不再内缩、不再自己做满圆角,而与窗格齐平。
|
||||
*
|
||||
* 列表/详情的外框在壁纸模式下让位给“每项一张卡”,但**工具条与头部**
|
||||
* 仍要有底色,否则会直接压在壁纸上读不清 —— 所以只把列表容器那一层放透明。
|
||||
* ★ 玻璃这一半一直是保留的(用户 09-20 明确要的),判据
|
||||
* `顶部页签条与底部导航条同一族` 钉的就是它。
|
||||
* 我这次一度把它一并推翻(改成 `Theme.surface` 实心),那是把**几何反转误当成材质反转** ——
|
||||
* 把用户当初点名要的东西删掉了,也正是这次「不够通透」的直接原因。
|
||||
*
|
||||
* 而页签条的父层级(`.comm-pane`)是 `bg-white`(`MailList.tsx:73`),
|
||||
* 所以页签条背后**是白的**,不是壁纸。
|
||||
* ⇒ "不计材质" 不等于 "透明"。我上一版把这二者搞成同一件事了。
|
||||
* ── WebUI 的真实结构(几何 + 材质的完整解释)──
|
||||
* 底部导航条是**独立悬浮在内容之上**的(它确实该是浮条);
|
||||
* 而页签条是**列表栏顶上的那条边** —— WebUI 里 `CommTabs` 与 `MailList`
|
||||
* 同属 `.comm-pane`(`App.tsx:214-216`):
|
||||
* <div className="comm-pane relative flex-1 …">
|
||||
* <CommTabs /> ← className="shrink-0 … px-3 border-b border-gray-200"
|
||||
* {listBody} ← 自己带 .glass-card 头部
|
||||
* 它**自己无背景**,透出的是底下那张玻璃卡 —— 这才是它"通透"的来源。
|
||||
* 所以:几何上它随窗格(通栏、只左上圆角),材质上它仍是玻璃族。
|
||||
*
|
||||
* `Theme.surface` = `ohos_id_color_list_card_bg`(浅色白/深色深灰,
|
||||
* 自动跟随主题)—— 它就是 WebUI `--c-white`(`255 255 255` / 深色 `24 27 33`)
|
||||
* 的对应物。用令牌而不是手写白:深色主题才改得动。
|
||||
* ── 按 WebUI 对齐后的形状 ──
|
||||
* · **通栏**:`width('100%')`,无左右留白;
|
||||
* · **无实心面**、**吃系统玻璃**:与底部导航条同档(`Theme.navMaterial`);
|
||||
* · **只给左上圆角**(右上 0):与窗格左上角那道弧重合;
|
||||
* · **底边线**:`Theme.border`(WebUI 的 `border-b border-gray-200`)。
|
||||
*
|
||||
* ── ★★★ 2026-09-24 再纠一次(同一个错我前后犯了两次)──
|
||||
*
|
||||
* 我上一版把"无背景"实现成了 `backgroundColor(Theme.surface)`,
|
||||
* 理由是「WebUI 列表栏是 `bg-white`」—— **那个引用是错的**。
|
||||
* WebUI 的真实结构:
|
||||
* .comm-pane ← 一张 `.glass-card`(半透明白,浅色 0.92 / 壁纸下 0.78)
|
||||
* └─ CommTabs ← `shrink-0 … px-3 border-b border-gray-200`,**自己无背景**
|
||||
* 页签条**自己不带底色**,透出的是底下那张玻璃卡。
|
||||
*
|
||||
* 给它铺 `Theme.surface`(系统实心卡片色)= 把那张玻璃卡换成一横条实心白
|
||||
* ⇒ **壁纸再也透不过来** —— 这正是用户这次说的
|
||||
* 「通信页面我觉得没有 webui 那么通透」。
|
||||
*
|
||||
* 判据 `harmony-nav.test.mjs:732`「顶部页签条与底部导航条同一族」
|
||||
* 当场判红(“一块实心白就是割裂”)—— 它盯的形状与我这次犯的**一模一样**。
|
||||
* 教训:判纪引的出处不能只看“像”,要看那个类的**真实层叠位置**。
|
||||
*
|
||||
* ── 顺带保留的那次修复(它是 bug,与本反转无关)──
|
||||
* 页签条下方曾有一条**渐变暗带**。真凶不是页签条自己,而是
|
||||
* `InboxTab` 根容器的投影向上扩散压住了它(几何取证:页签条 y142→271、
|
||||
* InboxTab y271→2202,观测到的渐变区 y240→268 完全吻合)。
|
||||
* 那条修在 `InboxTab` 上(`PaneModifier.plain`),**不随本次反转回退**。
|
||||
*/
|
||||
/*
|
||||
* ★★ 2026-09-24 回到**悬浮玻璃**(09-20 用户点名要的),只补一条底边。
|
||||
*
|
||||
* 我在这一轮中途曾把它改成 `Theme.surface + 无圆角 + 底边`,理由是
|
||||
* 「跟 WebUI 同步」—— 那是**读错了**:用户当时是配着 WebUI 整体布局截图,
|
||||
* 指的是页面结构对齐;而他自己 09-20 已经明确定过页签条要做成悬浮玻璃
|
||||
* (`harmony-nav.test.mjs` 那条判据把原话与理由都记着):
|
||||
* 「顶部的收件箱发件箱和授权为什么不做成悬浮玻璃?」
|
||||
* 两者不冲突:玻璃是**观感**选择,而那条阴影是**bug**。
|
||||
*
|
||||
* ── 阴影的真凶不是页签条自己 ──
|
||||
* 几何取证:页签条(y 142→271)与 `InboxTab` 根容器(y 271→2202)是
|
||||
* `Column` 里的**兄弟**且后者**绘制在后**,它的 `PaneModifier` 投影向上扩散
|
||||
* ~30px ⇒ 压住页签条(观测到的渐变区 y 240→268,完全吻合)。
|
||||
* 只把 `InboxTab` 换成 `PaneModifier.plain`(不投投影)实测落差 21→8,
|
||||
* 而页签条的玻璃观感一字未动。
|
||||
*
|
||||
* 底边仍保留:WebUI 的页签条本来就有 `border-b border-gray-200`,
|
||||
* 而且它让玻璃条与下方可滚内容有一条清晰分界(`Theme.border` = 同一个分隔线语义)。
|
||||
*/
|
||||
.backgroundColor(Color.Transparent)
|
||||
.width('100%')
|
||||
/* 玻璃:与底部导航条同一族(判据 `顶部页签条与底部导航条同一族` 钉的正是这条)。
|
||||
壁纸关着(`bgActive=false`)时退成 `BlurStyle.NONE`,与底部条同一取舍。 */
|
||||
.backgroundBlurStyle(this.bgActive ? Theme.navMaterial : BlurStyle.NONE)
|
||||
/*
|
||||
* ★ 只给**左上角**圆角,右上角不要(用户:「鸿蒙布局还略有不同的,
|
||||
* 你直接改成右边没圆角就行了」)。
|
||||
*
|
||||
* 右上角的弧与窗格右上角的弧**重叠成两道**(页签条一道、窗格一道),
|
||||
* 中间那弯月牙形的空玻璃就是"奇怪"的来源。
|
||||
* 去掉右边那一角,右端就只剩窗格自己那一道边。
|
||||
*/
|
||||
.borderRadius({ topLeft: Theme.glassRadius, topRight: 0 })
|
||||
.border({ width: { bottom: 1 }, color: Theme.border })
|
||||
}
|
||||
@ -2043,6 +2088,18 @@ struct MainPage {
|
||||
* 而账号列表只有主框架持有(与徽标走同一套"父算子读")。
|
||||
*/
|
||||
@State accountLabel: string = '';
|
||||
/**
|
||||
* App 级 SSE 监听(`aboutToAppear` 注册 / `aboutToDisappear` 摧掉)。
|
||||
*
|
||||
* ★★ 2026-09-24:原来这个监听在 `InboxTab` 里 —— 但那个组件是**条件挂载**的,
|
||||
* 切到发件箱/授权就被销毁,监听跟着没 ⇒ 在那两栏时收不到新邮件。
|
||||
* 搬到 `MainPage`(`@Entry`,全程在)。
|
||||
*
|
||||
* ★ 保存同一个函数引用(`onGlobalSseEvent`)才能摧掉:
|
||||
* `SseService.removeListener` 用 `indexOf` 比对引用,
|
||||
* 传一个新写的箭头函数是摧不掉的(本仓 `ComposeIntent.clearListener` 同类坑)。
|
||||
*/
|
||||
private sseService: SseService | null = null;
|
||||
/** 当前是否深色(侧栏主题按钮的图标);由 `applyAppearance` 算出来 */
|
||||
@State isDarkNow: boolean = false;
|
||||
/**
|
||||
@ -2132,15 +2189,84 @@ struct MainPage {
|
||||
private gridCtx: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.gridSettings);
|
||||
|
||||
aboutToAppear(): void {
|
||||
// 主界面也要应用外观:只在设置页生效的话"一进主界面就变回去"(WebUI 侧踩过)
|
||||
this.applyAppearance();
|
||||
this.watchEnvironment();
|
||||
/*
|
||||
* ★★ 2026-09-24:**把 SSE 监听提到 App 级**(用户:「鸿蒙 app 接收邮件的能力也有点不正常」)。
|
||||
*
|
||||
* ── 原来错在哪 ──
|
||||
* 监听原来挂在 `InboxTab.aboutToAppear`(`this.sseService.addListener`),
|
||||
* 而 `CommPage` 里三个 tab 是 `if/else` **条件挂载**的:
|
||||
* 用户切到「发件箱」或「授权」时 `InboxTab` 被销毁 → `aboutToDisappear`
|
||||
* 就把监听摧掉了 ⇒ **在那两栏时收不到任何新邮件通知**,
|
||||
* 切回收件箱也不会补(它只在挂载时拉一次)。
|
||||
*
|
||||
* `MainPage` 才是真正的常驻组件(`@Entry`),所以监听放这里。
|
||||
*
|
||||
* ── 与 WebUI 对齐 ──
|
||||
* WebUI 的 `connectSSE` 注册在 **App 根组件**(全程在),
|
||||
* 不是某个页面(`client/electron/src/api/sse.ts`);事件到达后叫 store 重拉。
|
||||
* 同一个形状。
|
||||
*
|
||||
* ── 不在这里直接 `loadInbox` ──
|
||||
* `loadInbox` 需要 `ctx` + 当前 `accountFilter`,那是**窗格**的状态。
|
||||
* 所以只调 `notifyRemoteChange()` 广播修订号,由挂着的窗格
|
||||
* (`@Watch('onMailRevChanged')`)自己拿手上的参数去拉。
|
||||
*/
|
||||
this.sseService = SseService.getInstance();
|
||||
this.sseService.addListener(this.onGlobalSseEvent);
|
||||
}
|
||||
|
||||
aboutToDisappear(): void {
|
||||
this.unwatchEnvironment();
|
||||
if (this.sseService !== null) {
|
||||
this.sseService.removeListener(this.onGlobalSseEvent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* App 级的 SSE 事件处理(所有账号、所有栏都只有一个入口)。
|
||||
*
|
||||
* ★ 事件名与服务端逐字对应(`client/electron/src/api/sse.ts:7-13` 的 `EVENTS`):
|
||||
* `new_mail` / `permission_decision` / `session_update` /
|
||||
* `session_archived` / `agent_online`。
|
||||
*
|
||||
* 前四个都会影响列表内容 ⇒ 都是"重拉"的信号。
|
||||
* `agent_online` 只影响在线状态展示(侧栏),不动邮件列表。
|
||||
*
|
||||
* ★ 本仓失败的形状之一是“写了一个 handler 但只处理其中一种事件” ——
|
||||
* 原先 `InboxTab` 那个只看了 `new_mail`,而服务端在权限决策后发的是
|
||||
* `session_update`(`permission.go:387`)与 `new_mail`(`permission.go:265`),
|
||||
* 于是"授权栏里处理过的申请,收件箱还是旧的样子"。
|
||||
*/
|
||||
private onGlobalSseEvent = (event: SseEvent): void => {
|
||||
if (event.type === 'new_mail') {
|
||||
/*
|
||||
* 新邮件 toast —— 原来在 `InboxTab` 里(会拿 `AccountManager` 查显示名)。
|
||||
* 搬到 App 级后取不到那个窗格的 `accountList` 了,所以取**活跃账号**的显示名。
|
||||
* 多账号下“哪个账号来的”确实会不准 —— 但事件里只带 `accountId`,
|
||||
* 不查库是不知道名字的;宁可只说“新邮件到达”也不拿错的账号名骗人。
|
||||
*/
|
||||
let sourceName: string = '';
|
||||
const ctx = this.getUIContext().getHostContext();
|
||||
if (ctx !== undefined) {
|
||||
const active: AccountInfo | null = AccountManager.getInstance(ctx).getActiveAccount();
|
||||
if (active !== null && active.id === event.accountId) {
|
||||
sourceName = active.displayName;
|
||||
}
|
||||
}
|
||||
this.getUIContext().getPromptAction().showToast({
|
||||
message: sourceName.length > 0 ? '新邮件:' + sourceName : '新邮件到达'
|
||||
});
|
||||
MailStore.getInstance().notifyRemoteChange();
|
||||
return;
|
||||
}
|
||||
if (event.type === 'permission_decision' || event.type === 'session_update'
|
||||
|| event.type === 'session_archived') {
|
||||
MailStore.getInstance().notifyRemoteChange();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 算一遍「内容末尾要让多少位」——**唯一**的算法,两个触发点共用。
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user