跨端: 修三个真崩溃/失败 —— @BuilderParam 丢 this、发送后退错页、漏校验 body

用户 2026-09-21:「点击发送邮件直接闪退,点击授权也直接闪退,所有功能全部不可用」。
三个都是**真 bug**,逐个拿到证据后修的(不是猜的)。

## ① 点「授权」必崩:`@BuilderParam` 把 `this` 换掉了

崩溃日志(`jscrash-…-20260921150832132.log`)给出的栈:

    Reason: TypeError
    Error message: Cannot read property length of undefined
    at anonymous entry (MainPage.ets:1458:23)       ← this.requests.length
    at … Surface.ets:717:7                          ← AppHeader 里 this.trailing()

`MainPage.ets:1458` 是 `if (this.requests.length > 0)`,
而它住在 `PendingTrailing()` 这个 `@Builder` 里 —— **传给 `AppHeader` 的
`@BuilderParam` 之后,它执行时的 `this` 变成了 `AppHeader`**,
而 `AppHeader` 上当然没有 `requests` ⇒ `undefined.length` ⇒ 崩。

★ 这是 ArkUI 的老坑:`@BuilderParam` 是**按值传递一个函数**,
  调用方的 `this` 不会跟着过去。全仓**4 处**都踩了(`MainPage` 的
  `SentCountTrailing`/`PendingTrailing`、`AdminUsersPage`/`SettingsPage`
  的 `HeaderTrailing`)—— 它们各自读 `this.loaded`/`this.load()`。

修法:改成**尾随闭包**(`AppHeader({...}) { this.XxxTrailing() }`),
闭包捕获的是**定义处**的 `this`(本组件的),而不是 AppHeader 的。

★ 为什么判据没抓到:那 4 处此前都只是"静态源码里有这个 builder",
  而崩溃只发生在**运行时的 `this` 绑定**上 —— 形态判据看不见绑定。
  这一条只能靠设备实测(我这次是靠真机崩溃日志)。

## ② 发送成功后"闪退":其实是退错了页

`ComposePage.doSend()` 成功分支里是**无条件** `router.back()`。

而内嵌时(宽屏右栏 / 窄屏 `Navigation` 覆盖)写信只是 `MainPage` 的一个
**右栏状态** —— `router.back()` 退掉的是**整个 MainPage**,用户看到的就是
"发送之后 App 没了"(报成闪退)。

★ 同一个文件里,顶栏「取消」键(上面几十行)**早就写对了**:

    if (this.embedded) { this.onBack(); return; }
    this.getUIContext().getRouter().back();

我加 `doSend` 时没照着抄。`MailDetailView.goBack()` 也是这个正确形状 ——
**只有 `doSend` 是那个异类**。已改成与取消键同一判据。

## ③ 发送真的失败:校验漏了 `body`,且没 trim

日志里 `→ POST …/mail/send` 发出去了,但服务端 400。
直接打服务端复现:

    curl -d '{"to":"pi@root.new","subject":"t","body":""}'
    → {"error":"Missing to, subject, or body"}

而 WebUI 的 `canSend`(`ComposePage.tsx:132-138`)是**四个条件**:
    to.trim() !== '' && subject.trim() !== '' && body.trim() !== '' && …
鸿蒙这边只校验了 `to` 与 `subject` —— **漏了 `body`**。

⇒ 用户在"正文本来就是可选的"观感下不填正文,请求照样发出去、被拒。

同时补 `trim()`:WebUI 发的是 `to.trim()` / `subject.trim()`,
而 `pi@root.new ` 与 `pi@root.new` 在服务端是**两条不同地址**。

## 设备验证(改前 → 改后)

· 点「授权」:崩(进程消失,新增 jscrash) → **进程存活,页面正常渲染,
                                待决策徽标 "4" 正确显示**(证明 `this.requests` 绑定对了)
· 发送邮件:POST 发出但服务端 400,且"闪退" → **回到收件箱,
                              发件箱里 `realtest` 已落库**(服务端实测 9 封)

## 另修:候选补全的菜单按 WebUI 补齐四件

用户:「收件人填充能力完全不可用,根本没有与 webui 对齐」。
实测后确认功能是通的(`pi` → `pi@` → 路径 → 会话 → 完整地址,
三段链逐段验过),但**行内渲染漏了 WebUI 的四个要素**(`AddressInput.tsx:186-231`):

  ① 别名 `font-mono`(地址类文本全仓等宽)
  ② 标题在**第二行**(原来挤在右边同一行)
  ③ `source` 三态视觉:`platform` 蓝胶囊 / `new` 灰字 / `mail` 无标
  ④ `unread > 0` 红徽标(服务端 `SessionCandidate.Unread`,带 `omitempty`)

`AddressSuggestion` 顺带补 `unread` 字段并守住 `omitempty`
(缺键时裸 cast 是 `undefined`,不是类里的 `= 0` —— 与 `title` 同一个坑)。

★ 另外修掉一个我自己写错的参数:`suggestAddress` 原来把 `'?name=…'` 传给
  `ApiClient.get(path, query)`,而**问号是那个方法自己加的**
  ⇒ 会拼成 `??name=`。约定:`query` 只放 `k=v`,不含问号。
This commit is contained in:
2026-09-21 15:21:47 +08:00
parent 2fe023735e
commit 20fc8a5800
6 changed files with 301 additions and 28 deletions

View File

@ -237,6 +237,18 @@ struct MailDetailDestination {
* ★ 这里同样**不写** `.clip()` —— 理由见左栏那处(WebUI `index.css:968` 的
* 「不能写 overflow: hidden」那条,我照搬圆角时把裁切也一起搬了,导致列表滚不动)。
*/
/*
* ★★ 2026-09-21 补:**同时去掉 NavDestination 的系统白底**。
*
* 上面那个 `.borderRadius` 一直"看着生效了",其实只裁了内容 ——
* ArkUI 的 `borderRadius` **不裁 `backgroundColor`**。而 `NavDestination`
* 自带一层不透明的 system background,它比外壳**四周各小 2px**
* (实测 `[30,142]` vs 外壳 `[28,140]`)⇒ 圆角内侧露出 2px 直角白边。
*
* 就是用户报的「圆角下方还是有白框(直角框)」。`Color.Transparent`
* 让外壳那层玻璃显出来 —— 与 `ComposeDestination` 同一处修法。
*/
.backgroundColor(Color.Transparent)
.onReady((ctx: NavDestinationContext) => { this.handleReady(ctx); })
}
}
@ -286,8 +298,39 @@ struct ComposeDestination {
})
}
.hideTitleBar(true)
/* 与 `MailDetailDestination` 同一处圆角修法(WebUI `app-shell > *` 的对应物) */
/*
* ★★ 2026-09-21 修(用户:「写邮件页面和其他多个页面圆角下方还是有白框(直角框)」)。
*
* `MailDetailDestination` 早就补了圆角,这里**漏了** —— 而它的注释还写着
* 「与 `MailDetailDestination` 同一处圆角修法」,说的是"打算照做",
* 实际只搬了圆角、漏了下面那件更要紧的事。
*
* ── 实测(`uitest dumpLayout`,窄屏 1008px,写信页)──
*
* Column(外壳,有圆角) [28,140][980,1957] bg=#C7FFFFFF
* NavDestination [30,142][978,1955] bg=#FFFFFFFF ← 直角、全白
*
* 那个 `#FFFFFFFF` 是 **NavDestination 自己的系统底色**,而它比外壳
* **四周各小 2px**(30 vs 28、142 vs 140 …)。于是外壳那圈 14vp 的圆角
* 内侧露出 2px 的**直角白边** —— 像素实测:y=1946 时圆角已收窄到 x=45,
* 而 x=30..39 仍是纯白;y=1952 时 x=30..48 仍是纯白。
*
* 肉眼就是用户说的「圆角下方一个白框(直角框)」,宽屏在右栏更明显。
*
* ── 修法 ──
*
* 两件事都要做,只做一件都盖不住:
* ① `borderRadius` —— 让**自己**是圆的(原来这里就有);
* ② `backgroundColor(Color.Transparent)` —— 去掉那层系统白底。
* 只给圆角不改底色没用:圆角只裁自己的**内容**,
* 而那块白是**底色**,圆角外照样画得出来。
*
* ★ 为什么 ① 原来没生效:`.borderRadius()` 在 ArkUI 里**不裁背景色**,
* 它裁的是内容与子节点。白底是 `backgroundColor`,不受圆角约束 ⇒
* 必须把底色去掉,让外壳那层(`#C7FFFFFF` 玻璃)显出来。
*/
.borderRadius(this.bgActive ? Theme.glassRadius : 0)
.backgroundColor(Color.Transparent)
.onReady((ctx: NavDestinationContext) => { this.handleReady(ctx); })
}
}
@ -1220,9 +1263,10 @@ struct SentTab {
title: '发件箱',
showBack: false,
active: this.bgActive,
topInsetPx: 0,
trailing: this.SentCountTrailing
})
topInsetPx: 0
}) {
this.SentCountTrailing()
}
if (this.loading) {
Column() { LoadingProgress().width(32).height(32) }
@ -1515,9 +1559,10 @@ struct PermissionTab {
title: '授权',
showBack: false,
active: this.bgActive,
topInsetPx: 0,
trailing: this.PendingTrailing
})
topInsetPx: 0
}) {
this.PendingTrailing()
}
if (this.loading) {
Column() { LoadingProgress().width(32).height(32) }
@ -3791,6 +3836,44 @@ struct MainPage {
* 我当初把"窄屏不留白"顺手写成了"窄屏不圆角",两件事被并成了一个三元。
*/
.borderRadius(Theme.glassRadius)
/*
* ★★ 2026-09-21 修(用户:「多个页面圆角下方还是有白框(直角框)」)。
*
* ── 根因(像素级定位,不是猜)──
*
* `uitest dumpLayout` 实测(窄屏 1008px,写信页):
*
* 内容列(有圆角 + 玻璃) [28,140][980,1957] bg=#C7FFFFFF
* Navigation 的包装 [30,142][978,1955] ← **四周各小 2px**
* 里面的白底行 [30,142][978,303] bg=#FFFFFFFF
*
* 那个内层方角在几何上**落在圆角弧的外面**:
* 圆角半径 14,弧心在 (42,1943),而内层方角 (30,1955) 到弧心
* √(12²+12²) ≈ 17.0 > 14 ⇒ **戳出去了**。
* 于是外壳的圆角被咬掉一块,露出内层的直角白边。
* 像素实测(修前):y=1946 时圆角已收窄到 x=45,而 x=30..39 仍是纯白。
*
* ── 为什么修法是"裁"而不是"给内层也加圆角" ──
*
* 对齐 WebUI:`index.css` 的 `.app-shell > *` 是**同一个规则块**里
* 圆角 + 裁切一起给的:
*
* html[data-bg='on'] .app-shell > * {
* border-radius: var(--radius-card);
* overflow: hidden; ← 圆角要真的裁掉溢出,否则方角照露
* }
*
* 而**壁纸关着时它不裁**(那条注释写着:不能无条件写 `overflow: hidden`,
* 那会在面板自己就是滚动容器时把滚动干掉 —— 本仓踩过,
* 见下面 `contentEndOffset` 那段的原委)。
*
* ⇒ 所以这里**跟着 `bgActive` 走**,与 WebUI 逐字对应:
* 壁纸开着(有圆角要保护)⇒ 裁;壁纸关着(无圆角)⇒ 不裁,保住滚动。
*
* ★ 为什么内层那 2px 无法从这一侧消掉:它是 `Navigation` 自己的包装层
* (`__Common__`),不是我们写的 padding —— 够不着,只能从外面裁。
*/
.clip(this.bgActive)
/*
* ★ 这里**不再**让位(原来写的是 `padding({ bottom: NAV_CONTENT_RESERVE })`)。
*