跨端: 邮件转发上线(详情页缺的入口)+ 修两个真 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],
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import { ApiClient } from './ApiClient';
|
||||
import { MailSummary, Session, Contact, MailDetail, ThreadResponse, AttachmentInfo, SendMailRequest, SendMailResult, SentResponse, PermissionRequest, PendingResponse, DecideResponse } from '../model/Models';
|
||||
import { MailSummary, Session, Contact, MailDetail, ThreadResponse, AttachmentInfo, SendMailRequest, SendMailResult, SentResponse, PermissionRequest, PendingResponse, DecideResponse, ForwardMailRequest} from '../model/Models';
|
||||
|
||||
/** 收件箱响应 */
|
||||
export class InboxResponse {
|
||||
@ -187,8 +187,19 @@ export class MailApi {
|
||||
return this.client.post<SendMailResponse>('/me/mail/send', req);
|
||||
}
|
||||
|
||||
/** 转发 */
|
||||
async forward(mailId: string, req: SendMailRequest): Promise<SendMailResponse> {
|
||||
/**
|
||||
* 转发。
|
||||
*
|
||||
* ★★ 参数类型是 **`ForwardMailRequest`**(不是 `SendMailRequest`)——
|
||||
* 两者字段集不同:服务端 `forwardRequest` 没有 `body` / `reply_to` /
|
||||
* `attachment_ids` / `max_rounds` / `permission_mode`,
|
||||
* 而它用的 `Decode()` 是 `DisallowUnknownFields()` ⇒ 多带字段直接 400。
|
||||
*
|
||||
* 这个方法**原先的签名写的就是 `SendMailRequest`** —— 一直没被发现,
|
||||
* 因为**从来没有调用方**(详情页没有转发入口)。审计补上入口时才暴露出来:
|
||||
* "写好了但没人用"的代码,连它自己的类型对不对都没人验过。
|
||||
*/
|
||||
async forward(mailId: string, req: ForwardMailRequest): Promise<SendMailResponse> {
|
||||
return this.client.post<SendMailResponse>('/me/mail/' + mailId + '/forward', req);
|
||||
}
|
||||
|
||||
|
||||
@ -255,6 +255,31 @@ export class SendMailRequest {
|
||||
permission_mode: string = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 转发请求体(`POST /me/mail/{id}/forward`)。
|
||||
*
|
||||
* ★★ 为什么**不**复用 `SendMailRequest`(这是我刚踩过的那个坑的第二次):
|
||||
* 服务端 `forwardRequest`(`forward.go:23`)只认五个字段 ——
|
||||
* `to` / `cc` / `comment` / `subject` / `session_alias`,
|
||||
* **没有** `body`、`reply_to`、`attachment_ids`、`max_rounds`、`permission_mode`。
|
||||
* 而服务端的 `Decode()` 是 `DisallowUnknownFields()`(严格)⇒
|
||||
* 多带一个字段就 **400**。
|
||||
*
|
||||
* 同一个错误我今天在 `AppearancePayload` 上刚犯过一次(拿响应类型当请求体),
|
||||
* 表现也一样隐蔽:本地状态立刻变、看起来成功了,只有看 HTTP 状态码才发现 400。
|
||||
* ⇒ 请求体一个端点一个类型,**不复用**。
|
||||
*/
|
||||
export class ForwardMailRequest {
|
||||
to: string = '';
|
||||
cc: string = '';
|
||||
/** 转发者附加的说明,置于引用原文之前 */
|
||||
comment: string = '';
|
||||
/** 留空 ⇒ 服务端自动加 `Fwd: ` 前缀(它处理了 "Fwd: Fwd:" 无限叠加) */
|
||||
subject: string = '';
|
||||
/** 仅在目标地址以 .new 结尾时生效 */
|
||||
session_alias: string = '';
|
||||
}
|
||||
|
||||
/** 发信响应(含预算) */
|
||||
export class SendMailResult {
|
||||
mail_id: string = '';
|
||||
|
||||
@ -9,7 +9,7 @@ import { Theme } from '../common/Theme';
|
||||
import { Markdown } from '@luvi/lv-markdown-in';
|
||||
import { MailApi } from '../api/MailApi';
|
||||
import { AccountManager, AccountInfo } from '../api/AccountManager';
|
||||
import { MailDetail, SendMailRequest, Address } from '../model/Models';
|
||||
import { MailDetail, SendMailRequest, ForwardMailRequest, Address } from '../model/Models';
|
||||
import { MailDetailParams } from '../model/RouteParams';
|
||||
import { AmIcon } from '../common/Icons';
|
||||
import { permissionLabel } from '../model/MailGrouping';
|
||||
@ -94,6 +94,19 @@ export struct MailDetailView {
|
||||
@State showReplyBox: boolean = false;
|
||||
@State replyBody: string = '';
|
||||
@State sending: boolean = false;
|
||||
/*
|
||||
* 转发(2026-09-19 审计发现缺失)。
|
||||
*
|
||||
* `MailApi.forward()` **早就实现了**(`api/MailApi.ets:191`),
|
||||
* 服务端 `forward.go` 也完整(含引用块渲染与 `Fwd:` 叠加处理)——
|
||||
* 缺的只是这一页的入口。对齐 WebUI `MailView.tsx:378` 的 `ForwardBar`。
|
||||
*/
|
||||
@State showForwardBox: boolean = false;
|
||||
@State forwardTo: string = '';
|
||||
@State forwardCc: string = '';
|
||||
/** 抄送输入是否展开(WebUI 的 `ccOpen`:默认收起,点「抄送」才出现) */
|
||||
@State forwardCcOpen: boolean = false;
|
||||
@State forwardComment: string = '';
|
||||
@State sessionId: string = '';
|
||||
@State switchingPerm: boolean = false;
|
||||
/**
|
||||
@ -460,14 +473,50 @@ export struct MailDetailView {
|
||||
* 实测 1280×800 下回复框占 31%;输入框不是阅读时每刻都要用的东西,
|
||||
* 就该按需展开。球同时与列表页的 ComposeFab 同一套观感,不新增一种视觉语言。
|
||||
*/
|
||||
Button() {
|
||||
AmIcon({ iconName: 'chatBubble', iconSize: 24, iconColor: Theme.accentFg })
|
||||
/*
|
||||
* 两个动作球:转发(次要)+ 回复(主要)。
|
||||
*
|
||||
* ★★ 2026-09-19 修(真 bug,设备实测撞出来的):第一版把两个球**各自**
|
||||
* 写在 `Stack({alignContent: BottomEnd})` 里、各带一个 margin ——
|
||||
* 于是它们**几乎完全重叠**(实测 dump 的 bounds:
|
||||
* 转发 `[2984,2010][3122,2148]`、回复 `[2949,1998][3110,2159]`,
|
||||
* 重叠区 2984–3110)。屏幕上只看得到一个球,转发入口等于不存在。
|
||||
*
|
||||
* 根因:`Stack` 的 `alignContent` 把**每个**子元素都摆到同一个角,
|
||||
* margin 只是各自微调 —— 想并排就得用**一个容器**把它们排起来。
|
||||
* ⇒ 用 `Row({ space })` 包住两个球,Row 自己带 margin 到右下角。
|
||||
*
|
||||
* 视觉分工:实心球是**主动作**的语言(回复),
|
||||
* 转发用描边球(`surfaceMuted` 底 + 品牌色图标)—— 两个实心球并排
|
||||
* 会让人分不出哪个更主要。
|
||||
*/
|
||||
Row({ space: 12 }) {
|
||||
Button() {
|
||||
AmIcon({ iconName: 'forward', iconSize: 20, iconColor: Theme.accent })
|
||||
}
|
||||
.width(48).height(48)
|
||||
.borderRadius(24)
|
||||
.backgroundColor(Theme.surfaceMuted)
|
||||
.onClick(() => {
|
||||
/*
|
||||
* 打开转发框时把回复框关掉:两者都是底部弹层,
|
||||
* 同时开着会叠在一起(WebUI 也用同一份 `forwarding` 状态互斥)。
|
||||
*/
|
||||
this.showReplyBox = false;
|
||||
this.showForwardBox = true;
|
||||
})
|
||||
|
||||
Button() {
|
||||
AmIcon({ iconName: 'chatBubble', iconSize: 24, iconColor: Theme.accentFg })
|
||||
}
|
||||
.width(56).height(56)
|
||||
.borderRadius(28)
|
||||
.backgroundColor(Theme.accent)
|
||||
.onClick(() => { this.showForwardBox = false; this.showReplyBox = true; })
|
||||
}
|
||||
.width(56).height(56)
|
||||
.borderRadius(28)
|
||||
.backgroundColor(Theme.accent)
|
||||
.margin({ right: 16, bottom: this.navReserve + 16 })
|
||||
.onClick(() => { this.showReplyBox = true; })
|
||||
.alignItems(VerticalAlign.Bottom)
|
||||
/* 让出底部悬浮条;右边距与原来的回复球一致(球没变位置,只是多了左边一个) */
|
||||
.margin({ right: 16, bottom: this.navReserve + 16 })
|
||||
}
|
||||
.width('100%').layoutWeight(1)
|
||||
|
||||
@ -530,12 +579,147 @@ export struct MailDetailView {
|
||||
.width('100%').height('100%')
|
||||
.position({ x: 0, y: 0 })
|
||||
}
|
||||
|
||||
/*
|
||||
* 转发弹层 —— 与回复弹层同构(同一套遮罩 + 底部弹层 + 同一套入场动画),
|
||||
* 但字段是 WebUI `ForwardBar` 那四个:收件人 / 抄送(可折叠)/ 说明 / 引用原文。
|
||||
*
|
||||
* WebUI 的字段顺序与占位文案逐条对齐(`MailView.tsx:412-437`)。
|
||||
*/
|
||||
if (this.showForwardBox) {
|
||||
Column() {
|
||||
Column()
|
||||
.width('100%').layoutWeight(1)
|
||||
.backgroundColor(Theme.overlay)
|
||||
.onClick(() => { this.showForwardBox = false; })
|
||||
|
||||
Column() {
|
||||
Row() {
|
||||
Text('转发「' + this.subject + '」')
|
||||
.fontSize(14).fontWeight(FontWeight.Bold).fontColor(Theme.textPrimary)
|
||||
.layoutWeight(1)
|
||||
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
/* 「抄送」是个**折叠开关**(WebUI 同形:默认收起,点它才出现输入框) */
|
||||
Text(this.forwardCcOpen ? '收起抄送' : '抄送')
|
||||
.fontSize(11)
|
||||
.fontColor(this.forwardCcOpen ? Theme.accent : Theme.textMuted)
|
||||
.onClick(() => { this.forwardCcOpen = !this.forwardCcOpen; })
|
||||
}
|
||||
.width('100%')
|
||||
.margin({ bottom: 10 })
|
||||
|
||||
TextInput({ text: this.forwardTo, placeholder: '新收件人:pi@root.new' })
|
||||
.width('100%').height(38).fontSize(13)
|
||||
.onChange((v: string) => { this.forwardTo = v; })
|
||||
|
||||
if (this.forwardCcOpen) {
|
||||
TextInput({ text: this.forwardCc, placeholder: '抄送:逗号分隔,可多个' })
|
||||
.width('100%').height(38).fontSize(13)
|
||||
.margin({ top: 8 })
|
||||
.onChange((v: string) => { this.forwardCc = v; })
|
||||
}
|
||||
|
||||
/*
|
||||
* 说明框用 `layoutWeight(1)` 而不是固定高度:弹层现在有明确高度
|
||||
* (`height('60%')`),剩下多少空间它就占多少 —— 这样键盘顶上来时
|
||||
* 它**自己会缩短**,把「取消 / 转发」按钮留在屏幕内。
|
||||
* 固定 `height(70)` 在键盘弹出后会把按钮挤出视口(第一版就是这个症状)。
|
||||
*/
|
||||
TextArea({
|
||||
placeholder: '转发说明(可选,置于引用原文之前;原文将以引用块附在下方)'
|
||||
})
|
||||
.width('100%').layoutWeight(1).fontSize(13)
|
||||
.margin({ top: 8 })
|
||||
.onChange((v: string) => { this.forwardComment = v; })
|
||||
|
||||
Row() {
|
||||
Button('取消')
|
||||
.width(80).height(36)
|
||||
.backgroundColor(Theme.surfaceMuted)
|
||||
.fontColor(Theme.textMuted)
|
||||
.fontSize(13)
|
||||
.onClick(() => { this.showForwardBox = false; })
|
||||
|
||||
Blank()
|
||||
|
||||
Button(this.sending ? '转发中…' : '转发')
|
||||
.width(80).height(36)
|
||||
.backgroundColor(Theme.accent)
|
||||
.fontSize(13)
|
||||
/* 收件人为空时不可点 —— WebUI 同口径(`disabled={busy || !to.trim()}`) */
|
||||
.enabled(!this.sending && this.forwardTo.length > 0)
|
||||
.onClick(() => { this.doForward(); })
|
||||
}
|
||||
.width('100%')
|
||||
.margin({ top: 12 })
|
||||
}
|
||||
.width('100%')
|
||||
/*
|
||||
* ★★ 2026-09-19 修(真 bug,设备实测撞出来的):
|
||||
* 第一版这里**没有高度**,只有 `padding(16)` —— 于是弹层的高度
|
||||
* 完全由内容决定,而内容一旦被键盘顶起来,`TextArea` 与
|
||||
* 「取消 / 转发」两个按钮就**跑到键盘下面**去了(实测截图:
|
||||
* 只看得见收件人输入框 + 键盘,「转发」按钮点不到)。
|
||||
*
|
||||
* ArkUI 默认的键盘避让是 `KeyboardAvoidMode.OFFSET`
|
||||
* (整个布局上移),而要让"上移之后按钮仍在屏幕内",
|
||||
* 弹层必须有**明确的高度**(内容才能在里面重新分配空间)。
|
||||
*
|
||||
* 与回复框对齐:那里写的是 `height('60%')`,一直没出问题 ——
|
||||
* 两个弹层本来就应该同构(同一套交互)。
|
||||
*/
|
||||
.height('60%')
|
||||
.padding(16)
|
||||
.backgroundColor(Theme.surface)
|
||||
.borderRadius({ topLeft: 12, topRight: 12 })
|
||||
/* 同回复框:`rise-in` 挂在**弹层本身**(理由见回复框那段注释) */
|
||||
.transition(Theme.paneRiseIn())
|
||||
}
|
||||
.width('100%').height('100%')
|
||||
.position({ x: 0, y: 0 })
|
||||
}
|
||||
}
|
||||
}
|
||||
.width('100%').height('100%')
|
||||
.backgroundColor(Theme.pageBg)
|
||||
}
|
||||
|
||||
/**
|
||||
* 转发:`POST /me/mail/{id}/forward`(服务端 `forward.go`)。
|
||||
*
|
||||
* 字段与 WebUI `ForwardBar.submit()` 逐条对齐:`to` / `cc` / `comment`
|
||||
* (`subject` 留空让服务端自动加 `Fwd: ` 前缀 —— 它处理了"Fwd: Fwd:" 无限叠加)。
|
||||
*
|
||||
* ★ 成功后**不刷新列表**:WebUI 那里刷了 inbox/sent/sessions/contacts 四份,
|
||||
* 因为它是长驻的 SPA、靠 store 驱动。鸿蒙这一页是 push 出来的详情页,
|
||||
* 返回时会重新挂载列表页(`aboutToAppear` 重新拉)——
|
||||
* 在这里刷是白拉一次(而且用户还看不到,因为列表在下一层)。
|
||||
*/
|
||||
async doForward(): Promise<void> {
|
||||
const m: MailApi | null = this.mailApi;
|
||||
if (m === null || this.sending || this.forwardTo.length === 0) {
|
||||
return;
|
||||
}
|
||||
this.sending = true;
|
||||
try {
|
||||
const req: ForwardMailRequest = new ForwardMailRequest();
|
||||
req.to = this.forwardTo;
|
||||
req.cc = this.forwardCc;
|
||||
req.comment = this.forwardComment;
|
||||
/* subject 留空 ⇒ 服务端自动加 `Fwd: ` 前缀(不在这里拼,避免叠成 Fwd: Fwd:) */
|
||||
await m.forward(this.mailId, req);
|
||||
this.showForwardBox = false;
|
||||
this.getUIContext().getPromptAction().showToast({ message: '已转发' });
|
||||
} catch (e) {
|
||||
const ae = e as ApiError;
|
||||
this.getUIContext().getPromptAction().showToast({
|
||||
message: ae.message.length > 0 ? ae.message : '转发失败'
|
||||
});
|
||||
} finally {
|
||||
this.sending = false;
|
||||
}
|
||||
}
|
||||
|
||||
async doReply(): Promise<void> {
|
||||
const m: MailApi | null = this.mailApi;
|
||||
if (m === null || this.replyBody.length === 0) {
|
||||
|
||||
Reference in New Issue
Block a user