跨端: 2in1 键盘可达(Ctrl+N 写信 / ↑↓ 换补全 / Enter 选中)+ 三处判据被实测改判

用户 2026-09-21:「接下来做一下 2in1 上的快捷键,比如快捷键打开发信页面」
「上下键切换发信目标」「回车展开输入框等」

## ① 打开发信页:Ctrl+N,用官方 `keyboardShortcut`

先查了官方文档再动手,避开两条静默失败的坑:

  · `keyboardShortcut`(组件快捷键事件,API 10+)「**无论组件是否获焦** ——
    只要窗口获焦,快捷键就会响应」。而 `onKeyEvent` 要求**组件先获焦**,
    邮件列表里焦点落在哪是不确定的(点一下就换)⇒ 用它做全局快捷键会时灵时不灵。
  · 「多个不同组件设置相同组合键 ⇒ 只响应节点树**深度最浅**的那个」。
    ⇒ 再给别处的"写信"按钮补一个 Ctrl+N 不是"多一个入口",而是**让后来那处永久失效**。
    判据因此钉"全仓只许绑一处"。

绑定位置在 `MainPage` 的**根 Stack**(窗口组件树的根)—— 绑在 FAB 上不行,
它在 `if` 分支里、窄屏/宽屏位置也不同,会随分支挂卸。

键位 `Ctrl+N` 避开了官方列出的五个**禁止绑定**组合(Alt+F4/Alt+Tab/Ctrl+Shift+Esc…)。
判据直接解析调用参数校验这三点。

## ② 根够不着 `openCompose()` ⇒ 照 `PushService` 的"两半"形状

`openCompose()` 住在**条件挂载**的 `CommPage` 上(`if (currentIndex === 0)`),
根组件拿不到它。新建 `common/ComposeIntent.ets`:

  · **格子**(`pending`)—— 用户此刻在别的页、`CommPage` 还没实例化;
  · **回调**(`listener`)—— 用户此刻就在通信页、页面早挂载完了。

缺任何一半都是**按键静默失效**:只有格子 ⇒ `aboutToAppear` 不重跑;
只有回调 ⇒ 没有监听者。这形状与 `api/PushService.ets` 处理"点通知跳转"时
踩的是同一个坑,那边注释里已写过解法 —— 这次是照着抄,不是重新踩。

## ③ 收件人三段式补全(↑↓ 切换、Enter/Tab 选中、Esc 收起)

WebUI 的逻辑原先**散在组件闭包里**(`parseParts` 没 export、`apply`/`onKeyDown`
直接改 React state)⇒ 判据根本 import 不到。先把它抽成一对纯逻辑:
`client/electron/src/lib/addressSuggest.ts` ↔ `model/AddressSuggest.ts`,
**抽的时候行为一字不改**(抽出来顺手"改进"会让判据比新行为、线上跑旧行为,两边都错)。
`AddressInput.tsx` 改接这份 lib。

`cross-client-logic` 新增 `AddressSuggest` 一对,22 个用例两边逐例比。
其中 `nextActiveIndex(0,3,-1)` 是**负下标陷阱**:JS 的 `%` 对负数返回负数
(`-1 % 5 === -1`),而负下标在数组访问里**不报错**(返回 `undefined`),
只表现为"按上键后没有任何一项高亮"。直接写 `(i-1) % n` 就会这样静默坏掉。

候选走**内联渲染**而不是 `bindPopup`/`bindMenu`:那两者各有焦点体系,
会先吃掉按键 ⇒ "↑↓ 换候选"落不到 `onToKey` 上。

## ④ 另修一个真 bug:`AddressSuggestion` 字段名整套写错

模型声明 `value`/`kind`,而服务端(`contacts.go:206`)给的是 `alias`/`title`/`source`
—— **从来没返回过** `value`/`kind`。按本仓纪律「声明了服务端从不返回的字段 ⇒ 删掉声明」改正。

顺带守住 `title` 的 `omitempty`:缺键时裸 cast 给 `undefined`(**不是**类里那个 `= ''`),
直接读会 `Cannot read property of undefined` —— 本仓在 `MailDetail.normalize()` 上
踩过同一形状(整页白屏)。判据钉住那句 `typeof … === 'string'` 的守。

## ⑤ 三处判据被**实测**改判(不是我挑一边,是拿数字定的)

### a. 卡片不该有模糊 —— 反转原断言
原判据断言「玻璃卡要走参数化 `backgroundEffect`」。那是**记录旧实现的副作用**
(重言式)。逐字读 WebUI 的 CSS:全仓 `backdrop-filter` **只有两处**
(`.glass-control` 8px/1.1、`.narrow-nav` 18px/1.5),而 `.glass-card`(`index.css:1629`)
**完全没有** —— 它的玻璃感是 `rgb(255 255 255 / 0.92)` 这个 alpha。
留着旧断言更坏:下次谁把卡片改成正确的"白 + alpha"会被判红,然后去**把模糊加回来**。

### b. 我试了"把模糊移到导航条",被实测否掉
推断「真归属是底栏」,于是把底栏改成 `backgroundEffect({radius:18, saturation:1.5})`。
实测(模拟器窄屏 1008×2232,底栏中心列 x=504):

    y      backgroundEffect        backgroundBlurStyle
    1960   rgb(191,199,209)        rgb(234,235,239)      ← 差 -36 亮度
    2060   rgb(206,211,219)        rgb(234,235,239)      ← 差 -24

⇒ `backgroundEffect` **只给模糊、不给底色**,壁纸原样透上来,底栏暗了 24~36 级。
WebUI 的 `.narrow-nav` 是**两条声明**组合的(`background-color` + `backdrop-filter`),
我只搬了后者。系统材质**同时含色调 + 模糊 + 深浅两套** ⇒ 在这里它才是正解
(§7.12 把它判成"有意差异"是对的,我的"改进"是退步)。
判据改成**反向钉住** `backgroundEffect`,并把这段实测数字留在 Theme.ets 里。

### c. 页签条判据记录的是被否掉的"胶囊"
原判据钉 `TAB_BAR_RADIUS`/`TAB_BAR_SIDE`/`TAB_BAR_TOP` —— 那正是用户否掉的形状
(「你又在内部套了一个胶囊」)。CDP 读 WebUI 的实测几何:

    .comm-pane   x=80 w=320 radius=14px overflow=hidden   ← 窗格,裁圆的是它
    tabstrip     x=80 w=320 radius=0px                    ← 条自己无圆角

设备实测(`uitest dumpLayout`):页签条 `[28,140][980,267]`、窗格 `[28,140][980,1957]`
⇒ 左右边缘逐像素相同。判据改为钉"与窗格齐平 + 只有左上角圆角"这两条**不变式**,
`TAB_BAR_RADIUS`/`TAB_BAR_SIDE` 一并**删除**(留着就是孤儿,会邀请人把胶囊拼回来)。

## ⑥ 顺带修两条判据自己的正则
`{6}` 看不见 8 位色 ⇒ 把 `glassCard`/`glassCardWall` 报成"清册过期",
**病因报错了**。改成 `{6}(?:[0-9A-Fa-f]{2})?`(不能写 `{6,8}`,那会连 7 位也放进来)。
半透明禁令改为**枚举白名单**(不是放宽:遮罩那个真实约束原样保留,
`overlay` 写成 8 位单色照样红)。

新增判据 `harmony-2in1.test.mjs` 12 条;`files=34 checks=543 pass=540 fail=2`(收尾前)。
This commit is contained in:
2026-09-21 13:29:24 +08:00
parent f83c23362a
commit 125aec1191
22 changed files with 1608 additions and 109 deletions

View File

@ -195,29 +195,58 @@ export class Theme {
static readonly cardMaterial: BlurStyle = BlurStyle.BACKGROUND_THIN;
/*
* ── 玻璃的**参数化**配方(比材质档更接近 WebUI)──
* ── 关于「参数化玻璃」(`backgroundEffect`):**本工程不用** ──
*
* `BlurStyle` 是**固定档**(系统给的那几组),而 WebUI 的玻璃是**显式参数**:
* 2026-09-21 实测记录(我被自己的实验推翻,留证以免后人再走一遍)。
*
* .narrow-nav backdrop-filter: blur(18px) saturate(1.5) ← 浮条
* --bg-blur-panel: 10px ← 面板
* 这里原先有两个令牌 `glassCardRadius = 18` / `glassCardSaturation = 1.5`,
* 注释自己写着「与 WebUI `.narrow-nav` 逐字同源」—— 也就是**底部导航条**
* 那一档(`index.css:1204`:`backdrop-filter: blur(18px) saturate(1.5)`),
* 名字却叫 `glass**Card**…`。名与实不符立刻兑现了代价:
* 我照名字把它挂到了**每张卡片**上,而 WebUI 的卡片根本没有 `backdrop-filter`
* (`.glass-card` 只有白 + alpha)⇒ 多层模糊叠加成灰雾 + 滚动掉帧。
*
* 关键差别是 **saturate(饱和度增强)**:它让透过玻璃的颜色更鲜艳,
* 正是"玻璃感"的主要来源。`BlurStyle` 没有这个旋钮,
* 所以纯用材质档会偏灰 —— 用户说"不够炫酷"就是这个观感。
* 上一轮"卡片去模糊"之后,这两个令牌**失去全部消费者** ——
* 是 `cross-client-theme` 的**孤儿令牌判据**把它们报出来的
* (「这些 Theme 令牌在页面/组件里一次都没被引用」),
* 而不是烂在那里没人知道。
*
* ArkUI 的参数化 API 是 `backgroundEffect({ radius, saturation, brightness })`,
* 字段与 CSS 的 `backdrop-filter` 一一对应,于是可以**照抄 WebUI 的数**。
* 取值规则(不是拍的):
* · `CARD_RADIUS = 18` / `CARD_SATURATION = 1.5` —— 与 WebUI `.narrow-nav`
* 逐字同源(那是 WebUI 里最"玻璃"的一档)。卡片面积小,用最有质感的档;
* · 面板面积大、透太多会伤正文可读性 ⇒ 用 `--bg-blur-panel: 10px`,
* 饱和度取 1.3(比 1.5 收一点 —— 大面积高饱和会让整页发艳)。
* ── 我接着做了一次实验,然后被实测否掉 ──
*
* ★ 为什么不直接抄 1.5 给面板:WebUI 那边这两个值本来就是**分开的**
* (10px 面板 / 18px 浮条),抄就得抄全套,不能只挑一个数。
* 我推断「真归属是导航条」,于是把底栏从
* `backgroundBlurStyle(Theme.navMaterial)` 改成
* `backgroundEffect({radius:18, saturation:1.5})`,理由是
* 「固定材质档没有 `saturate` 旋钮、观感偏灰」。
*
* 实测(模拟器窄屏 1008×2232,取底栏中心列 x=504 的像素):
*
* y backgroundEffect backgroundBlurStyle
* 1960 rgb(191,199,209) rgb(234,235,239) ← 差 -36 亮度
* 2000 rgb(198,204,212) rgb(234,235,239) ← 差 -31
* 2060 rgb(206,211,219) rgb(234,235,239) ← 差 -24
*
* ⇒ `backgroundEffect` **只给模糊、不给底色**,壁纸原样透上来,
* 整条底栏暗了 24~36 个亮度级 —— 比原来的"偏灰"糟得多。
*
* ★ 为什么:WebUI 的 `.narrow-nav` 是**两条声明**组合出来的 ——
* background-color: rgb(var(--nav-bg));
* backdrop-filter: blur(18px) saturate(1.5);
* 我只搬了后者、丢了前者。而 `backgroundBlurStyle` 的**系统材质本身**
* 就同时含「色调 + 模糊 + 深浅两套」,正好把这两条一起给了。
*
* ★ 更要紧的一层:WebUI 那个 `--nav-bg` 是**手写 alpha**,跟不了深色主题
* (§7.12 那一行的原话:「手写 alpha 跟不了深色」)。⇒ 在这里系统材质
* 不是"退而求其次",而是**唯一能同时满足深浅两套**的方案。
* §7.12 当初把这条判成"有意差异"是对的,我的"改进"才是退步。
*
* ⇒ 结论:`navMaterial`(`BlurStyle.COMPONENT_THICK`)保持不变;
* 两个令牌**删除**(不删就是孤儿,孤儿判据会一直红着)。
*
* ★ 留这段注释而不是直接删干净的原因:**下一个人会再想一遍同样的事**
* (WebUI 那行 `saturate(1.5)` 就摆在那里,很显眼)。
* 把"试过了、实测数字在此、结论是不要"写在这里,
* 比让他重跑一遍模拟器便宜。
*/
static readonly glassCardRadius: number = 18;
/*
* ── 卡片的"玻璃":**白色 + alpha,不做模糊** ──
*
@ -256,9 +285,30 @@ export class Theme {
* 「基材恒为白,主题之间只差 alpha」)。深色下靠 `brightness` 与
* 文字令牌保证对比度,而不是把底也调黑。
*/
/*
* ── 卡片底:白色 + alpha(**与 WebUI `.glass-card` 逐字同源**)──
*
* index.css:1629 .glass-card → rgb(255 255 255 / 0.92)
* index.css html[data-bg='on'] … → rgb(255 255 255 / 0.78)
* ⇒ glassCard #EBFFFFFF(0.92 × 255 ≈ 235 = 0xEB)
* ⇒ glassCardWall #C7FFFFFF(0.78 × 255 ≈ 199 = 0xC7)
*
* ★ 为什么必须是**手写色**而不是 `$r('sys.*')`:
* 系统材质(`bgMaterial*` / `compBackground*`)是**不透明**的整块色板,
* 没有"白 92% 叠在壁纸上"这一档。而 WebUI 的玻璃观感**正是**这个 alpha
* —— 它不是模糊(`.glass-card` 全仓没有 `backdrop-filter`,只有
* `.glass-control` 与 `.narrow-nav` 有)。换成系统材质 ⇒ 壁纸被整块盖死,
* 那正是用户报的「玻璃不透明」。
*
* ★ 为什么用"白 + alpha"而不去调亮度模拟:
* 壁纸是**用户可换的图**,颜色不可预知;只有半透明白能同时适配浅壁纸
* 与深壁纸(深色档另有 `DARK_DIM_MIN` 兜底)。
*
* 已登记进 `cross-client-theme.test.mjs` 的 `SELF_OWNED_COLORS`
* (那条判据会红着提醒"理由不能只存在于写它那个人的记忆里")。
*/
static readonly glassCard: string = '#EBFFFFFF';
static readonly glassCardWall: string = '#C7FFFFFF';
static readonly glassCardSaturation: number = 1.5;
/**
* 「悬浮玻璃板」的另外两半:**投影 + 发丝描边**。
*
@ -386,6 +436,26 @@ export class Theme {
* 'plain' 档徽标底色(联系人数)。★ 必须石板灰而非红:
* 三档被压成两档时 `'plain'` 会走 danger,看着像"有未读"
*
* ★★ 2026-09-21 补登记:`glassCard` / `glassCardWall`
*
* · glassCard #EBFFFFFF WebUI `.glass-card`(`index.css:1629`)
* `background-color: rgb(255 255 255 / 0.92)`
* (0.92 × 255 ≈ 235 = 0xEB)
* · glassCardWall #C7FFFFFF 上者的"有壁纸"档 —— WebUI
* `html[data-bg='on'] .glass-card` 的 `… / 0.78`
* (0.78 × 255 ≈ 199 = 0xC7)
*
* 为什么必须自己写而不是走 `$r('sys.*')`:系统材质
* (`bgMaterial*` / `compBackground*`)是**不透明**的整块色板,
* 没有"白 92% 叠在壁纸上"这一档。而 WebUI 的玻璃观感**正是**这个 alpha
* —— 它**不是**模糊(`.glass-card` 全仓没有 `backdrop-filter`;
* 只有 `.glass-control` 与 `.narrow-nav` 有)。换成系统材质 ⇒ 壁纸被
* 整块盖死,那正是用户报的「玻璃不透明」。
*
* 为什么是"白 + alpha"而不是调亮度模拟:壁纸是**用户可换的图**,
* 颜色不可预知;只有半透明白能同时适配浅壁纸与深壁纸
* (深色档另有 `DARK_DIM_MIN` 兜底)。
*
* SSE 连接指示器的四个状态色(逐档对齐 WebUI 的 Tailwind 类,
* `ConnectionIndicator.tsx:22-25`):
* · sseConnected #22C55E bg-green-500 已连接