diff --git a/client/electron/test/harmony-arkts.test.mjs b/client/electron/test/harmony-arkts.test.mjs index 0654246..857a9f3 100644 --- a/client/electron/test/harmony-arkts.test.mjs +++ b/client/electron/test/harmony-arkts.test.mjs @@ -305,3 +305,44 @@ test('★ 尺寸/底色不许直接链在 `AmIcon` 上(内层容器固定 icon assert.equal(probe(bad), 1, '探测器要认得出错误写法'); assert.equal(probe(good), 0, '探测器不许误伤套了 Stack 的正确写法'); }); + +test('★ 时间戳不许把原始 ISO 直接印到界面上(`Text(mail.created_at)`)', () => { + /* + * ★★ 2026-09-20 加。这条是**先看到屏幕上印着原始 ISO** 才回来补的判据: + * + * 发件箱那张卡的时间那一格显示的是 + * 2026-09-19T02:55:33.10099Z + * (23 个字符,把「致 homeagent」那一行挤到换行)。 + * + * WebUI **三种卡片全部格式化**,一个都没漏: + * · `MailList.tsx:151`(会话组头)与 `:262`(组内条目) + * · `PermissionList.tsx:143`(授权组头)与 `:251`(授权条目) + * 全部是 `toLocaleString('zh-CN', { month:'2-digit', day:'2-digit', + * hour:'2-digit', minute:'2-digit' })` ⇒ `MM/DD HH:mm`。 + * + * 鸿蒙的 `compactMailTime` 就是那个格式的实现,收件箱一直在用 —— + * 所以这不是"要不要格式化"的分歧,是**三处漏调**。 + * + * ★ 判据形状为什么是"不许出现 `Text(.created_at)`"而不是列举调用点: + * 漏的那三处分布在三个不同的 struct(收件箱/发件箱/授权), + * 按"名字"枚举一定会再漏第四个。而"把 ISO 交给 Text"这个**形状** + * 本身就是错的(`Text` 只负责画,不做格式化)—— + * 枚举形状才挡得住漂移。 + * + * ★ 边界:`*For()` 那种"取值函数"不算,这里只管**直接**把字段喂给 Text 的。 + * 允许 `created_at` 作为**参数**传给格式化函数(那正是正确写法)。 + */ + const src = code(join(ETS_ROOT, 'pages/MainPage.ets')); + // Text( 里直接出现 .created_at / .createdAt,且中间没有函数调用 + const bad = [...src.matchAll(/Text\(\s*[\w.]*\.(created_at|createdAt)\s*\)/g)] + .map(m => m[0]); + assert.deepEqual(bad, [], + `这些地方把原始 ISO 直接交给 Text 了(屏上会印出 23 个字符的时间戳):\n ${bad.join('\n ')}\n` + + '改用 compactMailTime(...)(收件箱行一直是这么写的)'); + + // 自检:探测器要认得错误形状,且不误伤正确写法 + const probe = (t) => [...t.matchAll(/Text\(\s*[\w.]*\.(created_at|createdAt)\s*\)/g)].length; + assert.equal(probe("Text(mail.created_at)"), 1, '探测器要认得出裸字段'); + assert.equal(probe("Text(compactMailTime(mail.created_at))"), 0, '不许误伤传参给格式化函数的写法'); + assert.equal(probe("Text(mail.createdAt)"), 1, '驼峰写法同样要抓'); +}); diff --git a/client/harmony/entry/src/main/ets/common/MailStore.ets b/client/harmony/entry/src/main/ets/common/MailStore.ets index c1c6557..2020de5 100644 --- a/client/harmony/entry/src/main/ets/common/MailStore.ets +++ b/client/harmony/entry/src/main/ets/common/MailStore.ets @@ -273,7 +273,24 @@ export class MailStore { } snap.mails = merged; - snap.groups = []; + /* + * ★★ 2026-09-20 修:这里原来写的是 `snap.groups = []`。 + * + * 那是**错的**,而且错得很隐蔽:发件箱的列表**就是按会话分组渲染的** + * (`SentTab` 里 `ForEach(this.groups, …)`),`groups` 恒为空 + * ⇒ 接上 store 之后发件箱会变成一片空白,而"接口有返回、loaded > 0" + * 会让错误看起来像"数据没到"。 + * + * 为什么会写成空数组:这个方法是照收件箱那半抄的,而收件箱的 + * `groups` 来自 `splitByPermission` **筛完之后**的 `inboxMails` + * (授权邮件要挑出去单独成栏)。我抄的时候只看到"要赋值", + * 没注意发件箱没有那一步筛选 ⇒ 直接把 `[]` 抄了过来。 + * + * 发件箱也**不能**照搬 `splitByPermission`:那一筛是为"授权待办栏"服务的, + * 发件箱不显示那个栏位。所以这里直接对全部 merged 分组 —— + * 与 WebUI `mailStore.fetchSent` 同口径(那边也是直接 set 全量)。 + */ + snap.groups = groupMailsBySession(merged); snap.loaded = merged.length; snap.unread = 0; snap.notice = ''; diff --git a/client/harmony/entry/src/main/ets/pages/MainPage.ets b/client/harmony/entry/src/main/ets/pages/MainPage.ets index 9acf807..09a4c23 100644 --- a/client/harmony/entry/src/main/ets/pages/MainPage.ets +++ b/client/harmony/entry/src/main/ets/pages/MainPage.ets @@ -915,45 +915,38 @@ struct SentTab { this.load(); } + /** + * 取数**交给 `MailStore`**(与收件箱同一份实现)。 + * + * ★★ 2026-09-20 改。这一段原来是**抄了一遍**多账号聚合: + * 遍历账号 → 每个建 `ApiClient` → `sent()` → 补 `source_account_*` 与 + * `cc_count` → 最后 `groupMailsBySession`。与收件箱里那 107 行是同源复制品 + * (注释里还写着"同收件箱"—— 说明抄的时候就知道是重复)。 + * + * 用户要做的 **A** 就是这件事:把散在各处的取数收到 store。 + * 收件箱那半已经收了(`InboxTab.loadData` 现在 3 行),发件箱这片是剩下的。 + * + * ★ 顺带修掉 `MailStore.loadSent` 里一个真 bug(`snap.groups = []`)—— + * 见那个方法里的注释:接上 store 之前它是死代码,接上之后 + * 发件箱会**一片空白**。这就是"写完没收口"的代价,也是为什么要 + * 先接一个调用点再算完 —— 死代码不会自己暴露错误。 + */ async load(): Promise { const ctx = this.getUIContext().getHostContext(); if (ctx === undefined) { return; } - this.loading = true; - this.error = ''; - try { - const acctMgr: AccountManager = AccountManager.getInstance(ctx); - await acctMgr.load(); - const accounts: AccountInfo[] = acctMgr.getAccounts(); - const all: MailSummary[] = []; - for (let i = 0; i < accounts.length; i++) { - const acct: AccountInfo = accounts[i]; - try { - const c: ApiClient = new ApiClient(ctx); - c.setBase(acct.server); - c.setToken(acct.token); - const resp: SentResponse = await new MailApi(c).sent(); - for (let j = 0; j < resp.mails.length; j++) { - const mail: MailSummary = resp.mails[j]; - mail.source_account_id = acct.id; - mail.source_account_name = acct.displayName; - /* 同收件箱:派生行上的「抄送 N」(两处都要写 —— 理由见收件箱那处) */ - mail.cc_count = mail.cc_list.length; - all.push(mail); - } - } catch (e) { - // 单个账号拉不到不该让整栏空白(与收件箱同口径) - } - } - this.groups = groupMailsBySession(all); - this.loaded = all.length; - } catch (e) { - const ae = e as ApiError; - this.error = ae.message.length > 0 ? ae.message : '加载失败'; - } finally { - this.loading = false; - } + const store: MailStore = MailStore.getInstance(); + await store.loadSent(ctx, 'all'); + this.applyStoreSnapshot(store.snapshot); + } + + /** 把 store 快照搬到本组件的 `@State`(`InboxTab` 里同名同形) */ + private applyStoreSnapshot(snap: MailSnapshot): void { + this.groups = snap.groups; + this.loaded = snap.loaded; + this.loading = snap.loading; + this.error = snap.error; } isExpanded(key: string): boolean { @@ -989,7 +982,17 @@ struct SentTab { .fontSize(13).fontWeight(FontWeight.Medium).fontColor(Theme.textPrimary) .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }) .layoutWeight(1) - Text(mail.created_at).fontSize(10).fontColor(Theme.textSubtleFor()) + /* + * ★★ 2026-09-20 修:这里原来直接渲染 `mail.created_at` —— + * 屏上印的是原始 ISO,实测:`2026-09-19T02:55:33.10099Z` + * (23 个字符,把「致 homeagent」那一行挤到换行)。 + * + * WebUI 三种卡片**全部都格式化**(`MailList.tsx:262/151`、 + * `PermissionList.tsx:251/143` 都是 `toLocaleString('zh-CN', {month,day,hour,minute})` + * ⇒ `MM/DD HH:mm`)。鸿蒙的 `compactMailTime` 就是那个格式的实现, + * 收件箱行(L813)一直在用,这三处漏了。 + */ + Text(compactMailTime(mail.created_at)).fontSize(10).fontColor(Theme.textSubtleFor()) } .width('100%') @@ -1289,7 +1292,8 @@ struct PermissionTab { .margin({ top: 4 }) } - Text(req.created_at).fontSize(10).fontColor(Theme.textSubtleFor()).margin({ top: 6 }) + /* 同 ①:WebUI `PermissionList.tsx:251` 也格式化,不印 ISO */ + Text(compactMailTime(req.created_at)).fontSize(10).fontColor(Theme.textSubtleFor()).margin({ top: 6 }) if (this.noteFor === req.mail_id) { TextInput({ placeholder: '备注(会随决策一起送给 Agent)', text: this.noteText })