From d429e4af24ac641276fa879ef1cc587385a53f7a Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Mon, 21 Sep 2026 22:28:02 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B7=A8=E7=AB=AF:=20=E4=BF=AE=E5=8F=91?= =?UTF-8?q?=E4=BB=B6=E7=AE=B1=E3=80=8C=E4=B8=A5=E9=87=8D=E9=97=AE=E9=A2=98?= =?UTF-8?q?=E3=80=8D+=20=E6=8A=84=E9=80=81=E5=AD=97=E6=AE=B5/=E8=A1=A5?= =?UTF-8?q?=E5=85=A8=EF=BC=88=E7=94=A8=E6=88=B7=E7=82=B9=E5=90=8D=E7=9A=84?= =?UTF-8?q?=E4=B8=A4=E5=A4=84=E7=B3=BB=E7=BB=9F=E6=80=A7=E9=81=97=E6=BC=8F?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户两句话把问题指到了根上: ① 「发件箱存在严重问题」 ② 「webui 存在好几个自动填充位置,比如抄送,转发等。你为什么要我一个 一个点出来呢?skill 也给你了 codegraph 也给你了,why 不好好用呢?」 第 ② 句是对的。我这轮一直在用 `grep`/`sed` 手工翻,`codegraph_explore` 只调了一次。`codegraph_callers AddressInput` **一条命令**就给出 7 个调用点 —— 我本该先跑它、拿清单、再逐条比,而不是等你指一个我找一个。 ## ① 发件箱:文字对比度 1.06:1(读不出来) 设备实测(宽屏 3184,发件箱): 行标题 rgb(209,210,212) 压 rgb(241,242,244) ⇒ **1.35:1** 正文预览 rgb(224,224,224) 压 rgb(254,254,254) ⇒ **1.31:1** 时间 rgb(234,235,237) 压 rgb(241,242,244) ⇒ **1.06:1** 根因:`SentRow` **一个修饰符都没挂**。同文件里所有别的列表行都有: `MailRow`(L898/902)、`GroupHeader`(L996)、`PermissionTab`(L1373/1726)。 `SentGroupHeader` 更离谱 —— 它铺的是 `Theme.surfaceMuted` (`ohos_id_color_sub_background`,**不透明**),而收件箱那个孪生的 `GroupHeader` 早就改成 `GlassCardModifier` 了。 ⇒ 「同一个错误的两个副本,只修了一个」。 为什么"少个修饰符"会变成"字看不见":没有卡底 ⇒ 行底就是**壁纸自己**。 用户的壁纸是浅色人物图,那些位置恰好是浅灰(`241,242,244`), 而字色同向 ⇒ 掉到 1.3:1。玻璃卡的作用**正是把"壁纸不可预知"变成 "基材恒为白"**。没有这层,文字就得跟用户的壁纸赌运气。 修后实测:**19.77 / 19.10 / 9.81 / 20.56 : 1**。 ★ 没有只补一句 `backgroundColor` —— 收件箱那条路径踩过这个坑 (手写实心色 ⇒ 一行里"单出一张不透明的"),走**同一件基础件**。 ★ 同时去掉调用点上重复的那层玻璃(两层 `backgroundEffect` 会走两次), 并对齐 `MailRow` 的选中态三目。 ## ② 抄送字段:不是"缺补全",是**字段本身就不存在** `codegraph_callers AddressInput` 给出 WebUI 的 6 个挂点: ComposePage:214 收件人 ✓ 鸿蒙有(且有补全) ComposePage:277 抄送 ✗ **字段都没有** MailView:425 回复·收件人 (回复固定收件人,不需要) MailView:428 回复·抄送 ✗ 字段不存在 MailView:1087 转发·抄送 ✗ 无补全 CalendarEventEditor:471 日历事件·收件人 ✗ 无补全 而**服务端 `SendMailRequest.CC` 一直存在**,转发条里的抄送我们**反而做了**。 ⇒ 写信页缺这一项是单点遗漏,不是设计选择。 ## ③ 多地址切分:`splitEditing` 抽成跨端共享纯函数 WebUI `AddressInput` 从第一天起就有 `allowMultiple`(抄送框里 `a@x, b@y, c@z`,补全只作用于**最后一段**)。这段逻辑原先只活在 `AddressInput.tsx:38-43` 的 `useMemo` 里 ⇒ 判据 import 不到、鸿蒙没基准可抄。 移到 `lib/addressSuggest.ts` + `model/AddressSuggest.ts` 一对,并进 `cross-client-logic` 用例表(9 条边界:单地址不切 / 无分隔符 / 逗号 / 逗号后空格 / 分号 / 分号逗号取更右 / 连续分隔符 / 末尾分隔符 / 空串)。 **变异验证**:把鸿蒙侧改成只认逗号 ⇒ `pass 6 / fail 1`, 逐条报「分号也切」「分号+逗号取更右的」;恢复 ⇒ `pass 7 / fail 0`。 ## ④ 鸿蒙侧接线 - `ComposeView` 加 `@State cc`,UI 加「抄送」行(提示文案与 WebUI 逐字一致: 「多个地址用逗号分隔」); - 补全从"硬编码读 `this.to`"改成**字段无关**(`suggestField` + 三件 取值/写回/是否多地址的小方法)—— 新字段多两行,不用复制整套逻辑; - `SendMailRequest.cc` 补上(**原来填了也发不出去**,静默丢字段)。 ★ 为什么不做成真正的可复用子组件:ArkUI 的 `@Builder` 参数是**值传递**, 把 `onChange` 回调穿进去时 `this` 会丢(本仓已撞过 `@BuilderParam` 那轮)。 字段标记法没这个问题。 ## 验证 ✓ 发件箱四行逐行取像素,全部 ≥9.8:1(原 1.06~1.35) ✓ 视觉确认:每条组头/邮件行都有独立玻璃卡(原为空底直通壁纸) ✓ `cross-client-logic` 7/7,且变异会红 ✓ 编译通过、进程存活 ## 未做(诚实交代) ✗ 转发条 `forwardCc`、日历事件收件人的补全**还没接**(上面 ② 的 4 个 ✗ 我 只修了写信页那一处)。它们是**同一条线索的剩余项**,不是新发现 —— 但我不该再一次只修被点到的那一个。 --- client/electron/src/lib/addressSuggest.ts | 29 +++++ .../electron/test/cross-client-logic.test.mjs | 29 ++++- .../src/main/ets/model/AddressSuggest.ts | 38 ++++++ .../entry/src/main/ets/pages/ComposePage.ets | 109 +++++++++++++++++- .../entry/src/main/ets/pages/MainPage.ets | 83 +++++++++++-- 5 files changed, 272 insertions(+), 16 deletions(-) diff --git a/client/electron/src/lib/addressSuggest.ts b/client/electron/src/lib/addressSuggest.ts index 4d44747..0ac578a 100644 --- a/client/electron/src/lib/addressSuggest.ts +++ b/client/electron/src/lib/addressSuggest.ts @@ -21,6 +21,35 @@ export interface AddressParts { hasDot: boolean; } +/** + * 把可能含多个地址的输入切成「已完成的前缀」+「正在编辑的最后一段」。 + * + * ★★ 2026-09-21 新增(用户:「webui 存在好几个自动填充位置,比如抄送,转发等」)。 + * + * 2026-09-21 我给鸿蒙写信页写补全时,只做了**单地址**的收件人框 + * (`parseParts(整串)`)—— 而 WebUI 的 `AddressInput` 从第一天起就有 + * `allowMultiple`:抄送框里可以写 `a@x, b@y, c@z`,补全**只作用于最后一段**。 + * + * 原实现写在组件闭包里(`AddressInput.tsx:38-43` 的 `useMemo`), + * 和 `parseParts` 当初一样的病:判据 import 不到,鸿蒙只能手抄一份。 + * ⇒ 移到 `lib/`,两边共用同一份。 + * + * 行为**逐字照搬**组件里那段(含 `trimStart()`、含逗号/分号都算分隔符): + * · 分隔符取 `,` 与 `;` 里**更靠右**的那个 —— 只认逗号的话, + * 用户写分号(中文输入法下很容易打出)会把两段粘成一段。 + * · `trimStart()` 不能省:`"a@x, b"` 里 b 前面有空格, + * 不 trim 则 name 段是 `" b"`,拼出来是 `"a@x, b@..."`(空格带进地址)。 + * + * @param value 输入框全文 + * @param allowMultiple 单地址字段(收件人/主题等)传 false,抄送类传 true + */ +export function splitEditing(value: string, allowMultiple: boolean): { head: string; editing: string } { + if (!allowMultiple) return { head: '', editing: value }; + const idx = Math.max(value.lastIndexOf(','), value.lastIndexOf(';')); + if (idx < 0) return { head: '', editing: value }; + return { head: value.slice(0, idx + 1), editing: value.slice(idx + 1).trimStart() }; +} + /** * 把编辑中的一段拆成三段。 * diff --git a/client/electron/test/cross-client-logic.test.mjs b/client/electron/test/cross-client-logic.test.mjs index 601a7bd..3a1e917 100644 --- a/client/electron/test/cross-client-logic.test.mjs +++ b/client/electron/test/cross-client-logic.test.mjs @@ -300,7 +300,34 @@ const PAIRS = [ ['标题也参与匹配', 'filterIndexes', [['a@x.n', 'b@y.m'], ['缓存选型', '其他'], '缓存']], ['无匹配', 'filterIndexes', [['a@x.n'], [''], 'zzz']], ['空片段全保留', 'filterIndexes', [['a@x', 'b@y'], ['', ''], '']], - ['candidates 比 suggestions 短', 'filterIndexes', [['a@x', 'b@y'], ['t1'], 'b']] + ['candidates 比 suggestions 短', 'filterIndexes', [['a@x', 'b@y'], ['t1'], 'b']], + + /* + * ── splitEditing:多地址字段只补全**最后一段** ── + * + * ★★ 2026-09-21 新增(用户:「webui 存在好几个自动填充位置, + * 比如抄送,转发等」)。 + * + * 这段逻辑原先只活在 `AddressInput.tsx:38-43` 的 `useMemo` 里, + * 所以判据 import 不到、鸿蒙也没基准可抄 —— 我给鸿蒙写信页写补全时 + * 就只做了单地址,**抄送框至今没有补全**。 + * 移到 `lib/` + `model/` 后才比得了。 + * + * 钉的四个边界全是真会踩的: + * · 单地址字段(allowMultiple=false)不得切; + * · 分号也算分隔符(中文输入法下很容易打出); + * · 逗号后的空格要 trimStart; + * · 多个逗号取**最后**一个(前面的都已成地址)。 + */ + ['单地址不切', 'splitEditing', ['a@x, b@y', false]], + ['无分隔符', 'splitEditing', ['pi@root.new', true]], + ['逗号切', 'splitEditing', ['a@x, b@y', true]], + ['逗号后带空格', 'splitEditing', ['a@x, b@y', true]], + ['分号也切', 'splitEditing', ['a@x;b@y', true]], + ['分号+逗号取更右的', 'splitEditing', ['a@x, b@y;c@z', true]], + ['连续分隔符', 'splitEditing', ['a@x,,b@y', true]], + ['末尾就是逗号', 'splitEditing', ['a@x,', true]], + ['空串', 'splitEditing', ['', true]] ] } ]; diff --git a/client/harmony/entry/src/main/ets/model/AddressSuggest.ts b/client/harmony/entry/src/main/ets/model/AddressSuggest.ts index 49726b7..085e69d 100644 --- a/client/harmony/entry/src/main/ets/model/AddressSuggest.ts +++ b/client/harmony/entry/src/main/ets/model/AddressSuggest.ts @@ -33,6 +33,44 @@ export class AddressParts { hasDot: boolean = false; } +/** + * 把可能含多个地址的输入切成「已完成的前缀」+「正在编辑的最后一段」。 + * + * ★★ 2026-09-21 新增(用户:「webui 存在好几个自动填充位置,比如抄送,转发等」)。 + * + * 我给鸿蒙写信页写补全时,只做了**单地址**的收件人框(`parseParts(整串)`)—— + * 而 WebUI `AddressInput` 从第一天起就有 `allowMultiple`:抄送框里可以写 + * `a@x, b@y, c@z`,补全**只作用于最后一段**。 + * + * 行为**逐字照搬** `AddressInput.tsx:38-43` 那个 `useMemo`: + * · 分隔符取 `,` 与 `;` 里**更靠右**的 —— 只认逗号的话, + * 用户用中文输入法打出分号会把两段粘成一段; + * · `trimStart()` 不能省:`"a@x, b"` 里 b 前有空格,不 trim 则 name + * 段是 `" b"`,拼出的地址带空格。 + */ +export class EditingSplit { + head: string = ''; + editing: string = ''; +} + +export function splitEditing(value: string, allowMultiple: boolean): EditingSplit { + const out: EditingSplit = new EditingSplit(); + if (!allowMultiple) { + out.editing = value; + return out; + } + const comma: number = value.lastIndexOf(','); + const semi: number = value.lastIndexOf(';'); + const idx: number = comma > semi ? comma : semi; + if (idx < 0) { + out.editing = value; + return out; + } + out.head = value.slice(0, idx + 1); + out.editing = value.slice(idx + 1).trimStart(); + return out; +} + /** * 把编辑中的一段拆成三段。 * diff --git a/client/harmony/entry/src/main/ets/pages/ComposePage.ets b/client/harmony/entry/src/main/ets/pages/ComposePage.ets index 20b9553..e35e316 100644 --- a/client/harmony/entry/src/main/ets/pages/ComposePage.ets +++ b/client/harmony/entry/src/main/ets/pages/ComposePage.ets @@ -16,7 +16,7 @@ import { Insets, KEY_WINDOW_INSETS, topInset } from '../model/WindowInsets'; import { PressEffectModifier } from '../common/Surface'; import { Motion } from '../common/Motion'; import { KeyCode } from '@kit.InputKit'; -import { parseParts, mergeCandidate, nextActiveIndex, queryFor, filterIndexes, AddressParts, SuggestQuery } from '../model/AddressSuggest'; +import { parseParts, mergeCandidate, nextActiveIndex, queryFor, filterIndexes, splitEditing, AddressParts, EditingSplit, SuggestQuery } from '../model/AddressSuggest'; /** * 写邮件窗格。 @@ -75,6 +75,13 @@ export struct ComposeView { @StorageProp(KEY_WINDOW_INSETS) windowInsets: Insets = new Insets(); @State to: string = ''; + /* + * ★★ 2026-09-21 新增(用户:「webui 存在好几个自动填充位置,比如抄送,转发等」)—— + * 鸿蒙写信页原来**连抄送框都没有**,而 WebUI `ComposePage.tsx:277` 有一个, + * 且带完整补全。服务端 `SendMailRequest` 也一直有 `cc` 字段(`Models.ets`)。 + * ⇒ 字段 + 补全都补上。 + */ + @State cc: string = ''; @State subject: string = ''; @State body: string = ''; @State replyTo: string = ''; @@ -122,6 +129,26 @@ export struct ComposeView { */ private onToChanged(v: string): void { this.to = v; + this.scheduleSuggest('to'); + } + + /* + * ★★ 2026-09-21 新增(用户:「webui 存在好几个自动填充位置,比如抄送,转发等」)—— + * + * 原来补全只服务**收件人一个字段**(`fetchSuggestions()` 硬编码读 `this.to`)。 + * WebUI 的 `AddressInput` 是个**可复用组件**:同一个补全逻辑往六个地方 + * 一挂就行(收件人 / 抄送 ×写信 / 回复 / 转发 / 日历事件)。 + * + * 这里用 `suggestField` 记住"当前在给哪个字段补全", + * 把那一套拉取/过滤/应用逻辑变成**字段无关**的。 + * + * ★ 一个 `@State` 就够,不需要真做成可复用子组件:ArkUI 的 + * `@Builder` 参数是**值传递**,把 `onChange` 回调穿进子组件时 `this` + * 会丢(本仓已撞过,见 `@BuilderParam` 那段纪律)。 + * 字段标记法没有这个问题,代价是每个新字段多两行。 + */ + private scheduleSuggest(field: string): void { + this.suggestField = field; if (this.suggestTimer >= 0) { clearTimeout(this.suggestTimer); } @@ -130,6 +157,28 @@ export struct ComposeView { }, 120); } + /** 当前正在补全的字段名(`'to'` | `'cc'`),以及它是否允许多个地址 */ + private suggestField: string = 'to'; + + /** 该字段是否允许多个地址 —— 抄送类为 true(`allowMultiple`) */ + private suggestMulti(): boolean { + return this.suggestField === 'cc'; + } + + /** 读当前字段的整串值 */ + private suggestValue(): string { + return this.suggestField === 'cc' ? this.cc : this.to; + } + + /** 写回当前字段(多地址时拼在 `head` 之后,与 WebUI `apply` 同形) */ + private writeSuggestValue(v: string): void { + if (this.suggestField === 'cc') { + this.cc = v; + } else { + this.to = v; + } + } + private async fetchSuggestions(): Promise { const ctx = this.getUIContext().getHostContext(); if (ctx === undefined) { @@ -139,7 +188,13 @@ export struct ComposeView { if (api === null) { return; } - const parts: AddressParts = parseParts(this.to); + /* + * ★ 多地址字段先切段、只拿**最后一段**去解析。 + * 不切的话,`a@x, b@y` 的 `indexOf('@')` 会取到第一条的 `@`, + * 整串被当成一条地址 ⇒ 候选全是错的。 + */ + const editing: string = splitEditing(this.suggestValue(), this.suggestMulti()).editing; + const parts: AddressParts = parseParts(editing); const q: SuggestQuery = queryFor(parts); try { const res: AddressSuggestionResponse = await api.suggestAddress(q.name, q.path); @@ -200,9 +255,19 @@ export struct ComposeView { /** 选中一个候选 —— 拼回地址;name/path 段选完**仍停在补全态**(继续下一段) */ private applySuggestion(choice: string): void { - const parts: AddressParts = parseParts(this.to); + const full: string = this.suggestValue(); + const multi: boolean = this.suggestMulti(); + const split: EditingSplit = splitEditing(full, multi); + const parts: AddressParts = parseParts(split.editing); const kind: string = parts.hasDot ? 'session' : (parts.hasAt ? 'path' : 'name'); - this.to = mergeCandidate(parts, kind, choice); + const merged: string = mergeCandidate(parts, kind, choice); + /* + * 多地址:前缀(含分隔符)原样保留,只换最后一段 —— + * 与 WebUI `AddressInput.tsx:125` 的 + * `onChange(allowMultiple ? head + (head ? ' ' : '') + next : next)` + * 同一个拼法(`head` 自己尾上已经带了逗号,再加一个空格)。 + */ + this.writeSuggestValue(multi && split.head.length > 0 ? split.head + ' ' + merged : merged); if (kind === 'session') { this.suggestOpen = false; this.suggestItems = []; @@ -419,6 +484,12 @@ export struct ComposeView { * (`pi@root.new ` 与 `pi@root.new` 是两条不同地址)。 */ request.to = this.to.trim(); + /* + * ★★ 2026-09-21 新增:抄送也要 trim。 + * 服务端 `SendMailRequest.CC` 一直是存在的,只是鸿蒙从来没传过 —— + * 不加这句,用户填了抄送也发不出去(静默丢字段)。 + */ + request.cc = this.cc.trim(); request.subject = this.subject.trim(); request.body = this.body; if (this.replyTo.length > 0) { @@ -591,6 +662,36 @@ export struct ComposeView { .width('100%').height(48).padding({ left: 12, right: 12 }) .backgroundColor(Theme.surface) + /* + * ★★ 2026-09-21 新增:**抄送**字段(用户:「webui 存在好几个自动填充位置, + * 比如抄送,转发等」)。 + * + * 原来鸿蒙写信页**根本没有抄送框**,而: + * · WebUI 有(`ComposePage.tsx:277`,且带完整 `AddressInput` 补全); + * · 服务端 `SendMailRequest` 一直有 `cc` 字段(`Models.ets`); + * · 转发条里的抄送我们**反而做了**(`MailDetailPage.ets:1427`)。 + * ⇒ 写信页缺这一项是单点遗漏,不是设计选择。 + * + * 提示文案与 WebUI 逐字一致:`多个地址用逗号分隔`。 + * 补全走同一个菜单(`suggestField='cc'` ⇒ 多地址模式)。 + */ + Divider().color(Theme.border) + + Row() { + Text('抄送').fontSize(14).fontColor(Theme.textSubtleFor()).width(60) + TextInput({ placeholder: '多个地址用逗号分隔', text: this.cc }) + .layoutWeight(1).fontSize(14).backgroundColor(Color.Transparent) + .onChange((v: string) => { + this.cc = v; + this.scheduleSuggest('cc'); + }) + .onKeyEvent((e: KeyEvent) => { this.onToKey(e); }) + .onBlur(() => { this.suggestOpen = false; }) + } + .width('100%').height(48).padding({ left: 12, right: 12 }) + .backgroundColor(Theme.surface) + Divider().color(Theme.border) + /* * 候选列表 —— 贴在收件人行**下方**(WebUI `AddressInput` 的绝对定位菜单)。 * diff --git a/client/harmony/entry/src/main/ets/pages/MainPage.ets b/client/harmony/entry/src/main/ets/pages/MainPage.ets index a7a50dc..a487866 100644 --- a/client/harmony/entry/src/main/ets/pages/MainPage.ets +++ b/client/harmony/entry/src/main/ets/pages/MainPage.ets @@ -1270,6 +1270,54 @@ struct SentTab { } .width('100%').alignItems(HorizontalAlign.Start) .padding({ left: 12, right: 12, top: 10, bottom: 10 }) + /* + * ★★ 2026-09-21 修(真 bug,用户:「发件箱存在严重问题」)—— + * + * `SentRow` **一个修饰符都没挂**。而同文件里所有别的列表行都挂着: + * `MailRow` L898/902 CompositeModifier / GlassCardModifier + * `GroupHeader` L996 GlassCardModifier + * `PermissionTab` L1373/1726 … + * + * 后果**不是"少一层好看",是读不出来**。设备实测(宽屏 3184、发件箱): + * 行标题 `rgb(209,210,212)` 压底 `rgb(241,242,244)` ⇒ **1.35:1** + * 正文预览 `rgb(224,224,224)` 压底 `rgb(254,254,254)` ⇒ **1.31:1** + * 时间 `rgb(234,235,237)` 压底 `rgb(241,242,244)` ⇒ **1.06:1** + * 三行都远低于 WCAG AA(4.5:1)。 + * + * ── 为什么"少个修饰符"会变成"字看不见" ── + * 没有卡底 ⇒ 行底就是**壁纸自己**。而这个壁纸是用户头像图(浅色、 + * 大面积近似白)⇒ `Theme.textPrimary`(浅色下是深字)本该正常, + * 但实测拿到的是**浅灰字**:因为行里那几行用的是 `textPrimary`/ + * `textMuted`/`textSubtle`,而**壁纸本身**在那些位置恰好是浅灰 + * (`241,242,244`)—— 字色与底色**同向**,就掉到 1.3:1。 + * 对齐 WebUI:邮件列表的每一行都是 `.glass-card` + * (`MailList.tsx:280` 收件箱 / WebUI 发件箱同一件), + * **玻璃卡的作用正是把"壁纸不可预知"变成"基材恒为白"** —— + * 没有这层,文字就得跟用户的壁纸赌运气。 + * + * ── 为什么不只补一句 `backgroundColor` ── + * 收件箱那条路径(上面那段长注释)已经踩过同一个坑: + * 手写实心色 ⇒ 壁纸透不出来 ⇒ 一行里"单出一张不透明的"。 + * 所以这里走**同一件基础件**,不手写颜色。 + * + * ── 选中态 ── + * WebUI 发件箱的行也能被选中(`MailList.tsx:279` 的 `active`), + * 而本页 `currentMailId` 就是"正在读的那一封"。用与 `MailRow` + * **同一个三目**(选中 > 未读 > 普通)—— 发件箱没有未读概念, + * 所以只有两档。 + */ + .attributeModifier(this.currentMailId === mail.mail_id + ? CompositeModifier.of([ + GlassCardModifier.of(this.bgActive, false), + TintModifier.of(Theme.accentSoftFor(this.isDarkNow)) + ]) + : GlassCardModifier.of(this.bgActive)) + .border({ + width: 1, + color: this.currentMailId === mail.mail_id ? Theme.accentEdgeFor(this.isDarkNow) : Theme.border + }) + .borderRadius(Theme.radiusCard) + .clip(true) } @Builder @@ -1296,8 +1344,22 @@ struct SentTab { .width('100%').height(60) .padding({ left: 12, right: 12 }) .alignItems(VerticalAlign.Center) - .backgroundColor(Theme.surfaceMuted) + /* + * ★★ 2026-09-21 修(真 bug,用户:「发件箱存在严重问题」)—— + * 会话头原来铺的是 `Theme.surfaceMuted` + * (`$r('sys.color.ohos_id_color_sub_background')`,**不透明**)。 + * + * 与收件箱 `GroupHeader` 是**同一个错误的两个副本**: + * 收件箱那个已经改成 `GlassCardModifier`(L996),发件箱这个漏了。 + * + * 不透明底的代价:壁纸开着时每一条组头都是一块**实心灰板**, + * 与本仓反复强调的「玻璃 = 白 + alpha、**不是**不透明材质」正好相反。 + * + * ⇒ 与 `GroupHeader` 同一件基础件(不手写颜色)。 + */ + .attributeModifier(GlassCardModifier.of(this.bgActive)) .borderRadius(Theme.radiusCard) + .border({ width: 1, color: Theme.border }) .margin({ bottom: 6 }) .clip(true) .attributeModifier(PressEffectModifier.of()) @@ -1366,21 +1428,20 @@ struct SentTab { if (!isFlatGroup(g) && this.isExpanded(g.key)) { ForEach(g.mails, (m: MailLike) => { ListItem() { + /* + * ★★ 2026-09-21 修:这里原本再包一层 + * `.attributeModifier(GlassCardModifier.of(this.bgActive))` + * —— 而 `SentRow` 自己也挂了一份(刚补的)。 + * 两层玻璃卡叠加会让 `backgroundEffect` 走两次, + * 且外层的 `border`/`borderRadius` 与内层重复。 + * 收件箱那边(`MailRow`)就是"卡在行自己身上、 + * 调用点只给缩进和间距",这里改成同一形状。 + */ Column() { this.SentRow(m) } .width('100%') - .attributeModifier(GlassCardModifier.of(this.bgActive)) - .borderRadius(Theme.radiusCard) - /* 与 `MailRow` 同一口径:选中(正在读的那一封)上品牌边。 - WebUI 组内条目也是这么标的(`MailList.tsx:229` - 把 `currentMailID` 传进 `MailItem` 的 `active`)。 */ - .border({ - width: 1, - color: this.currentMailId === m.mail_id ? Theme.accentEdgeFor(this.isDarkNow) : Theme.border - }) .margin({ bottom: 6 }) - .attributeModifier(PressEffectModifier.of()) .onClick(() => { this.openMail(m); }) } .width('100%')