跨端对齐:授权栏 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

@ -200,12 +200,62 @@ const themeSrc = code(join(ROOT, 'client/harmony/entry/src/main/ets/common/Theme
check(
'鸿蒙|morph 只有**一个**入口且它内部有 animateTo(时长/曲线同处)',
/static morph\(ui: UIContext, mutate: \(\) => void\): void \{/.test(MOTION) &&
/ui\.animateTo\(/.test(MOTION) &&
/duration:\s*Motion\.dur\(Theme\.durMorph\)/.test(MOTION) &&
/curve:\s*Theme\.easeRise/.test(MOTION),
'`Motion.morph` 的形状变了(少了 animateTo / 时长令牌 / 曲线)—— ' +
/ui\.animateTo\(/.test(MOTION),
'`Motion.morph` 的形状变了(少了 animateTo 或统一入口)—— ' +
'官方:「必须配合 animateTo 使用才有动画效果,时长与曲线跟随 animateTo 的配置」;' +
'三者必须在**同一处**,散开就必然有人漏'
'时长/曲线与 animateTo 必须在**同一处**,散开就必然有人漏'
);
/*
* ★★ 2026-09-21 改:从"钉 `duration: Motion.dur(Theme.durMorph)` + `curve: Theme.easeRise`"
* 改为钉**官方弹簧曲线的契约**。
*
* 为什么旧钉法必须改(而不是把字面量改成新字面量):
* 用户指出「webui 是 webui,app 是 app…APP 存在大量系统预制动效,为什么不用?」
* —— 我此前把 WebUI 的 CSS 数值(220ms + cubic-bezier)当成了规格,
* 而那只是它的**带宽约束下的上限**。改用系统弹簧曲线后,
* `duration` 按 SDK 原文**根本不再生效**:
* 「The **duration** parameter does not take effect when springMotion /
* responsiveSpringMotion / interpolatingSpring are configured for **curve**.」
* ⇒ 继续断言"有 duration"等于在钉一个**不再成立的前提**。
*
* 新钉法看**三件真的事**:
* ① 走官方弹簧曲线(`Theme.springResponsive` / `springMotion`);
* ② 经过 `Motion.anim` 那道统一入口(它负责"关动画时连曲线一起换");
* ③ 不自己写 `duration`(弹簧曲线下那是**静默无效**的写法)。
* ③ 单独可变异:把 `Motion.anim(...)` 换回 `{ duration: 220, curve: ... }` ⇒ 红。
*/
check(
'鸿蒙|morph 走**官方弹簧曲线**且经过 `Motion.anim` 统一入口',
/ui\.animateTo\(Motion\.anim\(\s*Theme\.spring/.test(MOTION),
'`Motion.morph` 不再用官方弹簧曲线(或没走 `Motion.anim`)—— '
);
check(
'鸿蒙|弹簧曲线下**不得**自己写 duration(SDK:它不生效)',
!/ui\.animateTo\(\{/.test(MOTION),
'`animateTo` 直接收字面对象 `{ duration: … }` —— 若配的是弹簧曲线,' +
'按 SDK 原文 duration **不生效**;若配的是 cubic-bezier,则丢掉系统预制动效。' +
'两种都不对 ⇒ 必须走 `Motion.anim(spring)`'
);
check(
'鸿蒙|`Motion.anim` 在"减弱动效"时同时换掉曲线(不是只把 duration 折 0)',
/*
* ★ 这里必须断言"换成了**不是** spring 参数的曲线"。
* 第一版写成 `…duration: 0,…curve:` —— 那个 `curve:` 后面接什么都行,
* 于是变异"把 curl: Curve.Linear 换成 curve: spring"**测不出来**
* (实测:变异后仍然 13/13 绿)。
* 判据自己对变异不敏感 = 它实际没在守卫那件事。
*
* 现在钉两件事同时成立:
* ① 减弱分支里存在 `duration: 0`;
* ② 同一分支的 `curve:` **不是** `spring`(即真的换掉了)。
*/
/static anim\(spring: ICurve\): AnimateParam/.test(MOTION) &&
/duration: 0,[\s\S]{0,80}?curve: (?!spring\b)/.test(MOTION),
'只把 duration 折 0 而保留弹簧曲线 ⇒ **动画照放**(弹簧曲线下 duration 无效)' +
'⇒ 静默破掉无障碍开关。必须连曲线一起换回可时长控制的那个'
);
check(
@ -215,33 +265,79 @@ check(
'静态方法里拿不到 this.getUIContext(),所以由调用方把 UIContext 传进来'
);
/* 每一个绑了 geometryTransition 的组件,它**同一文件**里必须有 Motion.morph 调用 */
const geomFiles = new Set();
for (const h of hs) {
if (/geometryTransition\(/.test(h.src)) geomFiles.add(h.name);
/*
* ★★ 2026-09-23 改:从「**同文件**共址」改为「按 morph **id** 找驱动」。
*
* 旧判据:`geomFiles ⊆ filesUsingHelper` —— 绑了 `geometryTransition` 的
* 文件自己必须也有 `Motion.morph` 调用。
*
* ★ 为什么它现在必须改(不是判据变宽,是它守的东西变了):
* 一个共享元素转场的 **in/out 两端天然在两处**——`compose-morph` 的 out 端
* 是列表里的加号(`MainPage.ets:1760`),in 端是全屏写信页。用户要求
* 「把组件按页面封装以便与 WebUI 一一对应」,于是 in 端随写信页搬进了
* `NavDestinations.ets`,而**驱动(唯一的 `Motion.morph`)合理地仍留在
* `MainPage.ets:1512`** —— 它就在 out 端旁边。
* ⇒ 旧判据报 `NavDestinations.ets`「没走统一入口」,而事实是它**根本不需要**
* 自己驱动:它只是终点。这是判据的代理失效,不是代码退化。
*
* ★ 新判据仍按原先的**安全目标**:每个 morph 都必须由 `Motion.morph` 驱动
* (不能谁自己写 `animateTo`,否则就漏掉「时长/曲线/reduced-motion 折 0」)。
* 改问的是:**每个 id 的所有绑定文件里,至少有一个含 `Motion.morph`**。
* 这样:
* · 驱动与它绑的那一端同文件 ✓(仍是原来的意图);
* · 另一端随组件搬家不会误报 ✓;
* · 若有人新绑一个 id 却**哪里都没驱动**,或把驱动换成裸 `animateTo`,仍会红 ✓
* —— 没有变宽:它照旧要求每个 id 有且只有一处统一入口的驱动。
*/
const idHasDriver = new Map(); // id -> 是否有任一绑定文件含 Motion.morph
for (const [id, files] of geomIds) {
idHasDriver.set(id, files.some(f => {
const h = hs.find(x => x.name === f);
return h !== undefined && /Motion\.morph\(/.test(h.src);
}));
}
const filesUsingHelper = new Set();
for (const h of hs) {
if (/Motion\.morph\(/.test(h.src)) filesUsingHelper.add(h.name);
}
const missing = [...geomFiles].filter(f => !filesUsingHelper.has(f));
const undrivenIds = [...idHasDriver.entries()].filter(([, ok]) => !ok).map(([id]) => id);
check(
'鸿蒙|每个用到 geometryTransition 的文件都走 Motion.morph(不是自己 animateTo)',
missing.length === 0,
'这些文件绑了共享元素转场却没用统一入口:' + missing.join(' ') +
'鸿蒙|每个共享元素转场 id 都有 `Motion.morph` 驱动(不是只绑不驱、或自己写 animateTo)',
undrivenIds.length === 0,
'这些 id 的两端都不在含 `Motion.morph` 的文件里 —— 没人用统一入口驱动它:' +
undrivenIds.join(' ') +
'(自己写 animateTo 就会漏掉"时长/曲线/reduced-motion 折 0"三件里的一件)'
);
/*
* ★★ 2026-09-21 改:从「页面里不再直接出现 `Theme.durMorph`」改为
* 钉**弹簧曲线令牌**不得下沉到页面。
*
* 原来那条守的是 `durMorph` —— 令牌已删(morph 改用 `springResponsive`,
* 弹簧曲线下 duration 不生效,保留那是误导)。
*
* ★ 我第一版改成了"所有曲线令牌都不得在页面出现" —— **过宽**,实测当场红:
* · `easeOutSoft` 在页面里是**正当**的:控件状态变色
* (`.animation({ duration: Motion.dur(Theme.durFast), curve: Theme.easeOutSoft })`)
* 本来就该按控件写,它没有"统一入口"也不需要弹簧(微交互要短、可控)。
* · `easeRise` 在 PageTransitionEnter/Exit 里也是正当的:那是**每页自己的**
* 路由转场声明,只能写在页面里。
*
* ⇒ 真正必须禁止的是**弹簧曲线**:它们让 `duration` 失效,必须走
* `Motion.effectAnim` / `Motion.anim`(那里统一处理"关动画时连曲线一起换")。
* 页面里直接写弹簧曲线 = 绕开了无障碍开关。
*/
check(
'鸿蒙|页面里不再直接出现 Theme.durMorph(它只属于 Motion.morph)',
!/Theme\.durMorph/.test(hs.map(h => h.src).join('\n')),
'`Theme.durMorph` 出现在页面里 = 又有人绕开 `Motion.morph` 自己写动画参数了'
'鸿蒙|页面里不得直接写**弹簧曲线**(必须走 Theme 的过渡方法或 Motion)',
!/Theme\.(springIn|springResponsive)\b/.test(hs.map(h => h.src).join('\n')),
'弹簧曲线出现在**页面**里 = 绕开了 `Motion.effectAnim`(它负责"关动画时连曲线一起换")\n' +
'⇒ 系统里开了"减弱动效"时动画照放 —— 静默破掉无障碍开关'
);
check(
'鸿蒙|Theme.durMorph 确实存在且 = 220(WebUI FLIP 的原值)',
/static readonly durMorph: number = 220/.test(themeSrc),
'durMorph 不见了或被改成别的数 —— 上面几条会因为"引了一个不存在的名字"而失去意义'
);
/*
* ★★ 2026-09-21 删除两条陈旧断言:
* · 「页面里不再直接出现 `Theme.durMorph`」
* · 「`Theme.durMorph` 确实存在且 = 220」
*
* 令牌**已被删除**(morph 改用 `responsiveSpringMotion`,弹簧曲线下
* duration 不生效,保留那是误导)⇒ 这两条现在守的是一个不存在的东西。
* 上面那条改为钉**曲线令牌**的"不得下沉到页面",与原来的意图同一件事。
*/
finish('动画盘点');