跨端对齐:授权栏 navigator_only + 组件按页拆分 + 服务器补 permission_options

用户两项裁定落地(均为 ask_user 明确选择):

① 授权栏口径 = navigator_only(照 WebUI 架构)
   · 新建 pages/PermissionPanel.ets —— 详情页的决策面板,
     对应 MailView.tsx:693 的 PermissionPanel(审批型 / 主动提问 / 已处理 三态)
   · 决策入口从授权栏移到 MailDetailPage;MailDetailPage 原来只显示一个
     「权限请求」小标签、根本没有决策入口(比 WebUI 少一整块,且反了:
     栏里能决策、点进详情反而不能)
   · PermissionTab 删掉内联「同意/拒绝」+ 备注框 + decide():
     整卡可点 → onOpenMail(对齐 WebUI PermissionList.tsx:81 的 pick())
   · PermissionRequest 补 source_account_id(客户端侧记来源,跳详情要定位网关)

② 服务器补 permission_options —— 修一条真实的、跨端共有的缺口
   · mails.permission_options 从 INSERT 起就写进去,但**从来没有任何读路径
     选过它** ⇒ 详情端点永远返回空。WebUI 的决策面板读 mail.permission_options,
     所以提问型的预设选项**两端全部落空**(审批型靠 ['同意','拒绝'] 兜底蒙混)
   · GetMailByID 补选该列 + JSON 反序列化(与 cc_list 同款)

③ 组件按页封装(用户要求「以便与 WebUI 一一对应」)
   MainPage.ets 4592 → 3192 行
   · pages/PermissionTab.ets    720 行  ↔ PermissionList.tsx
   · pages/ContactsTab.ets      796 行  ↔ ContactPanel.tsx
   · pages/NavDestinations.ets  181 行  ↔ Navigation 壳
   · pages/NavShared.ets         65 行  ↔ 跨栏共用件

④ 判据跟着组件搬家(否则静默失效,不是红)
   harmony-logic 的 pageCode / harmony-nav 的 navSrc 改为显式文件名单;
   harmony-appearance 的 PANE_SOURCES 补 ContactsTab;harmony-contacts 三个
   test 并入 ContactsTab;harmony-logic 的决策断言改指 PermissionPanel,
   并新增「授权栏不许再有内联决策」两条(navigator_only 的正形状)。

   animation-audit:共享元素转场判据从「同文件共址」改为「按 id 找驱动」。
   旧形状把 in/out 端必须在同一文件当成代理,而两端**天然在两处**;
   抽出写信页(NavDestinations 持有 in 端)后误报。新判据仍要求每个 id
   都有 Motion.morph 驱动 —— 变异实测:把驱动换成裸 animateTo 仍判红。

判据:files=34 checks=556 red=1(仅 build-stamp,产物待重构建)
This commit is contained in:
2026-09-24 10:10:32 +08:00
parent 487c1c222b
commit 65de1c3884
31 changed files with 4147 additions and 1684 deletions

View File

@ -261,17 +261,46 @@ test('2in1 快捷键|只响应 KeyType.Down(Down/Up 都处理会让一次按
assert.match(body, /e\.type\s*!==\s*KeyType\.Down/, '要先挡掉非 Down 的按键事件');
});
test('2in1 快捷键|候选列表内联渲染,不用 bindPopup/bindMenu', () => {
test('2in1 快捷键|地址候选列表内联渲染,不用 bindPopup/bindMenu', () => {
/*
* 这两者各有自己的焦点体系 —— 用户的按键会先被它们吃掉,
* "↑↓ 切换候选"就落不到 `onToKey` 上(菜单收不到、输入框也收不到)。
*
* 内联渲染(条件挂载)能让焦点一直留在输入框里 —— 这是键盘可达的前提。
*
* 改坏会红:把候选改成 `bindPopup(...)`。
* ★★ 2026-09-21 改:原来这两条断言扫的是**整个文件**
* (`!/\.bindMenu\(/.test(COMPOSE)`)—— 那把**不属于候选列表**的菜单
* 也一并禁了。我在账号选择器上用了官方 `bindMenu`(那是正确的:
* 它是个"点开选一个"的菜单,没有 ↑↓ 候选导航),结果被判据误报。
*
* 两者性质完全不同:
* · 地址候选(`suggestOpen`):**要 ↑↓/Enter 导航** ⇒ 必须内联,
* 这是无障碍与键盘可达的硬要求;
* · 账号选择器(`showAccountPicker`):**没有键盘导航**,就是个下拉
* ⇒ 官方 `bindMenu` 正合适(还自带点外部关闭/边缘避让)。
*
* ⇒ 断言改为**只看候选那一段**(从 `suggestOpen` 条件挂载处取到下一段结束)。
* 这样它仍然拦得住"把候选改成 bindMenu"(真正要防的那件事)。
*/
assert.ok(!/\.bindPopup\(/.test(COMPOSE), '不得用 bindPopup(会吃掉按键)');
assert.ok(!/\.bindMenu\(/.test(COMPOSE), '不得用 bindMenu(会吃掉按键)');
const suggestAt = COMPOSE.indexOf('if (this.suggestOpen && this.suggestItems.length > 0)');
assert.ok(suggestAt > 0, '要能找到地址候选的挂载点(它必须内联渲染)');
/*
* ★ 窗口要**从候选的开头往前多取一段**,不能从 `if` 那个位置开始。
*
* 实测(变异验证时发现的):我第一版从 `suggestAt` 开始切,
* 而"把候选改成 `bindMenu`"最自然的写法是把 `.bindMenu(...)` 写在
* **那个 `if` 之前**(作为一个链式修饰符)—— 于是它落在窗口之外,
* 变异**测不出来**(实测确实绿)。
*
* 判据自己对变异不敏感 = 它没在守卫那件事。
* ⇒ 往前取 600 字符(够包住同一节点上的修饰符链),往后到该块结束。
*/
const from = Math.max(0, suggestAt - 600);
const tail = COMPOSE.slice(suggestAt);
const cut = tail.indexOf('Divider()');
const suggestBlock = COMPOSE.slice(from, suggestAt + (cut > 0 ? cut : 3000));
assert.ok(!/\.bindPopup\(/.test(suggestBlock), '地址候选不得用 bindPopup(会吃掉按键)');
assert.ok(!/\.bindMenu\(/.test(suggestBlock), '地址候选不得用 bindMenu(会吃掉按键)');
assert.match(COMPOSE, /if \(this\.suggestOpen && this\.suggestItems\.length > 0\)/, '候选要在布局里内联挂载');
});