跨端: 发件箱接上 MailStore(A 的剩余)+ 修 3 处把原始 ISO 印到界面上的时间

══ ① A 的剩余:发件箱走了 store(顺带撞出 store 里的一个真 bug)

`SentTab.load()` 原来把收件箱那 107 行**抄了一遍**(注释里还写着"同收件箱"
—— 抄的时候就知道是重复)。现在改成 3 行调用 `MailStore.loadSent`。

★ 接上之后**立刻炸出一个真 bug**:`MailStore.loadSent` 里写的是
      snap.groups = [];
  而发件箱的列表**就是按会话分组渲染的**(`ForEach(this.groups, …)`)
  ⇒ 接上 store 之后发件箱会**一片空白**,而"接口有返回、loaded > 0"
  会让症状看起来像"数据没到"。

  为什么会写成空数组:它是照收件箱那半抄的,而收件箱的 `groups` 来自
  `splitByPermission` **筛完之后**的 `inboxMails`(授权邮件要挑出去单独成栏)。
  抄的时候只看到"要赋值",没注意发件箱没有那一步筛选 ⇒ 把 `[]` 抄了过来。
  发件箱也**不能**照搬那个筛选:那是为"授权待办栏"服务的,发件箱不显示那栏。

★ 这段经历本身值得记:**死代码不会自己暴露错误**。
  `loadSent` 写完到今天我接上它之前,那行 `groups = []` 一直没人执行过。
  "写完就搁着"和"接上一个调用点"是两件事 —— 后者才算验过。

══ ② 3 处把原始 ISO 直接印到界面上(看到屏幕才发现的)

发件箱那张卡的时间格显示的是
      2026-09-19T02:55:33.10099Z
(23 个字符,把「致 homeagent」那行挤到换行)。

WebUI **三种卡片全部格式化**、一个没漏:`MailList.tsx:151/262`、
`PermissionList.tsx:143/251`,全是 `toLocaleString('zh-CN', {month,day,hour,minute})`
⇒ `MM/DD HH:mm`。鸿蒙的 `compactMailTime` 就是那个实现,收件箱一直在用 ——
所以这不是"要不要格式化"的分歧,是**三处漏调**(收件箱 / 发件箱 / 授权栏)。

══ ③ 补判据(形状而不是实例)

新判据:`Text(<x>.created_at)` 这个**形状**不许出现(`Text` 只负责画,
不做格式化)。为什么枚举形状而不是列举调用点:漏的三处分布在**三个不同 struct**,
按名字枚举一定会再漏第四个。
自检 + 变异验证都做了(把 SentRow 那处改回裸字段 → 判据转红;还原 → 绿)。

设备验证:发件箱正常渲染(会话组 + 条目,非空白),
时间显示 `09/19 10:55` 而非 ISO。
This commit is contained in:
2026-09-20 18:43:35 +08:00
parent 99a7bbccf7
commit f1db99ef41
3 changed files with 99 additions and 37 deletions

View File

@ -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 = '';

View File

@ -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<void> {
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 })