跨端: 发件箱接上 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:
@ -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(<x>.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, '驼峰写法同样要抓');
|
||||
});
|
||||
|
||||
@ -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 = '';
|
||||
|
||||
@ -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 })
|
||||
|
||||
Reference in New Issue
Block a user