跨端对齐:授权栏 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:
@ -621,8 +621,26 @@ test('★ 设备:管理页能从「我的」页打开,且列表真的渲染
|
||||
if (a.clickable !== 'true') return false;
|
||||
const m = /\[(-?\d+),(-?\d+)\]\[(-?\d+),(-?\d+)\]/.exec(a.bounds || '');
|
||||
if (!m) return false;
|
||||
const [, , y1, x2, y2] = m.map(Number);
|
||||
return x2 <= scrW * 0.08 && y1 > scrH * 0.5 && (y2 - y1) > 80;
|
||||
const [, x1, y1, x2, y2] = m.map(Number);
|
||||
/*
|
||||
* ★★ 2026-09-21 修:加宽高比约束(与 `harmony-appearance` 两处同一个隐患)。
|
||||
*
|
||||
* 侧栏下半部有**三个**可点方块,实测(3184×2232,density 2.875):
|
||||
* 头像 40×40vp(115×115px)ratio 1.000
|
||||
* 主题切换 36×28vp(104× 81px)ratio 1.284
|
||||
* 退出登录 36×28vp(104× 81px)ratio 1.284
|
||||
* `(y2-y1) > 80` **三者全中**,`.find()` 靠遍历顺序才取到头像。
|
||||
*
|
||||
* 一旦顺序变了就会点到**退出登录** ⇒ 设备被登出、账号被清空
|
||||
* (`performLogout` → `clearAll()`,实测 `accounts_json` 变 `[]`)。
|
||||
* 我这次调试期间设备真的被登出过一次。
|
||||
*
|
||||
* ⇒ 用宽高比把"正方形头像"与"扁矩形按钮"分开。阈值 1.15 落在
|
||||
* 1.000 与 1.284 之间,两边都有余量。
|
||||
*/
|
||||
const w = x2 - x1;
|
||||
const h = y2 - y1;
|
||||
return x2 <= scrW * 0.08 && y1 > scrH * 0.5 && h > 80 && w / h <= 1.15;
|
||||
});
|
||||
if (!avatar) return t.skip('宽屏侧栏头像找不到 —— 无法进「我的」');
|
||||
const c = D.boundsCenter(avatar.attributes.bounds);
|
||||
@ -747,6 +765,87 @@ test('★ 设备:管理页能从「我的」页打开,且列表真的渲染
|
||||
assert.ok(userRows.length > 0,
|
||||
'★ 管理页要真的渲染出**用户行**(带角色/状态的那种)—— ' +
|
||||
`只有标题而没有行,说明数据没渲染。实际读到:${texts.slice(0, 25).join(' | ')}`);
|
||||
|
||||
/*
|
||||
* ★★ 2026-09-21 补:**把应用放回主界面再退出**。
|
||||
*
|
||||
* ── 真 bug(连着两轮套件都因此红)──
|
||||
*
|
||||
* 本条爬到「管理」子页(`AdminUsersPage`,push 在 `MainPage` 之上的路由)
|
||||
* 之后**就地结束**,应用就停在那里。下一个跑的设备判据
|
||||
* (`harmony-nav` 的「底栏真渲染了可点的导航项」)直接 `dumpLayout` 量现场
|
||||
* ⇒ 量到的是**管理页**,那一页上根本没有底栏/侧栏 ⇒ 报
|
||||
* 「宽屏左侧栏要渲染出 ≥1 个可点的导航项(实际 0)」
|
||||
* 侧栏明明好好的。**那不是功能缺失,是我的判据留下的现场。**
|
||||
*
|
||||
* `backToMain` 的注释里早写着同一个根因("宽屏侧栏找不到 ⇒ 双双 skip"),
|
||||
* 只是本条的属主当时没顺手收尾。
|
||||
*
|
||||
* ★ 判据有义务**清理自己造成的状态**,不能指望"下一条会自己归位" ——
|
||||
* 那等于把顺序耦合藏进设备状态里(谁先跑谁后跑决定了绿红)。
|
||||
* `harmony-nav` 那边我也补了自保(它现在会先 `backToMain`),
|
||||
* 但两边都做才是对的:一条产生脏状态、一条消化脏状态,
|
||||
* 任何一边缺失都只是"碰巧还能过"。
|
||||
*/
|
||||
await D.launchOurApp(hdc, { settle: 400, tries: 20 }).catch(() => {});
|
||||
await D.backToMain(hdc).catch(() => {});
|
||||
});
|
||||
|
||||
test('★ 两个表单型半模态必须**同形**(同类交互不能一个能叉、一个不能)', () => {
|
||||
/*
|
||||
* ★★ 2026-09-21 加:用户「修改密码也应该做成弹窗吧」之后,
|
||||
* `SettingsPage` 里出现了**两个同类**的表单型半模态:
|
||||
* · 新增账号(`AddAccountSheet`)
|
||||
* · 修改密码(`PasswordSheet`)
|
||||
*
|
||||
* 第一版我给新增账号写了 `showClose: false`(理由是"内容里已经有「取消」按钮"),
|
||||
* 给修改密码写了 `true` —— 于是两个**同类**一个能叉掉、一个不能。
|
||||
* 用户看到的是"为什么这个能关、那个不能"。
|
||||
*
|
||||
* ── 为什么这条值得单独钉 ──
|
||||
*
|
||||
* 这是用户这一路反复指出的那个形状:**同类交互必须是同一种形状**。
|
||||
* (「修改密码也应该做成弹窗吧」的 `也应该` 本身就是这个意思 ——
|
||||
* 不是"这个要弹窗",而是"两个表单别一个内联一个弹窗"。)
|
||||
*
|
||||
* 靠人记得"改一个要顺手改另一个"是不成立的 —— 这条判据把它变成**结构约束**:
|
||||
* 两份 sheet 的选项必须逐字相同。以后谁只改其中一个,这里立刻红。
|
||||
*
|
||||
* ★ 为什么用「逐字对比两份选项块」而不是逐项 assert:
|
||||
* 逐项 assert 只能钉我**今天想到的那几项**(height / showClose),
|
||||
* 而"同形"的含义是**整块相同** —— 明天加个 `dragBar: false`
|
||||
* 只加在一个上,逐项 assert 照样绿。对比整块才真的守住"同形"。
|
||||
*/
|
||||
const src = code(SETTINGS_PAGE);
|
||||
const grabOpts = (marker) => {
|
||||
const at = src.indexOf(marker);
|
||||
if (at < 0) return null;
|
||||
/* 取 `.bindSheet(...)` 的**最后一个参数**(选项对象):从 marker 起配对 `{}` */
|
||||
const open = src.indexOf('{', src.indexOf(',', at + marker.length));
|
||||
if (open < 0) return null;
|
||||
let depth = 0;
|
||||
for (let i = open; i < src.length; i++) {
|
||||
if (src[i] === '{') depth++;
|
||||
else if (src[i] === '}') { depth--; if (depth === 0) return src.slice(open, i + 1); }
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const add = grabOpts('.bindSheet($$this.showAddDialog');
|
||||
const pw = grabOpts('.bindSheet($$this.showPasswordSheet');
|
||||
assert.ok(add, '要能找到「新增账号」半模态(`.bindSheet($$this.showAddDialog`)');
|
||||
assert.ok(pw, '要能找到「修改密码」半模态(`.bindSheet($$this.showPasswordSheet`)');
|
||||
|
||||
/* 选项里的值各自不同(回调名),所以只对比**键**与**非回调字面量** */
|
||||
const normalize = (o) => o
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '') // 去注释(两份注释必然不同)
|
||||
.replace(/onDisappear\s*:\s*\(\)\s*=>\s*\{[^}]*\}/g, 'onDisappear:<cb>')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
assert.equal(normalize(pw), normalize(add),
|
||||
'★ 两个表单型半模态的选项必须**整块相同** —— 同类交互同一种形状。\n' +
|
||||
` 新增账号:${normalize(add)}\n` +
|
||||
` 修改密码:${normalize(pw)}\n` +
|
||||
' 只改其中一个会让两个同类面板长得不一样(用户看到的"割裂"就是这么来的)。');
|
||||
});
|
||||
|
||||
test('★ 详情页的两个动作球必须**分开摆**(Stack 的同一个角 + 各自 margin = 重叠)', () => {
|
||||
@ -882,17 +981,22 @@ test('★ 底部弹层必须有明确高度(否则键盘一弹,按钮全被
|
||||
'★ 不要显式设成 `OFFSET`(那就是"整页上移",正是要避免的那个模式)');
|
||||
|
||||
/*
|
||||
* 逐个**仍然存在的**覆盖式弹层:从 `borderRadius({ topLeft` 往回找高度声明。
|
||||
* 逐个**仍然自绘的**覆盖式弹层:从 `borderRadius({ topLeft` 往回找高度声明。
|
||||
*
|
||||
* ★ 2026-09-20:回复/转发已改成内联底栏(不再是弹层),这道检查现在只剩
|
||||
* **对话树弹层**一个对象。它仍然该有明确高度 —— 理由没变:
|
||||
* 尺寸由内容决定的覆盖层,键盘弹起时内容会被挤出可视区。
|
||||
* (内联底栏那半改用上面的 `KeyboardAvoidMode.RESIZE` 承担。)
|
||||
* ★ 2026-09-20:回复/转发已改成内联底栏(不再是弹层)。
|
||||
* ★★ 2026-09-21:**对话树弹层已换成官方 `bindSheet`**(用户:「现在最割裂的
|
||||
* 就是弹出效果」)⇒ 自绘的覆盖式弹层现在是 **0 个**。
|
||||
*
|
||||
* 「明确高度」这条要求**依然成立、且被满足得更好**:
|
||||
* · 旧自绘版:`height('60%')` —— 拍脑袋的百分比,内容多时会被挤出可视区;
|
||||
* · 官方 `bindSheet`:高度档位由系统定义(`SheetSize.LARGE`),
|
||||
* 并有 `keyboardAvoidMode` 专门做键盘避让。
|
||||
*
|
||||
* ⇒ 所以这里不再要求"至少 1 个自绘弹层"(那会把回退到自绘当成优点),
|
||||
* 改为:**若还有自绘弹层,它必须有明确高度**;同时断言换过去的那一个
|
||||
* 确实还在官方容器里(否则就是"既没自绘也没官方"= 硬弹且无避让)。
|
||||
*/
|
||||
const sheets = [...src.matchAll(/\.borderRadius\(\{\s*topLeft:\s*\d+/g)];
|
||||
assert.ok(sheets.length >= 1,
|
||||
`详情页应还有覆盖式弹层(对话树)—— 实际找到 ${sheets.length} 个。\n` +
|
||||
' 若回复/转发已全变内联底栏,至少对话树那一个还在;一个都没有说明结构被改坏了。');
|
||||
const noHeight = [];
|
||||
for (const m of sheets) {
|
||||
const before = src.slice(Math.max(0, m.index - 1200), m.index);
|
||||
@ -906,6 +1010,13 @@ test('★ 底部弹层必须有明确高度(否则键盘一弹,按钮全被
|
||||
'内容一多就会被挤出可视区(旧版回复/转发弹层实测过:按钮点不到)。\n' +
|
||||
`缺高度的弹层:\n ${noHeight.join('\n ')}`);
|
||||
|
||||
/* 换到官方容器的那一个必须真的在(不能"两边都不在") */
|
||||
assert.match(src, /\.bindSheet\(\$\$this\.showThread/,
|
||||
'★ 详情页的**覆盖式弹层**(对话树)必须二选一:\n' +
|
||||
' · 官方 `bindSheet`(推荐:自带拖拽条/下滑关闭/遮罩/键盘避让),或\n' +
|
||||
' · 自绘 `borderRadius({topLeft})` + 明确高度 + `transition`。\n' +
|
||||
' 两者都没有 = 旧弹层被删了却什么都没换上去。');
|
||||
|
||||
/*
|
||||
* ② 底栏内部各元素用**内容尺寸/百分比**,不得再用 `layoutWeight(1)` 抢空间。
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user