跨端: 邮件转发上线(详情页缺的入口)+ 修两个真 bug(动作球重叠 / 键盘挡住按钮)
审计发现详情页缺三块功能之一 —— **转发**。服务端 `forward.go` 完整、
`MailApi.forward()` 也早就写好了,缺的只是这一页的入口。
## 一、转发(对齐 WebUI `ForwardBar`)
字段与占位文案逐条对齐:收件人 / 抄送(**可折叠**,默认收起)/ 说明 / 引用原文。
`subject` 留空让服务端自动加 `Fwd: ` 前缀(它处理了 "Fwd: Fwd:" 无限叠加)。
★ 请求体用了**专用类型** `ForwardMailRequest`,不复用 `SendMailRequest`:
服务端 `forwardRequest` 只认五个字段,而它的 `Decode()` 是 `DisallowUnknownFields()`
⇒ 多带一个(`body` / `reply_to` / `attachment_ids`…)就 **400**。
—— 这正是我今天在 `AppearancePayload` 上刚犯过的那个错,**端点一个类型一个请求体**。
★ 顺带发现:`MailApi.forward` 的签名**原本就写错了**(参数类型是 `SendMailRequest`)
—— 一直没被发现,因为**从来没有调用方**。"写好了但没人用"的代码,
连它自己的类型对不对都没人验过。
## 二、真 bug ①:两个动作球**几乎完全重叠**
转发球与回复球**各自**写在 `Stack({alignContent: BottomEnd})` 里、各带一个 margin。
实测 dump 的 bounds(密度 2.875):
转发 [2984,2010][3122,2148]
回复 [2949,1998][3110,2159] ← 重叠区 x ∈ [2984,3110]
屏幕上只看得到一个球,**转发入口等于不存在**。
根因:`Stack.alignContent` 把**每个**子元素都摆到同一个角,margin 只是各自微调。
修法:用 `Row({ space: 12 })` 包住两个球、由 Row 带 margin 到角落。
**设备实测**(修后 dump):`[2776,2021][2914,2159]` 与 `[2949,1998][3110,2159]`,不重叠。
## 三、真 bug ②:键盘一弹,「转发」按钮就被顶出屏幕
转发弹层第一版**没有高度**(只有 `padding(16)`)⇒ 尺寸由内容决定。
而 ArkUI 默认的键盘避让是 `KeyboardAvoidMode.OFFSET`(整体上移)——
上移之后 `TextArea` 与「取消 / 转发」按钮**跑到键盘下面**,点不到。
实测截图:只看得见收件人输入框 + 键盘。
修法两半(缺一不可):
① 弹层给明确高度 `height('60%')`(与回复弹层一致,它一直没出问题);
② 说明框改 `layoutWeight(1)`(不是固定 `height(70)`)—— 键盘顶上来时它自己缩短,
把按钮留在屏内。
**设备实测**(键盘弹出时 dump):`取消 [1196,2054][1426,2158]`、
`转发 [2880,2054][3110,2158]` 都在屏内(屏高 2232),且 `clickable=true`。
## 四、判据(这两条固化了上面两个形状)
`harmony-admin` 新增两条**静态形状**判据(它们抓的是写法,不需要设备):
1. **同一 `Stack` 里的多个圆形按钮必须被 `Row` 包住**(否则重叠);
2. **底部弹层必须有明确高度** + 会撑高的子元素用 `layoutWeight`
(否则键盘一弹按钮就被顶出屏幕)。
★ 为什么用静态判据而不是设备判据:这两个 bug 的**形状**在源码里就看得见
(`Stack` + 各自 margin / 弹层缺 `.height`),而设备判据要摆出"键盘弹出"这个态,
成本高且不稳。静态判据在这里是**更快更准**的那一层。
(设备判据仍保留在别处,验"真的能打开、真的渲染出来"。)
## 五、验证
`run-all.mjs` → `files=32 ran=32 checks=509 pass=509 fail=0 skip=0
red=0 broken=0 unreported=0`(`harmony-admin` 28 → 30)。`hvigorw assembleHap` 成功。
**设备实测**:转发球与回复球分开显示(各自图标可见);
点转发球 → 转发框打开(「转发「…」」+「抄送」折叠开关 + 取消/转发);
键盘弹出后按钮仍在屏内可点。
**未验**:真发一封转发(收件人输入在自动化里不稳 ——
`uitest inputText` 是**追加**而非替换,且 `keyEvent Back` 会退出页面而不是收键盘。
这条留待真人操作窗口,与 `harmony-p4c-boundary-decls` 那笔同性质)。
This commit is contained in:
@ -555,7 +555,14 @@ test('★ 设备:管理页能从「我的」页打开,且列表真的渲染
|
||||
return t.skip('设备不在 —— 本条的设备半边本次不跑(上面静态层仍把住逻辑与接线)');
|
||||
}
|
||||
assert.ok(await D.launchOurApp(hdc), '要能拉起应用并等到它到前台');
|
||||
await new Promise((r) => setTimeout(r, 2000));
|
||||
/*
|
||||
* ★★ 先回主界面:前面跑过的设备判据可能把前台留在 **push 出去的页面**上
|
||||
* (详情页 / 管理页 / 写信页 —— 它们没有侧栏)。实测:设备停在邮件详情页时
|
||||
* 本判据跑了 201 秒才失败,报"要能进「我的」页"—— 而那是**判据间干扰**,
|
||||
* 不是功能坏了(`backToMain` 的注释里记着同一个根因)。
|
||||
*/
|
||||
assert.ok(await D.backToMain(hdc), '要能回到主界面(侧栏/底栏可见)');
|
||||
await new Promise((r) => setTimeout(r, 1500));
|
||||
|
||||
const dims = () => {
|
||||
let w = 0;
|
||||
@ -725,3 +732,97 @@ test('★ 设备:管理页能从「我的」页打开,且列表真的渲染
|
||||
'★ 管理页要真的渲染出**用户行**(带角色/状态的那种)—— ' +
|
||||
`只有标题而没有行,说明数据没渲染。实际读到:${texts.slice(0, 25).join(' | ')}`);
|
||||
});
|
||||
|
||||
test('★ 详情页的两个动作球必须**分开摆**(Stack 的同一个角 + 各自 margin = 重叠)', () => {
|
||||
/*
|
||||
* ★★ 2026-09-19 真 bug,设备实测撞出来的:
|
||||
*
|
||||
* 详情页右下角有两个动作球(转发 + 回复)。第一版把它们**各自**写在
|
||||
* `Stack({ alignContent: Alignment.BottomEnd })` 里、各带一个 margin ——
|
||||
* 于是它们**几乎完全重叠**。实测 dump 的 bounds(密度 2.875):
|
||||
*
|
||||
* 转发 [2984,2010][3122,2148]
|
||||
* 回复 [2949,1998][3110,2159]
|
||||
* 重叠区 x ∈ [2984, 3110]
|
||||
*
|
||||
* 屏幕上只看得到一个球,**转发入口等于不存在**。
|
||||
* 根因是 ArkUI 的 `Stack.alignContent` 把每个子元素都摆到**同一个角**,
|
||||
* margin 只是各自微调 —— 想并排就得用**一个容器**把子元素排起来。
|
||||
*
|
||||
* 判据形状:找"同一容器下的多个圆形 Button,各自只带 margin"这种写法。
|
||||
* 正确写法是外面包一层 `Row({ space })`。
|
||||
*/
|
||||
const src = code(join(ETS, 'pages/MailDetailPage.ets'));
|
||||
|
||||
/*
|
||||
* 找 `Stack({ alignContent: ... })` 块里**直接**出现的两个以上圆形按钮
|
||||
* (`.borderRadius(N)` 且 N 使它是圆:宽高等于 2N)。
|
||||
* 简化判定:在同一个 Stack 块内出现 ≥2 处 `.borderRadius(` + 各自 `.margin(`
|
||||
* 而没有包在 Row 里。
|
||||
*/
|
||||
const stackBlocks = [...src.matchAll(/Stack\(\{[^}]*alignContent[^}]*\}\)\s*\{([\s\S]*?)\n\s{8}\}/g)];
|
||||
const bad = [];
|
||||
for (const m of stackBlocks) {
|
||||
const body = m[1];
|
||||
const circles = (body.match(/\.borderRadius\(\d+\)/g) ?? []).length;
|
||||
if (circles >= 2 && !/Row\(\{\s*space:/.test(body)) {
|
||||
const line = src.slice(0, m.index).split('\n').length;
|
||||
bad.push(`MailDetailPage.ets:${line}(Stack 里有 ${circles} 个圆角元素且没有 Row 包住)`);
|
||||
}
|
||||
}
|
||||
assert.deepEqual(bad, [],
|
||||
'★ `Stack.alignContent` 会把**每个**子元素都摆到同一个角 —— ' +
|
||||
'多个圆形按钮各自带 margin 时会**重叠**(实测:屏幕上只看得到一个球)。\n' +
|
||||
`命中:\n ${bad.join('\n ')}\n` +
|
||||
'正确写法:外面包一层 `Row({ space: N })`,由 Row 负责把它们排开、' +
|
||||
'Row 自己带 margin 到角落。');
|
||||
});
|
||||
|
||||
test('★ 底部弹层必须有明确高度(否则键盘一弹,按钮全被顶出屏幕)', () => {
|
||||
/*
|
||||
* ★★ 2026-09-19 真 bug(与上面那条同一次实测):
|
||||
*
|
||||
* 详情页的转发弹层第一版**没有高度**(只有 `padding(16)`)⇒ 高度完全由内容决定。
|
||||
* 而 ArkUI 默认的键盘避让是 `KeyboardAvoidMode.OFFSET`(整体上移)——
|
||||
* 上移之后,弹层里的 `TextArea` 与「取消 / 转发」按钮**跑到键盘下面**去了。
|
||||
* 实测截图:只看得见收件人输入框 + 键盘,「转发」按钮**点不到**。
|
||||
*
|
||||
* 修法两半(缺一不可):
|
||||
* ① 弹层要有**明确高度**(与回复弹层一致,`height('60%')`)——
|
||||
* 内容才能在里面重新分配空间;
|
||||
* ② 会撑高的那个子元素(说明框)要用 `layoutWeight(1)` 而不是固定高度 ——
|
||||
* 键盘顶上来时它自己缩短,把按钮留在屏内。
|
||||
*/
|
||||
const src = code(join(ETS, 'pages/MailDetailPage.ets'));
|
||||
|
||||
/* 找所有"底部弹层"(`borderRadius({ topLeft: ... })` 是这类弹层的标志) */
|
||||
const sheets = [...src.matchAll(/\.borderRadius\(\{\s*topLeft:\s*\d+/g)];
|
||||
assert.ok(sheets.length >= 2,
|
||||
`详情页应有多个底部弹层(回复 / 转发)—— 实际找到 ${sheets.length} 个`);
|
||||
|
||||
/*
|
||||
* 逐个弹层:从 borderRadius 往回找它的高度声明。
|
||||
* 在 `.borderRadius({ topLeft` 之前的一段里找 `.height(`。
|
||||
*/
|
||||
const noHeight = [];
|
||||
for (const m of sheets) {
|
||||
const before = src.slice(Math.max(0, m.index - 1200), m.index);
|
||||
/* 最近的 .height(...) 要在这一段里(弹层的修饰符链上) */
|
||||
if (!/\.height\(('100%'|\d+|'[0-9]+%')\)/.test(before)) {
|
||||
const line = src.slice(0, m.index).split('\n').length;
|
||||
noHeight.push(`MailDetailPage.ets:${line}`);
|
||||
}
|
||||
}
|
||||
assert.deepEqual(noHeight, [],
|
||||
'★ 底部弹层必须有明确高度 —— 没有高度时它的尺寸由内容决定,' +
|
||||
'而键盘弹出(默认 `KeyboardAvoidMode.OFFSET`)会把内容整体上移,' +
|
||||
'**「取消 / 转发」按钮会被顶到键盘下面**(实测截图:按钮点不到)。\n' +
|
||||
`缺高度的弹层:\n ${noHeight.join('\n ')}`);
|
||||
|
||||
/* ② 说明框用 layoutWeight 而不是固定高度(否则它撑住高度、按钮仍被挤出) */
|
||||
const ta = /TextArea\(\{[\s\S]{0,200}?转发说明[\s\S]{0,400}?\n\s*\}\)([\s\S]{0,300}?)\.onChange/.exec(src);
|
||||
assert.ok(ta, '要能找到转发弹层的说明框(TextArea)');
|
||||
assert.match(ta[1], /\.layoutWeight\(1\)/,
|
||||
'★ 转发说明框要用 `layoutWeight(1)`(不是固定 `height(N)`)—— ' +
|
||||
'弹层高度固定后,只有它会伸缩才能把「取消 / 转发」按钮留在键盘之上');
|
||||
});
|
||||
|
||||
@ -115,7 +115,7 @@ const SUITE = [
|
||||
['test/criteria-hygiene.test.mjs', [], 6],
|
||||
// 用户管理页(P4c 同批):动作↔服务端调用同名 / 门禁只认严格 admin /
|
||||
// 启停只发 status / 「受限」徽标口径 / 页面零写死色值 / 接线(纯逻辑真被调用)
|
||||
['test/harmony-admin.test.mjs', ['--experimental-strip-types', '--no-warnings'], 28],
|
||||
['test/harmony-admin.test.mjs', ['--experimental-strip-types', '--no-warnings'], 30],
|
||||
// P4c 图片上传:阈值与两档策略 / 失败必带原因 / 退档判定只有一处 /
|
||||
// release 都 await / 解码按目标尺寸 / multipart 字段名 / 上传后重新同步
|
||||
['test/harmony-imageprep.test.mjs', ['--experimental-strip-types', '--no-warnings'], 31],
|
||||
|
||||
Reference in New Issue
Block a user