跨端: 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:
@ -231,6 +231,26 @@ const SELF_OWNED_COLORS = [
|
||||
* 深色变体 `accentEdgeDark` 对齐 WebUI `.dark` 段的 `--c-blue-200`。
|
||||
*/
|
||||
'accentEdge', 'accentEdgeDark',
|
||||
/*
|
||||
* ★★ 2026-09-21 补登记:`glassCard` / `glassCardWall`。
|
||||
*
|
||||
* 这两个是卡片底 —— 与 WebUI `.glass-card` **逐字同源**:
|
||||
* index.css:1629 .glass-card { background-color: rgb(255 255 255 / 0.92) }
|
||||
* index.css html[data-bg='on'] … { background-color: 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`)。
|
||||
* 换成系统材质 ⇒ 壁纸被整块盖死,就是用户报的「玻璃不透明」。
|
||||
*
|
||||
* 为什么是"白 + alpha"而不是调亮度模拟:
|
||||
* 壁纸是**用户可换的图**,颜色不可预知;只有"半透明白"能同时满足
|
||||
* 浅壁纸与深壁纸(深色档另有 `DARK_DIM_MIN` 兜底)。
|
||||
*/
|
||||
'glassCard', 'glassCardWall',
|
||||
// 业务语义色:系统没有对应物(同意 / 拒绝 / 警示)
|
||||
'approve', 'danger', 'approveBg', 'approveFg', 'dangerBg', 'warnBg', 'warnFg',
|
||||
// 权限档位与预算档位的胶囊配色(档位是产品语义,系统不认识"plan/workspace/full")
|
||||
@ -336,9 +356,33 @@ test('B|旧机制不得回来:手写玻璃 alpha、替系统猜深色、与
|
||||
*/
|
||||
assert.ok(!/navBg(Light|Dark)/.test(codeOnly), 'navBgLight/navBgDark 不该再出现(玻璃交给系统材质)');
|
||||
|
||||
// 半透明色(8 位 #AARRGGBB)= 手写玻璃/手写遮罩那一类,一律不许
|
||||
const translucent = rawColors(codeOnly).filter(h => h.startsWith('#') && h.length === 9);
|
||||
assert.deepEqual(translucent, [], '源码里出现 8 位半透明色 —— 半透明属于系统材质/语义色的职责');
|
||||
/*
|
||||
* 半透明色(8 位 #AARRGGBB)= 手写玻璃/手写遮罩那一类,**原则上**不许。
|
||||
*
|
||||
* ★★ 2026-09-21:只有**两处**例外,且是"例外"而非"放宽" —— 理由是
|
||||
* 它们抄的对象**本身就是白色 + alpha**,而系统材质里没有这一档:
|
||||
*
|
||||
* index.css:1629 .glass-card → rgb(255 255 255 / 0.92)
|
||||
* index.css html[data-bg='on'] … → rgb(255 255 255 / 0.78)
|
||||
*
|
||||
* 系统材质(`bgMaterial*` / `compBackground*`)是**不透明**的整块色板。
|
||||
* 若把它们换成系统材质,壁纸会被整块盖死 —— 那正是用户报的
|
||||
* 「界面完全不透明,不显示背景」「玻璃也不透明」。
|
||||
*
|
||||
* ★ 为什么不允许"随手加一个":例外是**枚举**的(下面这个白名单),
|
||||
* 不是"只要是玻璃就放行"。加第三个半透明色 ⇒ 这条判据照样红,
|
||||
* 而那时得先回答"系统为什么没有对应物"这个问题。
|
||||
*
|
||||
* ★ 白名单项与 `SELF_OWNED_COLORS` 是**两个不同的问题**:
|
||||
* 那个问"色值有没有登记理由",这个问"半透明这件事合不合法"。
|
||||
*/
|
||||
const TRANSLUCENT_OK = ['#EBFFFFFF', '#C7FFFFFF']; // glassCard / glassCardWall
|
||||
const translucent = rawColors(codeOnly)
|
||||
.filter(h => h.startsWith('#') && h.length === 9)
|
||||
.filter(h => !TRANSLUCENT_OK.includes(h.toUpperCase()));
|
||||
assert.deepEqual(translucent, [],
|
||||
'源码里出现 8 位半透明色 —— 半透明属于系统材质/语义色的职责;' +
|
||||
`只有 glassCard/glassCardWall 这两个有登记理由(${TRANSLUCENT_OK.join(' ')})`);
|
||||
|
||||
// 鸿蒙侧不写 CSS 式颜色函数
|
||||
assert.ok(!/\brgba?\s*\(/.test(codeOnly), '鸿蒙源码里不该出现 rgb()/rgba()(那是 WebUI 的写法)');
|
||||
@ -588,17 +632,87 @@ test('C|玻璃:位置用系统材质、不许叠、每一处都要登记(
|
||||
assert.match(main, /\.backgroundBlurStyle\(Theme\.navMaterial\)/, '导航条要用系统材质(不是手写 alpha)');
|
||||
|
||||
/*
|
||||
* 参数化玻璃的**取值也要来自令牌**:`backgroundEffect({radius, saturation})`
|
||||
* 若不引 `Theme.glassCard*`,就等于把"玻璃浓度"写死在调用点 ——
|
||||
* 与 `#B8FFFFFF` 同类的问题(只是换了个旋钮)。
|
||||
* ══════════════════════════════════════════════════════════════════════
|
||||
* ★★ 2026-09-21 **反转**:卡片**不该**有模糊;模糊只属于导航/控件层
|
||||
* ══════════════════════════════════════════════════════════════════════
|
||||
*
|
||||
* 这条原先断言的是反面:
|
||||
* assert.ok(eff.length > 0, '玻璃卡要走参数化 backgroundEffect …')
|
||||
*
|
||||
* 那是**记录旧实现的副作用**,不是不变式 —— 本仓纪律:
|
||||
* 「记录旧实现副作用的判据是重言式」。旧实现确实给卡片挂了
|
||||
* `backgroundEffect({radius:18, saturation:1.5})`,于是判据照着它写。
|
||||
*
|
||||
* 但那个参数是**底部导航条那一档**(WebUI `.narrow-nav` 的
|
||||
* `blur(18px) saturate(1.5)`,`index.css:1204`),被我错安到了卡片上。
|
||||
*
|
||||
* ── WebUI 的事实(逐字读它的 CSS)──
|
||||
*
|
||||
* 全仓 `backdrop-filter` **只有两处**:
|
||||
* html[data-bg='on'] .glass-control → blur(8px) saturate(1.1)
|
||||
* .narrow-nav → blur(18px) saturate(1.5)
|
||||
* 而 `.glass-card`(`index.css:1629`)**完全没有 backdrop-filter**:
|
||||
* .glass-card { background-color: rgb(255 255 255 / 0.92); }
|
||||
* html[data-bg='on'] … { background-color: rgb(255 255 255 / 0.78); }
|
||||
* 它只有 radius + border + bg + transition。
|
||||
*
|
||||
* ⇒ 卡片的"玻璃感"来源是**半透明白**,不是模糊。给卡片加模糊的后果
|
||||
* 已实测:叠加成灰雾 + 掉帧,且用户报「邮件看着没有玻璃效果」。
|
||||
*
|
||||
* ── 所以现在钉的是这个方向 ──
|
||||
*
|
||||
* ① 卡片层(Surface.ets 的 GlassCardModifier)**不得**有 backgroundEffect;
|
||||
* ② 底色的两个 alpha 必须来自 Theme 令牌(不是写死在调用点);
|
||||
* ③ 真正的模糊只许出现在导航/控件层,且参数引 Theme 令牌。
|
||||
*
|
||||
* ★ 为什么不留着"必须有"那条:留着它,下次谁把卡片改成正确的
|
||||
* "白 + alpha"就会**被这条件据判红**,然后去把模糊加回来 ——
|
||||
* 判据会主动把实现推回错误方向。这比没有判据更坏。
|
||||
*/
|
||||
const surfaceSrc = code(join(HARMONY_ETS, 'common', 'Surface.ets'));
|
||||
const eff = [...surfaceSrc.matchAll(/\.backgroundEffect\(/g)];
|
||||
assert.ok(eff.length > 0, '玻璃卡要走参数化 `backgroundEffect`(BlurStyle 没有 saturation,观感偏灰)');
|
||||
assert.match(surfaceSrc, /radius:\s*Theme\.glass\w+/,
|
||||
'`backgroundEffect` 的 radius 要引 Theme 令牌(不许写死数字)');
|
||||
assert.match(surfaceSrc, /saturation:\s*Theme\.glass\w+/,
|
||||
'`backgroundEffect` 的 saturation 要引 Theme 令牌(它是"玻璃感"的主旋钮)');
|
||||
|
||||
/* ① 卡片构造器里不得出现模糊 */
|
||||
const cardAt = surfaceSrc.indexOf('class GlassCardModifier');
|
||||
assert.ok(cardAt > 0, '要能找到 GlassCardModifier');
|
||||
const cardBody = surfaceSrc.slice(cardAt, surfaceSrc.indexOf('\nclass ', cardAt + 10));
|
||||
assert.ok(!/\.backgroundEffect\(/.test(cardBody),
|
||||
'卡片层不得挂 backgroundEffect —— WebUI `.glass-card` 没有 backdrop-filter,' +
|
||||
'卡片的玻璃感来自半透明白。加模糊会叠成灰雾并掉帧(已实测)。');
|
||||
|
||||
/* ② 两个 alpha 令牌必须真的被卡片读(否则是孤儿令牌,见 C 的另一段) */
|
||||
assert.match(cardBody, /Theme\.glassCard\b/,
|
||||
'卡片要读 Theme.glassCard(0.92 那档,照抄 WebUI rgb(255 255 255 / 0.92))');
|
||||
assert.match(cardBody, /Theme\.glassCardWall\b/,
|
||||
'卡片要读 Theme.glassCardWall(0.78 那档,对应 WebUI html[data-bg=\'on\'])');
|
||||
|
||||
/*
|
||||
* ③ **不许**用 `backgroundEffect` —— 2026-09-21 实测否掉了我上一版的推断。
|
||||
*
|
||||
* 我曾把底栏改成 `backgroundEffect({radius:18, saturation:1.5})`,理由是
|
||||
* 「WebUI `.narrow-nav` 有 `saturate(1.5)`,而固定材质档没这个旋钮」。
|
||||
*
|
||||
* 实测(模拟器窄屏 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)。⇒ 系统材质是这里**唯一**能兼顾深浅的方案。
|
||||
*
|
||||
* ★ 为什么判据要**反向**钉住它:不留这条,下一个人看到 WebUI 那行
|
||||
* `saturate(1.5)` 会再想一遍同样的事,而他要重跑一遍模拟器才能知道答案。
|
||||
*/
|
||||
const effSites = [...surfaceSrc.matchAll(/\.backgroundEffect\(/g)];
|
||||
assert.equal(effSites.length, 0,
|
||||
'不得使用 `backgroundEffect` —— 它不给底色,实测底栏会暗 24~36 个亮度级;' +
|
||||
'系统材质 `backgroundBlurStyle(Theme.navMaterial)` 同时含色调 + 模糊 + 深浅两套');
|
||||
|
||||
/*
|
||||
* 自检:造一次**链式叠用**(`X.blur().blur()`),确认上面的判定抓得到。
|
||||
@ -820,7 +934,23 @@ test('遮罩:交给系统的遮罩语义色("随主题换向"这件事现在
|
||||
'而只盯声明的判据("声明了且不是 NONE")**照样绿**。');
|
||||
// 用它的必须是**自绘遮罩**的地方(系统自带遮罩的弹窗不需要它)
|
||||
assert.ok(overlayUsers.every(f => f.endsWith('.ets')), `遮罩使用点应该是页面:${overlayUsers.join('、')}`);
|
||||
assert.ok(!/#[0-9A-Fa-f]{8}/.test(stripComments(harmony)), '遮罩不该再写成色与透明度焊死的 #AARRGGBB 单值');
|
||||
/*
|
||||
* 遮罩不该写成 `#AARRGGBB` 单值(色与透明度焊死,跟不了主题换向)。
|
||||
*
|
||||
* ★★ 2026-09-21 与 B 条同因修正:原来是**全仓**扫 `#……{8}`,
|
||||
* 于是 `glassCard` / `glassCardWall` 这两个**不是遮罩**的令牌也把它撞红了 ——
|
||||
* 而它们的理由与遮罩无关(它们是 WebUI `.glass-card` 的
|
||||
* `rgb(255 255 255 / 0.92)` 与 `/ 0.78`,是卡片底、不是遮罩)。
|
||||
*
|
||||
* ⇒ 把例外限制成**同名白名单**(与 B 条的 `TRANSLUCENT_OK` 同一份名单),
|
||||
* 而不是把这条判据整个放宽:遮罩那个真实约束**原样保留**。
|
||||
* `overlay` / `wallpaperScrim` 若被写成 8 位单值,这条照样红。
|
||||
*/
|
||||
const glassAlphaOk = ['#EBFFFFFF', '#C7FFFFFF']; // 与 B 条同一份名单
|
||||
const all8 = (stripComments(harmony).match(/#[0-9A-Fa-f]{8}/g) ?? [])
|
||||
.filter(h => !glassAlphaOk.includes(h.toUpperCase()));
|
||||
assert.deepEqual(all8, [],
|
||||
`遮罩不该再写成色与透明度焊死的 #AARRGGBB 单值;只有 glassCard/glassCardWall 有登记理由(实见 ${all8.join(' ')})`);
|
||||
// WebUI 侧同构:颜色两套(浅/深,同一个变量名换向)+ 透明度独立
|
||||
assert.match(web, /--bg-scrim: 255 255 255/, 'WebUI 浅色遮罩色');
|
||||
assert.match(web, /--bg-scrim: 0 0 0/, 'WebUI 深色遮罩色');
|
||||
@ -911,7 +1041,20 @@ test('★ 手写色清册**跨文件**:全 ets 树里每个 `X: string = \'#RR
|
||||
for (const f of tree) {
|
||||
const rel = f.slice(HARMONY_ETS.length + 1);
|
||||
const src = prose(f).replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
|
||||
for (const m of src.matchAll(/(\w+)\s*:\s*string\s*=\s*'(#[0-9A-Fa-f]{6})'/g)) {
|
||||
/*
|
||||
* ★★ 2026-09-21 **正则太窄**(本仓反复出现的同一形状:判据自己的正则
|
||||
* 比被判的东西窄 ⇒ 静默漏报,然后以"清册过期"的错误面貌报出来)。
|
||||
*
|
||||
* 原来写 `{6}` —— 只认 6 位。而 `glassCard` / `glassCardWall` 是
|
||||
* **8 位**(`#EBFFFFFF` / `#C7FFFFFF`,半透明白是 `glass-card` 的本质),
|
||||
* 于是它们扫不到 ⇒ 下面那句 `assert.ok(byName.has(n))` 报
|
||||
* 「清册里的 glassCard 已不存在(清册过期)」——
|
||||
* **报的是错的病因**:不是清册过期,是这个正则看不见 8 位色。
|
||||
*
|
||||
* ⇒ 改成 `{6}(?:[0-9A-Fa-f]{2})?`:6 位必有、8 位可选。
|
||||
* 注意**不能**写成 `{6,8}` —— 那会连 7 位这种非法长度也放进来。
|
||||
*/
|
||||
for (const m of src.matchAll(/(\w+)\s*:\s*string\s*=\s*'(#[0-9A-Fa-f]{6}(?:[0-9A-Fa-f]{2})?)'/g)) {
|
||||
declared.push({ file: rel, name: m[1], value: m[2] });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user