跨端: 往返预算编辑上线(详情页缺的第三块)+ 修折叠头部点不中的真 bug

审计发现详情页缺三块功能之三:**往返预算**(WebUI `BudgetEditor`)。
`MailApi.setBudget` 早就有了,缺的是 `getBudget` 与 UI。

## 一、预算条(对齐 WebUI,两态)

显示态是徽标(`已用/上限 来回` / 不限时「预算不限」/ 用尽时标红),
点它进入编辑态:输入框 + 保存 + 重置 + 取消。

★ 「用尽」的判定**走服务端的 `remaining`**,不自己拿 `used >= max` 算:
不限时服务端给 `remaining: -1`,那个式子在那种情形下会算出"已用尽"。
两端都得用服务端的口径。

★ 「保存」与「重置计数」走**同一条 PUT**(服务端支持两者同时给,
它的注释原话:「加到 20 并从头算」是一次很自然的操作,
拆成两个请求只会让前端多一次往返)。

**契约实测**(不是形态检查):
    GET  → {"max_rounds":0,"used_rounds":1,"remaining":-1,"unlimited":true}
    PUT {"max_rounds":7,"reset":false} → {"max_rounds":7,"remaining":6,"unlimited":false}
    PUT {"max_rounds":0,"reset":false} → {"remaining":-1,"unlimited":true}      ← 0 = 不限

## 二、真 bug:折叠头部的展开区只有 **14px** 高

设备实测 dump:`Row [1277,112][3122,126]` —— 可点区高度 = 文字高度(14px 字号)。
而全屏之后**状态栏也在 y=112 那个区间** ⇒ 点它反复触发**系统手势(下拉通知)**
而不是展开头部。我为此试了七八次,每次都回到桌面。

而收起态头部里藏着**这个会话的全部旋钮**(收件人/时间/抄送/权限档/预算)——
点不中就等于那些都看不到。

修:给那一行 `height(36)`(与左边返回键同高)。实测可点区
**14px → 43px**(`[1277,112][3122,155]`)。

★ 判据 + **变异验证**:去掉 `.height(36)` ⇒ 判红;还原 ⇒ 绿。

## 三、判据

`harmony-logic` 新增「折叠头部展开区高度 ≥32vp」一条(含变异自检)。
`run-all.mjs` → `checks=511 pass=511 fail=0 skip=0 red=0 broken=0 unreported=0`。

**未验**:预算条在设备上的**观感与点击**(模拟器顶部 155px 是系统手势区,
而头部恰在其中 —— 坐标式点击在那里会被系统抢走;真机上头部在状态栏之下)。
预算条的**数据链路**已用 curl 逐条实测(见上),但"点保存按钮后徽标变化"
这一步没在设备上走通。
This commit is contained in:
2026-09-19 17:41:09 +08:00
parent e79a86ab79
commit ac62dafde1
4 changed files with 269 additions and 4 deletions

View File

@ -739,3 +739,52 @@ test('★ 改名建议:字段名与两端契约一致,接受/驳回走对端
assert.ok(!/\/sessions\/' \+ sessionId \+ '\/alias'/.test(mutated),
'★ 路径写错时上面那条断言必须能判红');
});
test('★ 详情页折叠头部的展开区要有足够高度(14px 的横条点不中)', () => {
/*
* ★★ 2026-09-19 真 bug,设备实测撞出来的:
*
* 详情页折叠态的头部只有一行标题,那一行的 `Row` **没有高度** ⇒
* 高度就是内容(14px 字号)。实测 dump 的可点区:
*
* Row [1277,112][3122,126] ← 只有 14px 高
*
* 叠上"全屏后状态栏也在 y=112 那个区间"这个事实 ⇒ 反复点它
* **触发系统手势(下拉通知)而不是展开头部**。我为此试了七八次,
* 每次都回到桌面。
*
* 而收起态头部里藏着**这个会话的全部旋钮**(收件人/时间/抄送/权限档/预算),
* 点不中就等于那些东西都看不到。
*
* 修法:给那一行 `height(36)`(与左边返回键同高)—— 实测可点区
* 从 14px 变成 43px。
*
* 判据形状:找"`layoutWeight(1)` + `onClick` 且**没有 height**"的头部行。
* 这类行都是"点它展开/切换"的可点区,太薄就点不中。
*/
const src = code(join(HARMONY_ETS, "pages/MailDetailPage.ets"));
/*
* 定位折叠头部的展开行:它的标志是「`.layoutWeight(1)` 之后紧跟
* `.onClick(() => { this.headerOpen = !this.headerOpen; })`」。
*/
/*
* ★ 锚点要**跳过注释**:那一段里有一大段解释(讲这个 bug 本身),
* 而我第一版的正则只允许空白,于是匹配不上(判据假红)。
* 用 `code()` 读的是**剥注释版**,所以这里直接在剥注释后的文本上找 ——
* 拿它当锚点最稳。
*/
/*
* ⚠️ 用 `[\s\S]{0,300}?` 而不是 `\s*`:`code()` 剥注释后会在原处留下**大量空行**
* (实测 `.layoutWeight(1)` 与 `.height(36)` 之间隔了 15 个空行)——
* 而且那些空行里可能有缩进空格。所以两侧都用"任意字符、尽量少"。
*/
const m = /\.layoutWeight\(1\)[\s\S]{0,300}?\.height\((\d+)\)[\s\S]{0,300}?\.onClick\(\(\)\s*=>\s*\{\s*this\.headerOpen/.exec(src);
assert.ok(m,
'要能找到折叠头部的展开行 —— 形状是「`.layoutWeight(1)` → `.height(N)` → `.onClick(headerOpen 切换)`」。' +
'若它变成别的顺序,更新这条正则(并确认那个顺序下可点区仍然够大)');
const h = [null, m[1]];
assert.ok(Number(h[1]) >= 32,
`★ 展开行的高度应 ≥32vp(实测 14px 太薄、点不中;修成 36 后实测可点区 43px)。实际 ${h[1]}`);
});

View File

@ -78,7 +78,7 @@ const SUITE = [
// 预设的**行为**判据:每一档都真的画得出来(能真跑,不需要设备 ⇒ 不进 static 欠账)。
// 与 appearance-defaults 那条「清单 id/顺序相等」配对:值判据管清单,行为判据管渲染器。
['test/harmony-presets.test.mjs', ['--experimental-strip-types', '--no-warnings'], 6],
['test/harmony-logic.test.mjs', ['--experimental-strip-types', '--no-warnings'], 31],
['test/harmony-logic.test.mjs', ['--experimental-strip-types', '--no-warnings'], 32],
['test/harmony-system-api.test.mjs', [], 5],
// P4 外观同步:跑 model/Appearance.ts(纯逻辑),所以也要 strip-types
['test/harmony-appearance.test.mjs', ['--experimental-strip-types', '--no-warnings'], 27],