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

@ -522,6 +522,47 @@ export struct ComposeView {
return account !== null ? account.displayName : '选择账号';
}
/**
* 账号选择菜单(`bindMenu` 的内容)—— 原来内联在 `build()` 里的那块列表。
*
* ★★ 2026-09-21 新增:从内联 `if/ForEach` 改为官方菜单内容。
*
* ★ 逐项不再挂 `Theme.menuIn()`:那个过渡原本是为了掩盖"列表推进布局流
* 导致下方内容被顶下去"。现在菜单是系统浮层(不占布局),
* 出入场由系统菜单自己负责 —— 再挂一层会与系统动画叠在一起。
*
* ★ 保留逐项的高亮与勾选(选中态的语言不变);
* 只是把"怎么弹出来"交给了系统。
*/
@Builder
AccountMenu() {
Column() {
ForEach(this.accountList, (acct: AccountInfo) => {
Row() {
Text(acct.displayName)
.fontSize(13)
.fontColor(this.selectedAccountId === acct.id ? Theme.accentFor() : Theme.textPrimary)
.fontWeight(this.selectedAccountId === acct.id ? FontWeight.Bold : FontWeight.Normal)
.layoutWeight(1)
if (this.selectedAccountId === acct.id) {
AmIcon({ iconName: 'check', iconSize: 14, iconColor: Theme.accentFor() })
}
}
.width(220).height(40).padding({ left: 12, right: 12 })
.backgroundColor(this.selectedAccountId === acct.id
? Theme.accentSoftFor(this.isDarkNow) : Color.Transparent)
.attributeModifier(PressEffectModifier.of())
.onClick(() => {
this.selectedAccountId = acct.id;
this.showAccountPicker = false;
})
}, (acct: AccountInfo) => acct.id)
}
.width(220)
.padding({ top: 4, bottom: 4 })
.alignItems(HorizontalAlign.Start)
}
build() {
Column() {
// 顶栏
@ -591,46 +632,25 @@ export struct ComposeView {
.rotate({ angle: this.showAccountPicker ? 90 : 0 })
/* 展开/收起箭头平滑旋转(WebUI `transition-transform` 的对应物) */
.animation({ duration: Motion.dur(Theme.durFast), curve: Theme.easeOutSoft })
.onClick(() => { this.showAccountPicker = !this.showAccountPicker; })
}
.width('100%').height(40).padding({ left: 12, right: 12 })
.backgroundColor(Theme.surface)
// 账号选择下拉
if (this.showAccountPicker) {
ForEach(this.accountList, (acct: AccountInfo) => {
Row() {
Text(acct.displayName)
.fontSize(13)
.fontColor(this.selectedAccountId === acct.id ? Theme.accentFor() : Theme.textPrimary)
.fontWeight(this.selectedAccountId === acct.id ? FontWeight.Bold : FontWeight.Normal)
.layoutWeight(1)
if (this.selectedAccountId === acct.id) {
AmIcon({ iconName: 'check', iconSize: 14, iconColor: Theme.accentFor() })
}
}
.width('100%').height(40).padding({ left: 40, right: 12 })
.backgroundColor(this.selectedAccountId === acct.id ? Theme.accentSoftFor(this.isDarkNow) : Theme.surface)
.attributeModifier(PressEffectModifier.of())
.onClick(() => {
this.selectedAccountId = acct.id;
this.showAccountPicker = false;
})
/*
* 弹层入场(对齐 WebUI `@keyframes menu-in`:下移 4vp + 缩到 0.985 + 淡入)。
*
* ★ 只给**弹层**挂 —— WebUI 那条 `.animate-menu-in` 的注释把适用范围
* 钉得很窄("只给真正是弹层的东西"),因为它曾经挂着 `glass-control`,
* 于是每次切视图页面上**所有**按钮与输入框一起淡入位移(几十个元素同时动),
* 2026-09-15 被摘掉。别把这个放到常驻控件上。
*
* 挂在 `Row` 上(即 `ForEach` 的每一项):整张候选列表逐项轻落,
* 与 WebUI 里"整块列表一起动"略有差别 —— 但 ArkUI 的 `ForEach` 每项
* 是独立节点,逐项入场反而更接近"列表展开"的观感,且不会让整层不可交互。
*/
.transition(Theme.menuIn())
}, (acct: AccountInfo) => acct.id)
}
/*
* ★★ 2026-09-21 改用 `bindMenu(isShow, content)`(**API 11** 重载)。
*
* 第一版写的是 `bindMenu(... ? this.AccountMenu() : undefined, {onDisappear})`
* —— 那是 API 7 那个靠"内容为 undefined 就不弹"的重载。
* 改成 `isShow` 版后:
* · 显隐是**显式参数**(不再靠内容是不是 undefined 隐式表达);
* · `$$` 两向绑定让"用户点外部关闭"自动回写状态(不再靠 onDisappear 补)。
*
* ★ 与 `bindSheet($$this.showAddDialog, …)` 同一套写法 ——
* 两处弹层用一种形状,而不是两种。
*
* ★ 仍然与 `MainPage` 的账号筛选器一致(同一交互,不该两种形状)。
*/
.bindMenu($$this.showAccountPicker, this.AccountMenu())
.onClick(() => { this.showAccountPicker = !this.showAccountPicker; })
Divider().color(Theme.border)
}
@ -757,6 +777,31 @@ export struct ComposeView {
.width('100%')
.backgroundColor(Theme.surface)
.borderRadius(Theme.glassRadius)
/*
* ★★ 2026-09-21 补:**候选列表的入场过渡**(原来一个都没有 ⇒ 硬弹)。
*
* 这是 `Theme.menuIn()` 的注释里点名要覆盖的那一类("账号选择器、**下拉候选**"),
* 但它从来没有真正挂到候选列表上 —— 只有账号选择器用了。
*
* ── 为什么现在才暴露 ──
* 三个原本挂 `menuIn()` 的地方里,两个账号选择器今天换成了官方
* `bindMenu`(系统自带转场),`menuIn()` 于是只剩定义、没有调用点。
* 我去查"要不要删掉这个死方法"时,顺手拿它的**注释**对了一遍实际用法:
* 它写的是给"账号选择器、下拉候选",**候选那一半从来没兑现**。
*
* ── 为什么必须补(不是纯观感)──
* WebUI 的同一处在 `AddressInput.tsx:261` 挂着 `animate-menu-in`
* (`.animate-menu-in` 就是 `@keyframes menu-in`,与 `menuIn()` 逐值对齐)。
* 也就是说:**WebUI 的候选是动的、鸿蒙的是硬弹** —— 这是真实的跨端不一致,
* 不是"我们少做了个美化"。
*
* ★ 挂在这一整层 `Column` 上(整块列表一起动),与 WebUI 的
* `<div class="animate-menu-in">` 包整块一致。
* (历史上一度逐项挂 `menuIn()` 的是**账号选择器**,那时是为了掩盖
* "下拉推进布局流把列表顶下去";现在的候选是浮在内容之上的,
* 整块入场才是对的,别再逐项。)
*/
.transition(Theme.menuIn())
}
Divider().color(Theme.border)