用户:「收件发件授权改为一个导航项,通过内部导航区分,然后新建作为他们内部的一个悬浮的圆形加号」。 所以这一期不是"再加两个页面",而是对齐信息架构。 ## 鸿蒙侧 - 底部第一项 **收件箱 → 通信**(`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)。 **未验**:页签/徽标/悬浮加号在真机上的观感与点击 —— 需设备或模拟器(模拟器要人在命令行启动)。
150 lines
5.7 KiB
TypeScript
150 lines
5.7 KiB
TypeScript
/*
|
||
* 「通信」页的**内部导航**:收件箱 / 发件箱 / 授权 —— 纯逻辑,无 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;
|
||
}
|