用户四条:
①「2in1 手势」(选了 悬停/右键菜单/触控板 + 快捷键:主页回车写信、
详情页回车回复、Esc 返回)
②「宽屏状态一个邮件被反复点击会被多次填充到右侧」
③「你在登陆页是不是没有做 enter 等键的监听」——**确实漏了**
④「app 顶栏为什么不沉浸」+「右侧三键应当有独立避让」
② 叠栈(实测复现 → 修 → 实测通过)
根因是框架语义用错:pushPath 默认 LaunchMode.STANDARD 每次入栈 ⇒
重建详情组件 + 重拉数据 + 重放入场动画;返回还要按多次。
而 WebUI 是 `set({currentMail})` 幂等赋值(mailStore.ts:115)。
改用 LaunchMode.MOVE_TO_TOP_SINGLETON(官方:同名已在栈里就移上去、
不新建),MainPage + ContactsTab 两处 push 点都改(只改一边=换栏点
又不正常)。
实测:连点同一封 3 次 → **点一次返回就回占位**(修复前要按 3 次)。
③ 登录页回车(用户点出来的真实缺失)
WebUI 是 `<form onSubmit={submit}>`(LoginPage.tsx:92)——浏览器里
输入框按回车就提交;鸿蒙登录页**一行键盘监听都没有**。
补上,走**已有的** doLogin()(不另写一条登录路,免得与按钮的条件分叉)。
① 快捷键:新增 model/KeyboardShortcuts.ts(规则集中一处,三页共用)
· 主页根 Stack:Enter → 写信(与 Ctrl+N 同一个 ComposeIntent.request)
· 详情页:Enter → 开回复(复用 openReplyWithMorph,连动画都不另开);
Esc → 返回,且**弹层开着时先关弹层**再按才返回(否则用户想关回复框
却被踢回列表,输入到一半的内容全没)
· 用键事件**冒泡**:子组件先拿到、未消费才到页面根 ⇒ "详情优先、
主页兜底"由框架保证,不是我自己排的优先级
★ 为何不用 keyboardShortcut:它只收组合键;不带修饰键时只认 FunctionKey,
而 FunctionKey 枚举(enums.d.ts:3444)**没有 Enter**(只有 ESC/F1-F12/
TAB/方向键)⇒ 单按回车表达不出来。
① 悬停反馈:MailRow/SentRow 挂 onHover + Theme.surfaceMuted
(该令牌此前**零使用**,注释本就写着"列表行 hover",正好归位)。
不用 .hoverEffect():系统叠层会与选中/未读的 accentSoft 叠成第三种颜色。
★ 状态存 mail_id 而不是布尔:行本体是 @Builder(无自身状态),
布尔会变成"悬停一行、同栏全亮",所以状态放栏上、存"是哪一封"。
④ 沉浸顶栏:EntryAbility 加 setWindowDecorVisible(false)
实测(2in1 截图硬证):标题栏(AgentMailHarmony)下面**还有一条白条**,
内容从第二条下面才开始。根因是**从未调过装饰接口**⇒用系统默认(PC 带标题栏)。
setWindowLayoutFullScreen(true) 管的是"内容铺到**屏幕**四边",
**不包含**"窗口自己的标题栏是否隐藏"——两件不同的事。
④ 三键避让:Insets 加 windowDecor + getWindowDecorHeight()
隐掉标题栏白条后,系统仍在右上角**浮着**三键(官方:全屏悬浮态固定 37vp)。
而 2in1 **没有状态栏** ⇒ TYPE_SYSTEM.topRect 是 0 ⇒ 只看 statusBar 就
认定"顶部无需避让",内容(右上是「授权」页签)被三键压住。
AvoidAreaType 六种里**没有**"标题栏/三键"这一类,只能单独读
getWindowDecorHeight()(它直接返回 vp)。
避让取**较大者**不加:两者互斥(有状态栏的形态没装饰,反之亦然)。
实测日志:`statusBar=0 navIndicator=0 windowDecor=37`,页签条下移。
判据(13 条新增/改,全部变异验证过)
- 新增 5 条「2in1 快捷键」:单一出处(三页都不得自己比 KEYCODE_ENTER)、
登录页回车、详情页 Enter/Esc + 先关弹层、窗口装饰必须隐掉。
★ 两条第一版是**我自己判红了自己**,都是判据比事实严格:
① 详情页确实有 KEYCODE_ENTER —— 那是**输入框内的候选导航**(onFwdKey),
与页面级快捷键是两件事 ⇒ 改成只查页面级 onKeyEvent 那一段;
② isEscapeKey 先在 import 行出现,从那里切片取到的是注释 ⇒ 改成
从页面级 onKeyEvent 内部起切。
★ 窗口装饰那条第一版写 `/setWindowDecorVisible\(false\)/` —— 紧邻两行**日志**
也含这个串,删掉真正的调用后判据**照样绿**(变异实测没红)。
改成匹配调用形态 `win.setWindowDecorVisible(false)` 后才真会红。
这是"判据匹配到的是关于这件事的文字、不是这件事"的形状。
- harmony-widescreen ⑥ 原来钉精确串
`pushPath({ name: MAIL_DETAIL_ROUTE, param: params })`,加了 launchMode
参数后判红 —— 那是**判据写死了写法**。改成按结构匹配(不变式:选中邮件
要经 navPathStack.pushPath 进 MAIL_DETAIL_ROUTE,带不带 options 是实现细节)。
- harmony-2in1 登记数 12 → 16。
环境(这次为了真验 2in1 专门搭的)
- 下载 2in1 镜像 HarmonyOS 6.1.0(23)(与 target 一致),建实例 HA2in1
(3120×2080,14.2" 笔记本),设备 127.0.0.1:5557,形态确认为 `2in1`。
- 带窗口启动要 Qt xcb:补了 5 个 xcb 库 + Xvfb :99(`-noWindow` 下 2in1 起不了 App)。
- ★ 多设备并存时设备判据会自己挑目标 ⇒ 必须 `AGENTMAIL_HARMONY_TARGET=127.0.0.1:5555`
才跑手机那台;不指定时判据连到未登录的 2in1 上会假红。
这正是 harmony-device.mjs 里 targetKey() 注释写明的已知行为。
★ 未验(要说清楚,不能算过)
- Enter/Esc/Ctrl+N 三个快捷键**仍未在设备上端到端验过**:2in1 模拟器 + Xvfb 下
键盘事件送不进 App(xdotool 的文本能进 TextInput 走输入法通道,但键事件不达;
hdc 的 uinput/uitest keyEvent 同样不生效)。日志显示 SubscribeKeyEvent
被调用 ⇒ 订阅注册成功,纯粹是键送达不了。属环境限制。
- 悬停同理(要有鼠标 hover 事件注入,xdotool mousemove 到窗口不一定转成
ArkUI 的 onHover)。
- 登录页回车:同一限制。
沉浸顶栏与三键避让是**截图硬证过**的(不依赖键盘)。
102 lines
4.8 KiB
TypeScript
102 lines
4.8 KiB
TypeScript
/*
|
||
* AgentMail 鸿蒙客户端 — 2in1(PC / 平板键盘态)键盘快捷键
|
||
*
|
||
* ★★ 2026-09-24 新增。用户:「还有快捷键,比如主页回车打开写信。
|
||
* 邮件详情页回车打开回复。esc返回上一级」。
|
||
*
|
||
* ## 为什么单独一个模块
|
||
*
|
||
* 这三条键各自要落在**不同的组件**上(主页在 `MainPage` 根、详情在
|
||
* `MailDetailView`),而它们的**判定规则是同一套**(哪些键、什么条件、
|
||
* 要不要吞掉事件)。规则写两遍必然分叉 —— 本仓反复出现的形状。
|
||
* 所以规则住在这里,两处只做"接线"。
|
||
*
|
||
* ## 为什么不用 `keyboardShortcut`(Ctrl+N 那条用了)
|
||
*
|
||
* `keyboardShortcut` 只接受**组合键**:
|
||
* · 字符键必须配修饰键(Ctrl / Shift / Alt);
|
||
* · 不配修饰键时只接受 `FunctionKey`。
|
||
* 而框架的 `FunctionKey`(`component/enums.d.ts:3444`)只有
|
||
* ESC / F1–F12 / TAB / DPAD_UP / DPAD_DOWN / DPAD_LEFT / DPAD_RIGHT
|
||
* —— **没有 Enter**。所以"单按回车"用它表达不出来。
|
||
*
|
||
* 官方给单键的路是 `onKeyEvent`(`common.d.ts:19510`),而它
|
||
* 「Triggered when a key operation is performed on the bound component
|
||
* **after it obtains focus**」—— 需要焦点。
|
||
*
|
||
* ## 焦点问题怎么解(这是本条能力的关键)
|
||
*
|
||
* 邮件列表里焦点会随点击乱跑,所以"绑在某个列表项上等它获焦"不可靠。
|
||
* 但官方同时给了**冒泡**语义(`common.d.ts:19560-19562`):
|
||
* 「If the callback returns **true**, the key event is marked as consumed
|
||
* and will not **bubble up** to parent components.」
|
||
* 反过来说:**回调返回 false(或不处理)时,事件会冒泡到父组件**。
|
||
*
|
||
* ⇒ 于是有了一条可靠的路:
|
||
* · 绑在**页面根容器**上(它不是 `Text`/`Image` 那种默认不可聚焦的节点,
|
||
* 而是布局容器 —— 见 `isConsumableKey` 的注释);
|
||
* · 子组件里真正的输入框(收件人输入、回复框)先拿到键,
|
||
* 它们自己消费掉(`ComposePage.onToKey` 就是这么做的);
|
||
* · 没被子组件消费的键**冒泡到页面根**,由这里处理。
|
||
*
|
||
* 这样"详情页优先、主页兜底"是**框架保证**的,不是我自己排的优先级。
|
||
*
|
||
* ## 与 WebUI 的关系(这条不是对齐项)
|
||
*
|
||
* WebUI **没有**这三条快捷键(`grep` 过全部 `onKeyDown`:只有
|
||
* `AccountSwitcher.tsx:40` 的 Esc 关下拉、`AddressInput.tsx` 的候选导航)。
|
||
* 所以它们是**鸿蒙侧新增的 2in1 能力**,不是"WebUI 有而鸿蒙没有"。
|
||
* 这一点要写明,免得后面审计时被当成缺失项去"补"。
|
||
*/
|
||
|
||
import { KeyCode } from '@kit.InputKit';
|
||
|
||
/**
|
||
* 一个键事件该不该由本模块处理。
|
||
*
|
||
* ★ 只认 `KeyType.Down`:`onKeyEvent` 对**按下与抬起**各触发一次,
|
||
* 两条都处理会让一次按键走两步(`ComposePage.onToKey` 头一行就是这道门,
|
||
* 本仓 2in1 判据也钉过)。
|
||
*/
|
||
export function isKeyDown(type: KeyType): boolean {
|
||
return type === KeyType.Down;
|
||
}
|
||
|
||
/** 回车:打开发信 / 打开回复 —— 两条路都要它 */
|
||
export function isEnterKey(keyCode: number): boolean {
|
||
return keyCode === KeyCode.KEYCODE_ENTER
|
||
|| keyCode === KeyCode.KEYCODE_NUMPAD_ENTER
|
||
|| keyCode === KeyCode.KEYCODE_DPAD_CENTER;
|
||
}
|
||
|
||
/** Esc:返回上一级 */
|
||
export function isEscapeKey(keyCode: number): boolean {
|
||
return keyCode === KeyCode.KEYCODE_ESCAPE;
|
||
}
|
||
|
||
/**
|
||
* 这次按键是不是"文本框该自己管的"。
|
||
*
|
||
* ★ 为什么需要这道门(真实后果,不是理论):
|
||
* 回复框、收件人输入框里按回车是**换行 / 提交**,不该被页面级
|
||
* "回车 = 打开回复"抢走。抢走的后果是:用户想换行,结果又开了一层回复框,
|
||
* 而且 Esc 想取消输入却返回到列表 —— 输入到一半的内容全没了。
|
||
*
|
||
* 判定用键本身而不是"焦点在哪":焦点位置要读 `FocusController`,
|
||
* 而它与"这个键是否已被消费"是两件事 —— 官方给的冒泡机制已经表达了
|
||
* "子组件消费掉了就别往上冒",这里只再兜一层**文本输入语义**的键。
|
||
*/
|
||
export function isTextEditingKey(keyCode: number): boolean {
|
||
/*
|
||
* 只列真正属于"文字输入"的键。
|
||
* ★ **不含 Enter** —— 故意的:
|
||
* 本页没有"回车换行"的多行输入(回复框是单行 `TextInput`,
|
||
* 见 `MailDetailPage` 的回复条),回车在这里的语义就是"提交/打开"。
|
||
* 若哪天回复框改成多行 `TextArea`,**必须**把 Enter 加进来,
|
||
* 否则用户换行会被抢去开新回复框。
|
||
* (这一条写在代码里而不是文档里 —— 改的人一定会看到这里。)
|
||
*/
|
||
return keyCode === KeyCode.KEYCODE_SPACE
|
||
|| keyCode === KeyCode.KEYCODE_TAB;
|
||
}
|