Files
MailUI4Agents/client/harmony/entry/src/main/ets/model/CommTabs.ts
JianFeeeee 73f886aea5 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)。
**未验**:页签/徽标/悬浮加号在真机上的观感与点击 —— 需设备或模拟器(模拟器要人在命令行启动)。
2026-09-14 14:13:45 +08:00

150 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* 「通信」页的**内部导航**:收件箱 / 发件箱 / 授权 —— 纯逻辑,无 UI 依赖。
*
* 用户2026-09-14「收件发件授权改为一个导航项通过内部导航区分然后新建作为
* 他们内部的一个悬浮的圆形加号」。于是三个页签落在**通信页内部**,而不是底部导航栏里。
*
* 为什么单独成文件(与 `MailGrouping.ts` 同一个理由):
* 「点页签 → 落在哪个 pane」「徽标显示几、什么颜色」这两件事是**判据的对象**。
* 写在 `build()` 里,判据只能 grep 源码字符串;放在这里,判据可以跑**同一份代码**
* —— `client/electron/test/harmony-logic.test.mjs` 直接执行本文件,断言的是行为:
* 未知页签落到哪、越界索引落到哪、徽标 0 时是不是真的不显示、99 以上怎么写。
*
* 对应关系(参考实现都是 WebUI改那边时这边要跟着核
* - 页签键与顺序:`src/stores/uiStore.ts` 的 `CommTab` + `components/CommTabs.tsx` 的 `TABS`
* - 徽标颜色:`CommTabs.tsx`(收件箱红 `bg-red-600`、授权橙 `bg-orange-700`、发件箱无徽标)
* - 徽标上限:`badge > 99 ? '99+' : badge`
*
* ⚠️ 本文件必须保持**类型可擦除**:不用 `enum`、`namespace`、构造器参数属性,
* 否则 node 的 strip-types 跑不起来,判据就断了。
*/
/** 三个页签的键,顺序即页签顺序(与 WebUI 的 `CommTab` 联合类型逐字一致) */
export const COMM_TABS: string[] = ['inbox', 'sent', 'permissions'];
/** 默认页签进通信页先看收件箱WebUI 的 `uiStore` 初值也是 'inbox')。 */
export const COMM_TAB_DEFAULT: string = 'inbox';
export function isCommTab(key: string): boolean {
return COMM_TABS.indexOf(key) >= 0;
}
/**
* 认不出的页签键 → 回到默认页签。
*
* 保守方向的选择:宁可落在收件箱,也不要落在一个空白 pane 上
* (旧数据、拼错的键、以后改过名字的键,都会走到这里)。
*/
export function normalizeCommTab(key: string): string {
return isCommTab(key) ? key : COMM_TAB_DEFAULT;
}
/** 页签中文名(与 WebUI `CommTabs.tsx` 的 label 逐字一致) */
export function commTabLabel(key: string): string {
if (key === 'sent') {
return '发件箱';
}
if (key === 'permissions') {
return '授权';
}
return '收件箱';
}
/** 页签键 → 下标(页签栏用下标渲染;认不出的键给默认页签的下标) */
export function commTabIndex(key: string): number {
const i: number = COMM_TABS.indexOf(normalizeCommTab(key));
return i < 0 ? 0 : i;
}
/**
* 下标 → 页签键(越界、非整数都回到默认页签,不抛错:这是 UI 状态,不是错误)。
*
* `Number.isInteger` 这一条是**判据逼出来的**:早先只判了范围,
* 于是 `1.5` 会走到 `COMM_TABS[1.5]` → `undefined` —— 页签栏拿到一个不存在的键,
* 表现是"点哪都不亮"。范围检查挡不住非整数,必须显式判。
*/
export function commTabFromIndex(index: number): string {
if (!Number.isInteger(index) || index < 0 || index >= COMM_TABS.length) {
return COMM_TAB_DEFAULT;
}
return COMM_TABS[index];
}
/**
* 页签徽标数字:收件箱看未读,授权看**待决策**(不是权限邮件总数 ——
* 决策过的那些不该继续喊人),发件箱没有徽标。
*
* 负数当 0计数来自网络响应不假设它一定干净。
*/
export function badgeCount(key: string, unread: number, pending: number): number {
const tab: string = normalizeCommTab(key);
if (tab === 'inbox') {
return unread > 0 ? unread : 0;
}
if (tab === 'permissions') {
return pending > 0 ? pending : 0;
}
return 0;
}
/** 徽标文字0 → 空串(页面据此不渲染),>99 → '99+'(与 WebUI 同一写法) */
export function badgeText(n: number): string {
if (n <= 0) {
return '';
}
if (n > 99) {
return '99+';
}
return n + '';
}
/**
* 徽标色调:收件箱红(未读=要读)、授权橙(待决策=有人被卡住,更急)、发件箱无。
*
* 返回的是**语义名**而不是色值:颜色由 `Theme` 决定(页面不再自己挑颜色)。
* 名字与 WebUI 的类名对应red → danger、orange → warn。
*/
export function badgeTone(key: string): string {
const tab: string = normalizeCommTab(key);
if (tab === 'inbox') {
return 'danger';
}
if (tab === 'permissions') {
return 'warn';
}
return 'none';
}
/**
* 空态说明(每个页签一句)。
*
* 收件箱/发件箱的**主句**与 WebUI 的 `MailList.tsx` 逐字一致(「暂无邮件」)——
* 两边对同一件事说同一句话;各页签再补一句"为什么是空的"
* 因为鸿蒙这边没有 WebUI 那种"列表面板 + 右侧详情"的上下文,
* 一句光秃秃的「暂无邮件」说不清是"没有信"还是"这栏本来就不放东西"。
*/
export function emptyTitle(key: string): string {
return '暂无邮件';
}
export function emptyHint(key: string): string {
const tab: string = normalizeCommTab(key);
if (tab === 'sent') {
return '你发出的信会出现在这里';
}
if (tab === 'permissions') {
return '没有待决策的请求 —— 没有被卡住的 Agent';
}
return '还没有收到新邮件';
}
/**
* 页签栏上"授权"那一栏要不要**催**(橙色徽标)。
*
* 与未读分开判断:未读是"有东西要读",待决策是"有人被卡住"。后者更急,
* 所以即使收件箱未读为 0只要有待决策请求通信这一项在底部导航上也该有提示。
*/
export function commNeedsAttention(unread: number, pending: number): boolean {
return pending > 0 || unread > 0;
}