跨端: 鸿蒙修收信/自动已读/发件箱点开 + 页签条通透(用户报的四个问题)

用户报了四个问题,逐个实测复现 → 定位根因 → 修 → 设备复验:

① 「邮件点进去自动已读的能力不正常」
   根因:鸿蒙只有「标记已读」按钮(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:
2026-09-24 16:34:44 +08:00
parent 548314013a
commit 187609480f
6 changed files with 873 additions and 147 deletions

View File

@ -6,6 +6,8 @@
import { http } from '@kit.NetworkKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
/* UTF-8 解码用(见 `arrayBufferToString` 的注释:逐字节 fromCharCode 会把中文解成乱码) */
import { util } from '@kit.ArkTS';
import { AccountManager, AccountInfo } from './AccountManager';
import { normalizeApiBase } from '../model/ApiBase';
@ -39,6 +41,13 @@ export class SseConnection {
reconnectTimer: number = 0;
buffer: string = '';
connected: boolean = false;
/*
* ★★ 2026-09-24:**每连接一个** 解码器(不要共享)。
* `decodeToString(..., {stream:true})` 会把「被 TCP 分片切断的半个汉字」
* 存在**该解码器实例内部**,等下一片到了再拼。多个连接共用同一个实例
* 就会把一个账号的半个字与另一个账号的半截拼在一起 ⇒ 乱码。
*/
decoder: util.TextDecoder | null = null;
}
export class SseService {
@ -191,6 +200,8 @@ export class SseService {
const httpRequest = http.createHttp();
conn.httpRequest = httpRequest;
/* 重连要换新的:旧解码器里可能还卡着上次断线时那半个字 */
conn.decoder = util.TextDecoder.create('utf-8', { ignoreBOM: true });
const url: string = conn.server + '/events/stream';
const header: Record<string, string> = {};
@ -201,7 +212,7 @@ export class SseService {
hilog.info(DOMAIN, TAG, 'connecting account %{public}s to %{public}s', conn.accountId, url);
httpRequest.on('dataReceive', (data: ArrayBuffer) => {
const text: string = this.arrayBufferToString(data);
const text: string = this.arrayBufferToString(conn, data);
conn.buffer += text;
this.processBuffer(conn);
});
@ -298,12 +309,29 @@ export class SseService {
conn.reconnectTimer = timer ?? 0;
}
private arrayBufferToString(buffer: ArrayBuffer): string {
const uint8Array: Uint8Array = new Uint8Array(buffer);
let result: string = '';
for (let i = 0; i < uint8Array.length; i++) {
result += String.fromCharCode(uint8Array[i]);
}
return result;
private arrayBufferToString(conn: SseConnection, buffer: ArrayBuffer): string {
/*
* ★★ 2026-09-24 修:**UTF-8 解码**(用户:「鸿蒙 app 接收邮件的能力也有点不正常」)。
*
* 原来这里是逐字节 `String.fromCharCode(uint8Array[i])` —— 那是
* **Latin-1 语义**:字节 ≥ 0x80 各自变成一个独立字符。
* SSE 流里一个汉字是 3 个 UTF-8 字节(如 「新」 = E6 96 B0)
* ⇒ 解出来是 `æ\x96°` 这种乱码。
*
* WebUI 没这个问题:它用浏览器原生 `EventSource`,规范本身按 UTF-8
* 解码(`client/electron/src/api/sse.ts:59`)。
* ⇒ 两端差在**解码这一步**,不是差在有没有 SSE。
*
* ★ 为什么不用 `util.TextDecoder`:HarmonyOS 有 `@kit.ArkTS` 的
* `util.TextDecoder`(标准 API),比自己手写 UTF-8 状态机可靠得多
* —— 手写容易在「3 字节序列被 TCP 分片切成两段」时错(SSE 常见)。
* `stream: true` 正是为这种分段场景准备的:不完整的尾字节会
* 留在内部缓冲里,等下一片到了再拼。
*/
const decoder: util.TextDecoder = conn.decoder !== null
? conn.decoder
: util.TextDecoder.create('utf-8', { ignoreBOM: true });
conn.decoder = decoder;
return decoder.decodeToString(new Uint8Array(buffer), { stream: true });
}
}