feat(harmony): P2b+P3 —— 「收件箱」改成「通信」(内部三栏 + 徽标 + 悬浮加号),发件箱与授权栏落地

用户:「收件发件授权改为一个导航项,通过内部导航区分,然后新建作为他们内部的一个悬浮的圆形加号」。
所以这一期不是"再加两个页面",而是对齐信息架构。

## 鸿蒙侧

- 底部第一项 **收件箱 → 通信**(`CommPage`),内部三栏 收件箱 / 发件箱 / 授权;
  页签下划线式(不是浮动白胶囊 —— WebUI 侧用户原话「通信页面的二级页面与其他位置极其割裂」)。
- **徽标**:收件箱红(未读)、授权橙(**待决策**)、发件箱无;0 不显示,>99 写 `99+`。
  合并成一个导航项后,底部看不到"授权有 3 个在等我",这个信息不能丢 —— 它比未读更急。
- **悬浮圆形加号**挂到通信页这一层(三个栏都要能新建);`⚙` 也搬上来(否则切栏就够不到设置)。
- **收件箱不再混权限邮件**(`splitByPermission`),未读按筛后算 ——
  WebUI 实测过"一个会话 17 封权限邮件挤掉另外两个会话"。
- **发件箱**:`GET /me/mail/sent`,与收件箱同构(同一套折叠/行),行上主角是**收件人**;
  空态有说明(主句与 WebUI 逐字一致「暂无邮件」+ 一句"这里放什么")。
- **授权栏**(P3 主体):`GET /permission/pending`(不从收件箱筛)+ `POST /permission/decide`。
  拒绝**可填备注且备注真的送出**;请求**过期**时当场说清「审批不会让那次调用继续」。
- 顺手删掉死代码:收件箱里「写邮件」的 `bindSheet`(`composeVisible` 从未置 true,谁也打不开)。

## 判据(harmony-logic 19 → 28 条)

页签键/顺序从 WebUI 源码抽取比对(`uiStore.ts` 的 `CommTab` + `CommTabs.tsx` 的 `TABS`);
页签状态机(键↔下标往返、脏键/越界/非整数 → 回收件箱);徽标规则(数字来源、0 不显示、
99+ 上限、红/橙与 WebUI 类名对应);分家语义(决策过的不再算待决策、空串与 null 同义);
三栏空态互不相同且主句与 WebUI 一致;接线(三个 pane 真的渲染、加号是圆形且在通信页、
"先分家再折叠"、接口路径与决策体三字段、拒绝传备注、过期分支)。

判据抓到一个**真 bug**:`commTabFromIndex` 只判范围,`1.5` → `COMM_TABS[1.5]` = `undefined`
(表现"点哪都不亮")。已加 `Number.isInteger`。

变异验证(六种):授权徽标看未读 / 权限邮件不分出去 / 类型名拼错 / 决策过仍算待决策 /
收件箱不分家 / 发件箱空态去掉说明 —— 全部判红。

## 排序说明

**日历暂时没有入口**:P6 的内容(网格 + 事件读写 + 滑动翻页)还没做,
先放一个点进去空着的入口比暂时没有更糟 —— 有意排序,记在 §7.15 以免被当成漏做。

## 验证 / 未验

`hvigorw assembleHap` BUILD SUCCESSFUL;`npm test` 退出码 0(10 个判据文件全绿 + vitest 258/258)。
**未验**:页签/徽标/悬浮加号在真机上的观感与点击 —— 需设备或模拟器(模拟器要人在命令行启动)。
This commit is contained in:
2026-09-14 14:13:45 +08:00
parent c6aaf8c468
commit 73f886aea5
7 changed files with 1196 additions and 65 deletions

View File

@ -6,7 +6,7 @@
*/
import { ApiClient } from './ApiClient';
import { MailSummary, Session, Contact, MailDetail, ThreadResponse, AttachmentInfo, SendMailRequest, SendMailResult } from '../model/Models';
import { MailSummary, Session, Contact, MailDetail, ThreadResponse, AttachmentInfo, SendMailRequest, SendMailResult, SentResponse, PermissionRequest, PendingResponse, DecideResponse } from '../model/Models';
/** 收件箱响应 */
export class InboxResponse {
@ -67,6 +67,14 @@ export class BudgetPayload {
max_rounds: number = 0;
}
/** 决策请求体(`POST /permission/decide`) */
export class DecidePayload {
mail_id: string = '';
/** allow | deny */
decision: string = '';
note: string = '';
}
export class MailApi {
private client: ApiClient;
@ -80,6 +88,39 @@ export class MailApi {
return this.client.get<InboxResponse>('/me/mail/inbox', query);
}
/**
* 发件箱。响应形状与收件箱同构(`{mails}`)—— 但**没有** `total` 那个坑
* (收件箱的 `total` 是未读数,见 docs/API.md 的「同名不同义」),这里也不读它。
*/
async sent(): Promise<SentResponse> {
return this.client.get<SentResponse>('/me/mail/sent');
}
/**
* 待决策的权限请求。
*
* **不从收件箱里筛 `permission_*`**:收件箱默认只取 50 封,而一个会话能连着产生
* 十几封权限邮件,从收件箱筛会把别的信挤出视野。与 WebUI 同源(`/permission/pending`)。
*/
async pendingPermissions(): Promise<PendingResponse> {
return this.client.get<PendingResponse>('/permission/pending');
}
/**
* 决策(同意/拒绝)。
*
* `note` 必须**真的送达模型** —— 这是 WebUI 侧踩过的坑(`gateway/handler/permission.go`
* 的 Note 传递)。所以这里不做任何"为空就不传"的省略:空串也照传,
* 由服务端决定怎么处理,避免"界面看起来能填备注、其实备注没发出去"。
*/
async decidePermission(mailId: string, decision: string, note: string): Promise<DecideResponse> {
const payload: DecidePayload = new DecidePayload();
payload.mail_id = mailId;
payload.decision = decision;
payload.note = note;
return this.client.post<DecideResponse>('/permission/decide', payload);
}
/** 会话列表 */
async sessions(): Promise<SessionListResponse> {
return this.client.get<SessionListResponse>('/me/sessions');