跨端: 鸿蒙修收信/自动已读/发件箱点开 + 页签条通透(用户报的四个问题)
用户报了四个问题,逐个实测复现 → 定位根因 → 修 → 设备复验:
① 「邮件点进去自动已读的能力不正常」
根因:鸿蒙只有「标记已读」按钮(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:
@ -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 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user