feat(harmony): 邮件详情与「我的」页 1:1 对齐 WebUI,并修两个线上 bug
用户:「为什么邮件页面没有对齐 webui?」「我的页面也没有对齐」,
选定「完全 1:1」。两个页面都不是「没做」,而是**做了一半** ——
`AuthApi` 的 logout/listKeys/createKey/revokeKey 全都写好但没人调,
`Models.ets` 的字段声明漏了服务端一直在返回的那些。
── 邮件详情页(对齐 WebUI `MailView.Header` / `CollapsibleHeader`)──
· 头部改**可折叠**(默认收起):收起只留标题 + 必须常驻的状态点
(未读 / 权限请求)+ 箭头。WebUI 的理由:顶部信息常驻会把可读区压成
一条缝(实测 1280×800 下头部 17% + 回复框 31%,正文只剩 48%)。
· 发件/收件改**三段式完整地址**:Agent → `pi@/home/program/agentmail.别名`,
人 → 只有名字。新增 `model/ReplyTarget.ts`(逐字移植 WebUI `replyTarget.ts`,
并对齐后端 `models.FormatAddress`)。
· 时间:`localDateTime()` → `2026/09/15 11:37:07`(原为裸 ISO `2026-09-15T03:37:07.14758Z`)。
· 档位:`permissionLabel()` → `只读/目录内/全权`(原为英文 `plan/workspace/full`;
这个函数早就在 `MailGrouping.ts` 里、列表页也在用,只有详情页没用)。
· 新增抄送行;回复入口改**右下悬浮球**(WebUI `reply-fab`),
不再是底部 56vp 通栏按钮。
── 「我的」页(对齐 WebUI `AccountPage`,补 5 个缺失 section)──
标题由「账号管理」改为「我的」(多账号只是其中一段,不是整页的目的)。
· 基本资料:用户名/显示名/角色/状态/创建时间/最后登录。
· 权限范围:可调用 Agent / 可访问目录(非管理员;空数组 = 不限)。
· 修改密码:三输入框 + 不一致就地提示(新增 `AuthApi.changePassword`,
该端点服务端一直有、客户端从未包)。
· 客户端连接密钥:列表 / 新建 / 吊销 + 一次性全文提示
(`AuthApi` 的三个方法终于被调用)。
· 退出登录:先注销推送 token → 清凭证 → **清全部账号** → 回登录页。
· 整页改为**一个 Scroll**:原来账号列表 `layoutWeight(1)` 占满剩余高度,
排在它后面的 section 被挤出可视区且滚不到(WebUI 注释里正是这个坑),
而「退出登录」在最下面 ⇒ 等于退不出去。
── 顺带修掉的两个线上 bug(都是判据发现的)──
① `MeApi.get()` 调 `GET /me`,但服务端**没有**这个路由(只有 `/auth/me`;
`/me/*` 下是 mail/sessions/keys/appearance 子资源)。
后果:这把调用恒 404 → `loadRole()` 恒走 catch → `isAdmin` 恒 false
→ 「管理」入口对**包括管理员在内**的所有人永远不显示。
实证:`GET /api/v1/me` → 404;`GET /api/v1/auth/me` → 200
`{"user":{"role":"admin",...}}`。修复后截图里「管理」入口已出现。
② 详情页回复写 `req.to = this.fromName + '@'`:那个游离的 `@` 让 `name@`
被后端 `ParseAddress` 解析成「有 path、无 session」⇒ 落到该 Agent 的
**默认会话**,而不是用户正在看的那条线索。改用 `replyTargetAddress()`。
── 判据(新增 7 + 8 条,全部含变异自检)──
· `harmony-reply-target.test.mjs`(新):直接执行 `ReplyTarget.ts` 断言行为
—— 空 path 必须留 `@`(否则整串被当成名字、投递 404)、人只有名字、
会话位必须带上、`from_workspace` 不得用来拼地址(会得到 `dsh@dsh`);
并读 Go 源码比对三分支结构。
· `harmony-admin.test.mjs`(+5):`/auth/me` 路径(含服务端注册与响应形状)、
八个 section 齐全、退出清全部账号、整页一个滚动容器、字段来自服务端。
变异自检:把 `/auth/me` 改回 `/me` → 判据变红(已验证)。
测试:212 passed / 0 failed;`devecocli build` 通过;
模拟器截图逐项核对(详情收起/展开两态、我的页全部 section)。
未验:真机观感;横屏 Auto Split 双栏。
This commit is contained in:
@ -9,9 +9,37 @@ 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 } from '../model/Models';
|
||||
import { MailDetail, SendMailRequest, Address } from '../model/Models';
|
||||
import { MailDetailParams } from '../model/RouteParams';
|
||||
import { AmIcon } from '../common/Icons';
|
||||
import { permissionLabel } from '../model/MailGrouping';
|
||||
import {
|
||||
participantAddress,
|
||||
mailReplyTarget,
|
||||
formatAddress
|
||||
} from '../model/ReplyTarget';
|
||||
|
||||
/**
|
||||
* 本地时间:与 WebUI `new Date(x).toLocaleString('zh-CN')` 同一口径。
|
||||
*
|
||||
* 原先详情页直接把 `mail.created_at`(ISO 8601,如 `2026-09-15T03:37:07.14758Z`)
|
||||
* 吐到界面上 —— 用户读到的是 UTC 串,既不好读也不是本地时间。
|
||||
* 与列表页的 `compactMailTime()` 分开:列表要短(`09/15 11:37`),
|
||||
* 详情要完整(含秒与年月日)。两处都走本地时区,不用 ISO 原文。
|
||||
*/
|
||||
function localDateTime(iso: string): string {
|
||||
const value: Date = new Date(iso);
|
||||
if (Number.isNaN(value.getTime())) {
|
||||
return '';
|
||||
}
|
||||
const y: number = value.getFullYear();
|
||||
const mo: string = (value.getMonth() + 1).toString().padStart(2, '0');
|
||||
const d: string = value.getDate().toString().padStart(2, '0');
|
||||
const h: string = value.getHours().toString().padStart(2, '0');
|
||||
const mi: string = value.getMinutes().toString().padStart(2, '0');
|
||||
const s: string = value.getSeconds().toString().padStart(2, '0');
|
||||
return y + '/' + mo + '/' + d + ' ' + h + ':' + mi + ':' + s;
|
||||
}
|
||||
|
||||
@Component
|
||||
export struct MailDetailView {
|
||||
@ -28,6 +56,13 @@ export struct MailDetailView {
|
||||
@State subject: string = '';
|
||||
@State fromName: string = '';
|
||||
@State toName: string = '';
|
||||
@State fromHuman: boolean = false;
|
||||
@State toHuman: boolean = false;
|
||||
@State sessionWorkspace: string = '';
|
||||
@State ccList: Address[] = [];
|
||||
@State mailType: string = '';
|
||||
/** 已读状态:`unread` 时头部常驻一个「未读」点(与 WebUI 同一位置与语义) */
|
||||
@State status: string = '';
|
||||
@State body: string = '';
|
||||
@State createdAt: string = '';
|
||||
@State permissionMode: string = '';
|
||||
@ -39,6 +74,18 @@ export struct MailDetailView {
|
||||
@State sending: boolean = false;
|
||||
@State sessionId: string = '';
|
||||
@State switchingPerm: boolean = false;
|
||||
/**
|
||||
* 头部**默认收起**,点标题行展开。
|
||||
*
|
||||
* 与 WebUI `CollapsibleHeader` 同一策略,理由也是同一条:
|
||||
* 「顶部邮件信息 + 底部输入框同时常驻会把可读区压成一条缝」——
|
||||
* 邮件的用途是读,头部信息不是每时每刻都要用的。
|
||||
* WebUI 实测 1280×800 下头部占 17%、回复框占 31%,留给正文只剩 48%,
|
||||
* 而手机竖屏比那更窄,这条更需要。
|
||||
*/
|
||||
@State headerOpen: boolean = false;
|
||||
/** 当前登录用户名 —— 决定「这封是不是我发的」,进而决定回给谁(见 ReplyTarget) */
|
||||
@State me: string = '';
|
||||
|
||||
private mailApi: MailApi | null = null;
|
||||
|
||||
@ -69,6 +116,13 @@ export struct MailDetailView {
|
||||
return;
|
||||
}
|
||||
this.accountId = account.id;
|
||||
/*
|
||||
* 当前登录用户名:决定「这封是不是我发的」,进而决定回给谁。
|
||||
* 与 WebUI 的 `useAuthStore(s => s.user?.username)` 同一口径 ——
|
||||
* 写死 'human' 是多用户之前的遗留(登录名可能是 jianf,判据恒为假),
|
||||
* 那会让「自己发的信」也算成别人发的,回复时回给自己。
|
||||
*/
|
||||
this.me = account.username;
|
||||
const accountClient: ApiClient = new ApiClient(ctx);
|
||||
accountClient.setBase(account.server);
|
||||
accountClient.setToken(account.token);
|
||||
@ -94,6 +148,12 @@ export struct MailDetailView {
|
||||
this.subject = mail.subject;
|
||||
this.fromName = mail.from_name;
|
||||
this.toName = mail.to_name;
|
||||
this.fromHuman = mail.from_human;
|
||||
this.toHuman = mail.to_human;
|
||||
this.sessionWorkspace = mail.session_workspace;
|
||||
this.ccList = mail.cc_list;
|
||||
this.mailType = mail.mail_type;
|
||||
this.status = mail.status;
|
||||
this.body = mail.body;
|
||||
this.createdAt = mail.created_at;
|
||||
this.permissionMode = mail.permission_mode;
|
||||
@ -134,28 +194,177 @@ export struct MailDetailView {
|
||||
this.getUIContext().getRouter().back();
|
||||
}
|
||||
|
||||
/**
|
||||
* 发件方在这条会话里的**完整地址**(`name@path.session`)。
|
||||
*
|
||||
* 与 WebUI `Header` 的 `participantAddress(mail.from_name, mail.from_human, ws, alias)`
|
||||
* 逐字对齐:人只有名字(不分段也不带会话位),Agent 要三段。
|
||||
* workspace 取**会话的** `session_workspace`,不取 `from_workspace`
|
||||
* (后者对 Agent 存的是 Agent 名,拿它拼会得到 `dsh@dsh`)。
|
||||
*/
|
||||
fromAddress(): string {
|
||||
return participantAddress(
|
||||
this.fromName, this.fromHuman, this.sessionWorkspace, this.sessionAlias
|
||||
);
|
||||
}
|
||||
|
||||
/** 收件方在这条会话里的完整地址 */
|
||||
toAddress(): string {
|
||||
return participantAddress(
|
||||
this.toName, this.toHuman, this.sessionWorkspace, this.sessionAlias
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 抄送行文案:优先用 `raw`(用户输入的原文)。
|
||||
*
|
||||
* 不用拼回来的字符串替:`.new` 这类**原始意图**在 `raw` 里,
|
||||
* 换成真实别名就把「他要新建一条线索」这件事抹掉了。
|
||||
*/
|
||||
ccText(): string {
|
||||
const parts: string[] = [];
|
||||
for (let i = 0; i < this.ccList.length; i++) {
|
||||
const a: Address = this.ccList[i];
|
||||
const shown: string = a.raw.length > 0
|
||||
? a.raw
|
||||
: formatAddress(a.name, a.path, a.session);
|
||||
if (shown.length > 0) {
|
||||
parts.push(shown);
|
||||
}
|
||||
}
|
||||
return parts.join('、');
|
||||
}
|
||||
|
||||
/**
|
||||
* 单封邮件视图的回复目标地址。
|
||||
*
|
||||
* **这是发信真正用的地址**,不是显示用的。原先是 `this.fromName + '@'` ——
|
||||
* 那个游离的 `@` 让 `name@` 被解析成「有 path、无 session」,落到该 Agent 的
|
||||
* **默认会话**,而不是用户正在看的这条线索。
|
||||
*/
|
||||
replyTargetAddress(): string {
|
||||
return mailReplyTarget(
|
||||
this.fromName, this.fromHuman, this.toName, this.toHuman,
|
||||
this.sessionWorkspace, this.sessionAlias, this.me
|
||||
);
|
||||
}
|
||||
|
||||
/** 详情里的一行「标签 + 值」(对应 WebUI `Header` 的 `<Row>`) */
|
||||
@Builder
|
||||
MetaRow(label: string, value: string) {
|
||||
Row() {
|
||||
Text(label)
|
||||
.fontSize(11).fontColor(Theme.textSubtle)
|
||||
.width(36).flexShrink(0)
|
||||
Text(value)
|
||||
.fontSize(12).fontColor(Theme.textMuted)
|
||||
.layoutWeight(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
}
|
||||
.width('100%')
|
||||
.margin({ bottom: 2 })
|
||||
.alignItems(VerticalAlign.Top)
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限档位行:**中文标签**,不是英文 `plan/workspace/full`。
|
||||
*
|
||||
* `permissionLabel()` 早就在 `MailGrouping.ts` 里了(列表页也用的它),
|
||||
* 而详情页原来直接吐英文 mode —— 同一个概念在同一个 App 里两种写法。
|
||||
* 标签与 WebUI `MODE_LABEL` 逐字一致(只读 / 目录内 / 全权),可点可改。
|
||||
*/
|
||||
@Builder
|
||||
PermissionRow() {
|
||||
Row() {
|
||||
Text('权限')
|
||||
.fontSize(11).fontColor(Theme.textSubtle)
|
||||
.width(36).flexShrink(0)
|
||||
if (this.switchingPerm) {
|
||||
LoadingProgress().width(16).height(16)
|
||||
} else {
|
||||
ForEach(['plan', 'workspace', 'full'], (mode: string) => {
|
||||
Text(permissionLabel(mode))
|
||||
.fontSize(11)
|
||||
.fontColor(this.permissionMode === mode ? Theme.surface : Theme.textMuted)
|
||||
.backgroundColor(this.permissionMode === mode ? Theme.accent : Theme.chipNeutralBg)
|
||||
.borderRadius(4)
|
||||
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
|
||||
.margin({ right: 6 })
|
||||
.onClick(() => { this.switchPermission(mode); })
|
||||
}, (mode: string) => mode)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
// 顶栏
|
||||
Row() {
|
||||
Text('‹')
|
||||
.fontSize(24).fontColor(Theme.accent)
|
||||
.width(40).height(40)
|
||||
.textAlign(TextAlign.Center)
|
||||
.onClick(() => {
|
||||
this.goBack();
|
||||
})
|
||||
Text(this.subject.length > 20 ? this.subject.substring(0, 20) + '…' : this.subject)
|
||||
.fontSize(16).fontWeight(FontWeight.Bold).fontColor(Theme.textPrimary)
|
||||
/*
|
||||
* ── 可折叠头部(与 WebUI `CollapsibleHeader` 同构)──
|
||||
*
|
||||
* 收起态只留一行:返回键 + 标题 + 必须常驻的状态点(未读 / 权限请求)+ 展开箭头。
|
||||
* 点这一行展开收发件人、时间、抄送、档位与操作行。
|
||||
* 返回键**不能**套在展开点击里(button 嵌 button)—— 与 WebUI 同一取舍。
|
||||
*/
|
||||
Column() {
|
||||
Row() {
|
||||
Text('‹')
|
||||
.fontSize(24).fontColor(Theme.accent)
|
||||
.width(36).height(36)
|
||||
.textAlign(TextAlign.Center)
|
||||
.onClick(() => { this.goBack(); })
|
||||
|
||||
Row() {
|
||||
Text(this.subject)
|
||||
.fontSize(14).fontWeight(FontWeight.Bold).fontColor(Theme.textPrimary)
|
||||
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.layoutWeight(1)
|
||||
|
||||
/* 状态点:收起态也要看得见(与 WebUI 的 meta 同一位置与语义) */
|
||||
if (this.status === 'unread') {
|
||||
Text('未读')
|
||||
.fontSize(10).fontColor(Theme.accent)
|
||||
.backgroundColor(Theme.accentSoft).borderRadius(4)
|
||||
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
|
||||
.margin({ left: 6 })
|
||||
}
|
||||
if (this.mailType === 'permission_request') {
|
||||
Text('权限请求')
|
||||
.fontSize(10).fontColor(Theme.warnFg)
|
||||
.backgroundColor(Theme.warnBg).borderRadius(4)
|
||||
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
|
||||
.margin({ left: 6 })
|
||||
}
|
||||
|
||||
AmIcon({ iconName: 'chevronRight', iconSize: 16, iconColor: Theme.textSubtle })
|
||||
.rotate({ angle: this.headerOpen ? 90 : 0 })
|
||||
.margin({ left: 6 })
|
||||
}
|
||||
.layoutWeight(1)
|
||||
Text(this.permissionMode)
|
||||
.fontSize(11).fontColor(Theme.permFg(this.permissionMode))
|
||||
.backgroundColor(Theme.permBg(this.permissionMode))
|
||||
.borderRadius(4).padding({ left: 6, right: 6, top: 2, bottom: 2 })
|
||||
.onClick(() => { this.headerOpen = !this.headerOpen; })
|
||||
}
|
||||
.width('100%')
|
||||
|
||||
if (this.headerOpen) {
|
||||
Column() {
|
||||
this.MetaRow('发件', this.fromAddress())
|
||||
this.MetaRow('收件', this.toAddress())
|
||||
if (this.ccList.length > 0) {
|
||||
this.MetaRow('抄送', this.ccText())
|
||||
}
|
||||
this.MetaRow('时间', localDateTime(this.createdAt))
|
||||
this.PermissionRow()
|
||||
}
|
||||
.width('100%')
|
||||
.margin({ top: 8 })
|
||||
}
|
||||
}
|
||||
.width('100%').height(56)
|
||||
.padding({ left: 8, right: 12 })
|
||||
.width('100%')
|
||||
.padding({ left: 8, right: 12, top: 8, bottom: 8 })
|
||||
.backgroundColor(Theme.surface)
|
||||
.onClick(() => {})
|
||||
|
||||
Divider().color(Theme.border)
|
||||
|
||||
if (this.loading) {
|
||||
Column() {
|
||||
@ -172,107 +381,62 @@ export struct MailDetailView {
|
||||
.width('100%').layoutWeight(1)
|
||||
.justifyContent(FlexAlign.Center)
|
||||
} else {
|
||||
Scroll() {
|
||||
Column() {
|
||||
// 元信息卡片
|
||||
Stack({ alignContent: Alignment.BottomEnd }) {
|
||||
Scroll() {
|
||||
Column() {
|
||||
Row() {
|
||||
Text('发件人').fontSize(12).fontColor(Theme.textSubtle).width(60)
|
||||
Text(this.fromName).fontSize(14).fontColor(Theme.textPrimary)
|
||||
}.width('100%').margin({ bottom: 6 })
|
||||
/*
|
||||
* 元信息**不在正文区**了:它已经上移到可折叠头部(用户展开才显示)。
|
||||
* 这里只留正文:主题 + Markdown 正文。
|
||||
* 原来那张常驻元信息卡把可读区压掉一块,而那正是用户抱怨的事。
|
||||
*/
|
||||
Text(this.subject)
|
||||
.fontSize(20)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor(Theme.textPrimary)
|
||||
.width('100%')
|
||||
.padding({ left: 16, right: 16, top: 16, bottom: 8 })
|
||||
|
||||
Row() {
|
||||
Text('收件人').fontSize(12).fontColor(Theme.textSubtle).width(60)
|
||||
Text(this.toName).fontSize(14).fontColor(Theme.textPrimary)
|
||||
}.width('100%').margin({ bottom: 6 })
|
||||
/*
|
||||
* 邮件正文:**用 Markdown 渲染**,不再直接吐原文。
|
||||
*
|
||||
* 2026-09-15 之前这里是 `Text(this.body)` —— 正文是 Markdown,
|
||||
* 于是用户看到的是 `**加粗**`、`# 标题`、`| 表 |` 的**字面量**。
|
||||
* WebUI 侧一直用 `react-markdown` + `remark-gfm` 渲染
|
||||
*(`MailView.tsx` 的 `.markdown` 容器),这是两端**功能不对等**的一处硬缺口。
|
||||
*
|
||||
* 用第三方库 `@luvi/lv-markdown-in`(鸿蒙原生 ArkTS 渲染引擎,
|
||||
* **不依赖 WebView**):解析与渲染全在原生层,`ohpm install` 一行接入。
|
||||
*/
|
||||
Markdown({ text: this.body })
|
||||
.width('100%')
|
||||
.padding({ left: 16, right: 16, bottom: 16 })
|
||||
|
||||
if (this.sessionAlias.length > 0) {
|
||||
Row() {
|
||||
Text('会话').fontSize(12).fontColor(Theme.textSubtle).width(60)
|
||||
Text(this.sessionAlias).fontSize(13).fontColor(Theme.accent)
|
||||
}.width('100%').margin({ bottom: 6 })
|
||||
}
|
||||
|
||||
Row() {
|
||||
Text('时间').fontSize(12).fontColor(Theme.textSubtle).width(60)
|
||||
Text(this.createdAt).fontSize(13).fontColor(Theme.textMuted)
|
||||
}.width('100%').margin({ bottom: 6 })
|
||||
|
||||
Row() {
|
||||
Text('权限').fontSize(12).fontColor(Theme.textSubtle).width(60)
|
||||
if (this.switchingPerm) {
|
||||
LoadingProgress().width(16).height(16)
|
||||
} else {
|
||||
ForEach(['plan', 'workspace', 'full'], (mode: string) => {
|
||||
Text(mode)
|
||||
.fontSize(11)
|
||||
.fontColor(this.permissionMode === mode ? Theme.surface : Theme.textMuted)
|
||||
.backgroundColor(this.permissionMode === mode ? Theme.accent : Theme.border)
|
||||
.borderRadius(4)
|
||||
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
|
||||
.margin({ right: 6 })
|
||||
.onClick(() => { this.switchPermission(mode); })
|
||||
}, (mode: string) => mode)
|
||||
}
|
||||
}.width('100%')
|
||||
/* 让出回复球的高度:球是浮在正文之上的,不让出最后一段会压在球底下 */
|
||||
Blank().height(80)
|
||||
}
|
||||
.width('100%')
|
||||
.padding(16)
|
||||
.backgroundColor(Theme.pageBg)
|
||||
.borderRadius(8)
|
||||
.margin({ left: 12, right: 12, top: 8 })
|
||||
|
||||
// 邮件正文
|
||||
Text(this.subject)
|
||||
.fontSize(20)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor(Theme.textPrimary)
|
||||
.width('100%')
|
||||
.padding({ left: 16, right: 16, top: 16, bottom: 8 })
|
||||
|
||||
/*
|
||||
* 邮件正文:**用 Markdown 渲染**,不再直接吐原文。
|
||||
*
|
||||
* 2026-09-15 之前这里是 `Text(this.body)` —— 正文是 Markdown,
|
||||
* 于是用户看到的是 `**加粗**`、`# 标题`、`| 表 |` 的**字面量**。
|
||||
* WebUI 侧一直用 `react-markdown` + `remark-gfm` 渲染
|
||||
*(`MailView.tsx` 的 `.markdown` 容器),这是两端**功能不对等**的一处硬缺口。
|
||||
*
|
||||
* 用第三方库 `@luvi/lv-markdown-in`(鸿蒙原生 ArkTS 渲染引擎,
|
||||
* **不依赖 WebView**):解析与渲染全在原生层,`ohpm install` 一行接入。
|
||||
* 我一度自己写了一个 GFM 子集解析器 —— 随后按用户指示换成第三方库:
|
||||
* 自研那份只覆盖子集、还要自己养判据,而库里有 LaTeX / Prism 高亮 /
|
||||
* 大文本懒渲染 / 70+ 样式 API,都是自研要重做的。
|
||||
*/
|
||||
Markdown({ text: this.body })
|
||||
.width('100%')
|
||||
.padding({ left: 16, right: 16, bottom: 16 })
|
||||
|
||||
Blank().height(60)
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.scrollBar(BarState.Off)
|
||||
.width('100%').height('100%')
|
||||
.scrollBar(BarState.Off)
|
||||
|
||||
// 底部回复按钮
|
||||
Row() {
|
||||
Button() {
|
||||
Row() {
|
||||
AmIcon({ iconName: 'forward', iconSize: 16, iconColor: Theme.accentFg })
|
||||
Text('回复').fontSize(15).fontColor(Theme.accentFg).margin({ left: 6 })
|
||||
}
|
||||
}
|
||||
.width('90%').height(44)
|
||||
/*
|
||||
* 回复入口:**右下角悬浮球**,与 WebUI 的 `reply-fab` 同形同位置。
|
||||
*
|
||||
* 原先是底部通栏按钮(占 56vp 常驻)—— WebUI 的注释把理由写得很清楚:
|
||||
* 「顶部邮件信息 + 底部输入框同时常驻会把可读区压成一条缝」,
|
||||
* 实测 1280×800 下回复框占 31%;输入框不是阅读时每刻都要用的东西,
|
||||
* 就该按需展开。球同时与列表页的 ComposeFab 同一套观感,不新增一种视觉语言。
|
||||
*/
|
||||
Button() {
|
||||
AmIcon({ iconName: 'chatBubble', iconSize: 24, iconColor: Theme.accentFg })
|
||||
}
|
||||
.width(56).height(56)
|
||||
.borderRadius(28)
|
||||
.backgroundColor(Theme.accent)
|
||||
.fontSize(15)
|
||||
.onClick(() => {
|
||||
this.showReplyBox = true;
|
||||
})
|
||||
.margin({ right: 16, bottom: 16 })
|
||||
.onClick(() => { this.showReplyBox = true; })
|
||||
}
|
||||
.width('100%').height(56)
|
||||
.justifyContent(FlexAlign.Center)
|
||||
.backgroundColor(Theme.surface)
|
||||
.width('100%').layoutWeight(1)
|
||||
|
||||
// 回复弹层
|
||||
if (this.showReplyBox) {
|
||||
@ -285,8 +449,9 @@ export struct MailDetailView {
|
||||
|
||||
// 回复框
|
||||
Column() {
|
||||
Text('回复给 ' + this.fromName)
|
||||
.fontSize(16).fontWeight(FontWeight.Bold).fontColor(Theme.textPrimary)
|
||||
Text('回复给 ' + this.replyTargetAddress())
|
||||
.fontSize(14).fontWeight(FontWeight.Bold).fontColor(Theme.textPrimary)
|
||||
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.margin({ bottom: 12 })
|
||||
|
||||
TextArea({ placeholder: '输入回复内容…' })
|
||||
@ -337,7 +502,15 @@ export struct MailDetailView {
|
||||
this.sending = true;
|
||||
try {
|
||||
const req: SendMailRequest = new SendMailRequest();
|
||||
req.to = this.fromName + '@';
|
||||
/*
|
||||
* ★ 用 `replyTargetAddress()`,**不是** `this.fromName + '@'`。
|
||||
*
|
||||
* 原先那个游离的 `@` 让 `name@` 被后端 `ParseAddress` 解析成
|
||||
* 「有 path、无 session」⇒ 落到该 Agent 的**默认会话**,而不是用户
|
||||
* 正在看的这条线索 —— 回复会跑到另一条任务里去。
|
||||
* 正确形式是 `name@path.session`(人则只有名字),会话位必须带上。
|
||||
*/
|
||||
req.to = this.replyTargetAddress();
|
||||
req.subject = 'Re: ' + this.subject;
|
||||
req.body = this.replyBody;
|
||||
req.reply_to = this.mailId;
|
||||
|
||||
Reference in New Issue
Block a user