/* * 「通信」页的**内部导航**:收件箱 / 发件箱 / 授权 —— 纯逻辑,无 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; }