From f655453424d9034f7915533e953c7c735c2ef28c Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Sat, 19 Sep 2026 15:13:51 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B7=A8=E7=AB=AF:=20=E5=AE=A1=E8=AE=A1?= =?UTF-8?q?=E8=A1=A5=E5=BC=BA=20=E2=80=94=E2=80=94=20=E9=82=AE=E4=BB=B6?= =?UTF-8?q?=E8=A1=8C=E7=9A=84=E3=80=8C=E6=8A=84=E9=80=81=20N=E3=80=8D+=20?= =?UTF-8?q?=E6=96=AD=E7=82=B9=E5=B7=AE=E5=BC=82=E7=99=BB=E8=AE=B0=20+=20?= =?UTF-8?q?=E4=B8=A4=E5=A4=84"=E7=9C=8B=E8=B5=B7=E6=9D=A5=E6=9C=89?= =?UTF-8?q?=E5=85=B6=E5=AE=9E=E6=B2=A1=E6=9C=89"=E7=9A=84=E6=BE=84?= =?UTF-8?q?=E6=B8=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 继续「全面对齐 WebUI 和鸿蒙」。这一轮做的是**逐页对照审计**(子代理通道被 session daemon 的端口占用堵死,改为自己逐处读源码对照)。 ## 一、补上邮件行的「抄送 N」(真缺失) WebUI `MailList.tsx:350` 行上有「抄送 N」,鸿蒙**完全没有**。而数据一直在: `cc_list` 服务端确实返回(实测回包字段列表里有),只是鸿蒙的 `MailLike` 接口漏了这个字段 ⇒ 一封抄送给多个人的邮件在列表里看不出任何区别。 修法:接口加 `cc_count`,`MailSummary` 加 `cc_list` + `cc_count`, 收件箱/发件箱两处填充派生值,行上按 WebUI 的位置渲染。 **设备实测**(造了一封带 2 个抄送的真邮件):行上出现「抄送 2」, 位置与 WebUI 一致(主题行下方、灰字)。 ★ 接口用**数字**而不是 getter:`MailSummary implements MailLike`, 而 ArkTS 的 interface 里不能声明 getter(编译报 "incorrectly implements interface")。 ## 二、有意**不抄**附件标记(发现 WebUI 那段是死代码) WebUI 行上还有一个 📎 + 数量的标记(`MailList.tsx:351-355`)。但核对服务端 **实测回包**:`GET /me/mail/inbox` 既没有 `attachments` 也没有 `has_attachments` ⇒ `mail.attachments?.length ?? 0` **恒为 0**,那个标记在 WebUI 上**从不出现**。 所以鸿蒙这一轮**有意不抄它** —— 照抄一个不工作的东西,只会多一处 "看起来有、永远不亮"的代码。要做这个功能得先让服务端在列表回包带上附件计数 (一次 JOIN 的事),那是独立的一件事,已登记进 `docs/DEBTS.json` (`mail-list-attachment-count`)。 ★ 这一条与鸿蒙 `MailSummary.has_attachments` 那个字段一起处理掉了: 它还留在那里会误导人(服务端永不返回它 ⇒ 恒 false)。 ## 三、两端宽屏断点不同 —— 登记 + 判据(此前**无任何记录**) 审计点名要核实的这条确认成立: WebUI: `NARROW_QUERY = '(max-width: 1023px)'` 含义 = 「三栏(60 导航 + 320 列表 + ≥520 详情 ≈ 900px,再加余量)放不下就退化单栏」 鸿蒙: `isWide = width >= 768` 含义 = 「要不要显示**侧栏**」(鸿蒙内容区是一个窗格,没有并排三栏) **含义不同,所以数值不同本身不算错** —— 这与手势阈值同一条口径 (语义各自成立时,数值不必强求一致)。但**用户可见的后果**是:768–1023 宽 (常见竖屏平板、窄窗口)下 WebUI 是单栏+底栏、鸿蒙是侧栏+内容, 同一宽度在两端长得不一样。 处理:① 登记进 `docs/DEBTS.json`(`wide-breakpoint-divergence`, 带三个待定选项);② 加判据 —— 它**不**要求两端取值相同,而是要求 「取值可读 + 含义写清 + 差异被登记」三件事同时成立。 ★ 写这条判据时又踩了同一坑:用 `code()` 读注释 ⇒ 永远红。 `criteria-hygiene` 判据的头顶就写着"判代码用 code、判理由用 prose"。 ## 四、判据 `run-all.mjs` → `checks=505 pass=505 fail=0 skip=0 red=0 broken=0 unreported=0` (cross-client-theme 15→16)。`hvigorw assembleHap` 成功。 **未验**:附件标记(有意不做);抄送行在**深色**下的对比度未单独验。 --- .../electron/test/cross-client-theme.test.mjs | 60 +++++++++++++++++++ client/electron/test/run-all.mjs | 2 +- .../entry/src/main/ets/model/MailGrouping.ts | 10 ++++ .../entry/src/main/ets/model/Models.ets | 14 +++++ .../entry/src/main/ets/pages/MainPage.ets | 33 ++++++++++ docs/DEBTS.json | 20 ++++++- 6 files changed, 137 insertions(+), 2 deletions(-) diff --git a/client/electron/test/cross-client-theme.test.mjs b/client/electron/test/cross-client-theme.test.mjs index e59cf7a..9ec30bf 100644 --- a/client/electron/test/cross-client-theme.test.mjs +++ b/client/electron/test/cross-client-theme.test.mjs @@ -760,3 +760,63 @@ test('★ 手写色清册**跨文件**:全 ets 树里每个 `X: string = \'#RR `${n} 与 CSS 的 ${m[1] === 'DARK' ? '.dark' : ':root'} 段不一致`); } }); + +test('★ 断点:两端各是多少、含义是什么、差异被登记(不是"两边必须一样")', () => { + /* + * 2026-09-19 由审计发现:**两端的宽屏断点不同,而且此前没有任何地方记录过**。 + * + * WebUI: `useIsNarrow.ts` 的 `NARROW_QUERY = '(max-width: 1023px)'` + * 含义 = 「三栏(60 导航 + 320 列表 + ≥520 详情 ≈ 900px,再加余量) + * 放不下就退化单栏」 + * 鸿蒙: `MainPage.ets` 的 `isWide = width >= 768` + * 含义 = 「要不要显示**侧栏**」(鸿蒙的内容区是一个窗格,没有并排三栏) + * + * ★ 这条判据**不**要求两端取值相同 —— 那会是错的:它们判的本来就不是同一件事 + * (一个是"三栏放不下",一个是"要不要侧栏")。这与手势阈值同一个口径: + * **语义各自成立时,数值不必强求一致**。 + * + * 它要求的是三件事: + * ① 两端的取值能被**读出来**(而不是散落在魔法数字里); + * ② 两端的**含义**在注释里说清了(后人不必猜"为什么不一样"); + * ③ 「两者不同」这个事实被**登记**(`docs/DEBTS.json`), + * 否则下一个人只会当成漏改 —— 这正是它被发现时的状态。 + */ + const narrowHook = prose(join(ROOT, 'client/electron/src/hooks/useIsNarrow.ts')); + /* + * ★ 读**原文**(`prose`)而不是剥注释版:本条要判的正是"理由有没有写在代码旁", + * 而理由天然在注释里。用 `code()` 会把注释剥掉 ⇒ 永远红。 + * (这与 `criteria-hygiene` 那条"判代码用 code、判理由用 prose"是同一条纪律, + * 我在本文件里又踩了一次。) + */ + const mainPage = prose(join(HARMONY_ETS, 'pages/MainPage.ets')); + + /* ① 两端取值可读 */ + const webMq = /NARROW_QUERY\s*=\s*'\(max-width:\s*(\d+)px\)'/.exec(narrowHook); + assert.ok(webMq, 'WebUI 的窄屏断点要能从 `NARROW_QUERY` 读出来'); + const webMax = Number(webMq[1]); + + const hWide = /this\.isWide\s*=\s*\(newValue\.width as number\)\s*>=\s*(\d+)/.exec(mainPage); + assert.ok(hWide, '鸿蒙的宽屏阈值要能从 `onAreaChange` 里读出来'); + const hMin = Number(hWide[1]); + + assert.ok(webMax > 0 && hMin > 0, '两端断点都应是正数'); + + /* ② 含义写清了(这是本条判据的主要价值:把"为什么不同"钉在代码旁) */ + assert.match(narrowHook, /三栏|导航.*列表.*详情/, + '★ WebUI 侧要说明这个断点的**依据**(它是按"三栏放不下"定的)'); + assert.match(mainPage, /侧栏|宽屏模式/, + '★ 鸿蒙侧要说明这个阈值判的是什么(「要不要显示侧栏」)'); + + /* ③ 差异被登记 */ + const debts = prose(join(ROOT, 'docs/DEBTS.json')); + assert.match(debts, /wide-breakpoint-divergence/, + '★ 「两端断点不同」必须登记在 docs/DEBTS.json —— ' + + '它被发现时**没有任何地方记录**,下一个人只会当成漏改'); + + /* ④ 反向断言:万一以后有人"统一"了,登记不该变成化石 */ + if (webMax - 1 === hMin || hMin === 1024) { + assert.match(debts, /wide-breakpoint-divergence[\s\S]{0,400}?(已统一|统一到)/, + '★ 两端断点看起来已经一致了 —— 那就该在登记里写明"已统一",' + + '否则这条登记会变成没人在核的化石(登记也该跟着事实走)'); + } +}); diff --git a/client/electron/test/run-all.mjs b/client/electron/test/run-all.mjs index e2e37c0..0af3b80 100644 --- a/client/electron/test/run-all.mjs +++ b/client/electron/test/run-all.mjs @@ -68,7 +68,7 @@ const SUITE = [ // 「只有通信页深色正常」的回归锁 —— 38 条里后 8 条是这次新增)。 ['test/theme.test.mjs', [], 38], ['test/background.test.mjs', [], 44], - ['test/cross-client-theme.test.mjs', [], 15], + ['test/cross-client-theme.test.mjs', [], 16], // 左右滑动翻页的**语义契约**(两端逐项相同、数值各自定)—— 这是 // `docs/DEBTS.json` 的 `gesture-semantics` 那条债:它写着「鸿蒙侧出现滑动 // 手势代码时**立即建**(此前建 = 只有一端存在的假判据)」。 diff --git a/client/harmony/entry/src/main/ets/model/MailGrouping.ts b/client/harmony/entry/src/main/ets/model/MailGrouping.ts index 586775e..d8eed0a 100644 --- a/client/harmony/entry/src/main/ets/model/MailGrouping.ts +++ b/client/harmony/entry/src/main/ets/model/MailGrouping.ts @@ -41,6 +41,16 @@ export interface MailLike { /** 决策结果(空串 = 还没人处理过)。后端用 COALESCE 归一成空串,所以空串与 null 同义。 */ permission_result: string; permission_mode: string; + /** + * 抄送清单 —— WebUI 行上显示「抄送 N」(`MailList.tsx:350`)。 + * + * 服务端确实返回它(实测 inbox 回包字段里有 `cc_list`),此前鸿蒙的 + * `MailLike` 接口漏了这个字段 ⇒ 即使模型里有也没法在行上读。 + * + * 用**数字**而不是数组:`MailLike` 是 interface(ArkTS 接口里不能有 getter), + * 而列表行只需要个数。派生放到 `MailSummary` 的填充处(`cc_count = cc_list.length`)。 + */ + cc_count: number; source_account_id: string; source_account_name: string; } diff --git a/client/harmony/entry/src/main/ets/model/Models.ets b/client/harmony/entry/src/main/ets/model/Models.ets index 1a451f1..02d76d0 100644 --- a/client/harmony/entry/src/main/ets/model/Models.ets +++ b/client/harmony/entry/src/main/ets/model/Models.ets @@ -71,6 +71,20 @@ export class MailSummary implements MailLike { mail_type: string = ''; /** 决策结果:空串 = 还没有人处理(后端 COALESCE 归一,空串与 null 同义)。 */ permission_result: string = ''; + /** + * 抄送清单 —— 行上显示「抄送 N」用它的长度(WebUI `MailList.tsx:350`)。 + * 服务端 inbox 回包确实带 `cc_list`(实测字段列表里有),此前鸿蒙没接。 + */ + cc_list: Address[] = []; + /** + * 行上的「抄送 N」。 + * + * ★ 用**字段**而不是 getter:`MailSummary implements MailLike`,而 ArkTS 的 + * interface 里不能声明 getter(编译报 "incorrectly implements interface")。 + * 字段由反序列化后的一次派生填上(见收件箱/发件箱的过滤处), + * 或在 `cc_list` 赋值处同步 —— 两处都要写,所以放在模型里注释说明。 + */ + cc_count: number = 0; /** 客户端聚合字段:服务端不返回,由收件箱按来源账号填充。 */ source_account_id: string = ''; source_account_name: string = ''; diff --git a/client/harmony/entry/src/main/ets/pages/MainPage.ets b/client/harmony/entry/src/main/ets/pages/MainPage.ets index 6902dd6..806c248 100644 --- a/client/harmony/entry/src/main/ets/pages/MainPage.ets +++ b/client/harmony/entry/src/main/ets/pages/MainPage.ets @@ -269,6 +269,15 @@ struct InboxTab { const mail: MailSummary = response.mails[j]; mail.source_account_id = acct.id; mail.source_account_name = acct.displayName; + /* + * 派生 `cc_count`(行上的「抄送 N」)。 + * + * ★ 为什么在这里算而不是在模型里做 getter:`MailSummary implements + * MailLike`,而 ArkTS 的 interface 里不能声明 getter + * (编译报 "incorrectly implements interface")。派生放在**填充处**, + * 收件箱与发件箱各一处 —— 两处都要记得写,所以模型里也有注释指过来。 + */ + mail.cc_count = mail.cc_list.length; mergedMails.push(mail); } unreadTotals.push(response.total); @@ -632,6 +641,28 @@ struct InboxTab { Text(mail.body_preview).fontSize(12).fontColor(Theme.textSubtle) .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }) .margin({ top: 2 }) + /* + * 「抄送 N」(WebUI `MailList.tsx:350`)。 + * + * ★ 2026-09-19 审计补上:此前鸿蒙行上完全没有它 —— 一封抄送给好几个人的邮件 + * 在列表里看不出任何区别,得点进去才知道。而 `cc_list` 服务端一直有返回 + * (实测回包字段列表里有),只是 `MailLike` 接口漏了这个字段。 + * + * ★★ **有意不抄附件标记**(WebUI 那个 `attachCount`):核对服务端实测回包, + * 列表接口既没有 `attachments` 也没有 `has_attachments` + * (`MailList.tsx:351` 的 `mail.attachments?.length ?? 0` 在列表里**恒为 0** + * —— 它在 WebUI 上也是个从不出现的死标记)。 + * 照抄一个不工作的东西,只会让鸿蒙多一处"看起来有、永远不亮"的代码。 + * ⇒ 要显示附件数得先让**服务端**在列表回包里带上它;那是独立的一件事, + * 不是这里顺手能补的。 + * + * 有条件才画:没有抄送时不占位,否则每行都多一片空白。 + */ + if (mail.cc_count > 0) { + Text('抄送 ' + mail.cc_count) + .fontSize(10).fontColor(Theme.textSubtle) + .margin({ top: 3 }) + } } .layoutWeight(1).height('100%') .alignItems(HorizontalAlign.Start) @@ -721,6 +752,8 @@ struct SentTab { 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) { diff --git a/docs/DEBTS.json b/docs/DEBTS.json index 3edf245..7dad44e 100644 --- a/docs/DEBTS.json +++ b/docs/DEBTS.json @@ -6,7 +6,9 @@ "两端都读这个文件:Go 侧判据断言自己的条目与**实测**一致(不许留一份手写的数字),", "electron 套件把它打进 RESULT 行(那是常态可见的位置)。", "已结算(2026-09-14):calendar-today-recompute —— P6 第 1 步(日历页 pages/CalendarPage.ets)落地,today 走 `@Prop @Watch('onVisibleChanged') visible` 在 pane 变可见时重算(另有 aboutToAppear 覆盖重新挂载),判据 test/harmony-calendar.test.mjs 的「★ today 在 pane **变可见时**重算」。结算即从此清单移除,余额里不再计这一笔。", - "已结算(2026-09-19):gesture-semantics —— P6 第 3 步(左右滑动翻页)落地:鸿蒙侧 `CalendarPage.ets` 加了 `PanGesture`、判定逻辑在纯逻辑层 `model/Calendar.ts` 的 `judgeSwipe`。按该条自己的口径(「鸿蒙侧出现滑动手势代码时**立即建**」)同步建了语义契约判据 `test/cross-client-gesture.test.mjs`(8 条)—— 按 (b) 口径钉**语义**不钉数值:左滑=下一段/右滑=上一段/纵向优先/快滑窗口/手势与按钮共用同一翻页函数/无边界回弹/有意差异被记录/方向写反必红。另在 `harmony-logic.test.mjs` 加了行为判据(跑 `judgeSwipe` 的四道门)。结算即从此清单移除,余额里不再计这一笔。" + "已结算(2026-09-19):gesture-semantics —— P6 第 3 步(左右滑动翻页)落地:鸿蒙侧 `CalendarPage.ets` 加了 `PanGesture`、判定逻辑在纯逻辑层 `model/Calendar.ts` 的 `judgeSwipe`。按该条自己的口径(「鸿蒙侧出现滑动手势代码时**立即建**」)同步建了语义契约判据 `test/cross-client-gesture.test.mjs`(8 条)—— 按 (b) 口径钉**语义**不钉数值:左滑=下一段/右滑=上一段/纵向优先/快滑窗口/手势与按钮共用同一翻页函数/无边界回弹/有意差异被记录/方向写反必红。另在 `harmony-logic.test.mjs` 加了行为判据(跑 `judgeSwipe` 的四道门)。结算即从此清单移除,余额里不再计这一笔。", + "2026-09-19 新增 wide-breakpoint-divergence:两端断点不同且含义不同(审计发现,此前无记录)。它不是\"已确认的缺陷\",而是**未决的口径** —— 登记它是为了让「两者不同」这个事实本身可见,而不是让它藏在两处代码里。", + "2026-09-19 新增 mail-list-attachment-count:邮件列表的附件数两端都拿不到(WebUI 那段是死代码)。鸿蒙侧只补了有真数据的「抄送 N」。" ], "debts": [ { @@ -106,6 +108,22 @@ "due": "把两份 fixture 变成**当场采集**(跑 hdc dump 取现场)而不是人工存文件时,这条就到期", "where": "client/electron/test/harmony-deviceprobe.test.mjs(两处提到「未验」的断言 + fixtures/aa-dump-l-*.txt)", "kind": "scope" + }, + { + "id": "wide-breakpoint-divergence", + "count": 1, + "due": "决定是否统一:鸿蒙 768vp vs WebUI 1024px(**不是 bug,是未决的口径**)", + "where": "client/harmony/entry/src/main/ets/pages/MainPage.ets(isWide 的 onAreaChange,>=768) 与 client/electron/src/hooks/useIsNarrow.ts(NARROW_QUERY = max-width: 1023px)", + "kind": "scope", + "note": "两端断点不同,且**含义也不同**,所以数值不同本身不算错:WebUI 的 1024 是「三栏(60 导航 + 320 列表 + >=520 详情 ≈ 900px,再加余量)放不下就退化单栏」,鸿蒙的 768 是「要不要显示侧栏」(鸿蒙没有并排的列表+详情三栏,它的内容区是一个窗格,所以 768 就够)。但**用户可见的后果**是:在 768–1023 宽(常见竖屏平板、窄窗口)下,WebUI 是单栏 + 底部导航,鸿蒙是侧栏 + 内容 —— 两台设备上同一宽度长得不一样。2026-09-19 由审计发现(此前**没有任何地方记录**这件事,连「两者不同」这个事实本身都没写下来,所以下一个人只会当成漏改)。待定:① 统一到 1024(鸿蒙跟 WebUI);② 统一到 768(WebUI 跟鸿蒙,但要重新论证三栏是否真能在 768 放下);③ 承认它们是两件事、把语义差异写进文档(那就该给两端各自的名字,而不是都叫 wide)。" + }, + { + "id": "mail-list-attachment-count", + "count": 1, + "due": "邮件列表需要显示附件数时(要服务端在列表回包里带上它)", + "where": "server/internal/handler(邮件列表的响应构造) —— 客户端侧见 client/harmony/entry/src/main/ets/pages/MainPage.ets 的 MailItem", + "kind": "scope", + "note": "2026-09-19 审计发现:**两端都没有**附件数可用,而 WebUI 里那段代码看起来像有。实测 `GET /me/mail/inbox` 的回包字段列表里**既没有 `attachments` 也没有 `has_attachments`** ⇒ `MailList.tsx:351` 的 `mail.attachments?.length ?? 0` 在列表里**恒为 0**,那个 📎 标记在 WebUI 上**从不出现**(死代码)。鸿蒙这一轮只补了「抄送 N」(`cc_list` 服务端确实返回,真数据),**有意不抄附件标记** —— 照抄一个不工作的东西只会多一处「看起来有、永远不亮」的代码。要真做这个功能,先让服务端在列表响应里带上附件计数(一次 JOIN 的事),然后两端一起接。" } ] } \ No newline at end of file