跨端: 导航项徽标两侧补齐(我上次"撤回"错了 —— WebUI 是有的)

上一笔我凭"两侧导航都没有徽标"把刚写好的徽标**撤回**了。那是错的:
`client/electron/src/components/Sidebar.tsx:88-95,111-125` 明确有——

```
const badge = isComm ? unread + pendingPerms
                      : modes.includes('contacts') ? contacts.length : 0;
const badgeTone = isComm && pendingPerms > 0 ? 'perm' : isComm ? 'unread' : 'plain';
```

并且是 `absolute top-0.5 right-1` 压在导航项右上角,`>99` 显示 `99+`。
我当时只看了底栏 `NavItem`(那里确实没有),就把结论推到了"两侧都没有"。

## 补的东西

- 新增**纯逻辑** `model/NavItems.ts` 的 `navBadgeCount` / `navBadgeTone` / `navBadgeText`,
  逐条对齐 WebUI 的口径:
  · **通信** = 未读 + 待决策(两类"要动手"合起来);
  · **联系人** = 联系人数;日历/「我的」= 0(没有徽标);
  · 色调:待决策**橙**(有人卡在那儿等)优先于未读**红**(只是还没看);
  · 负数当 0(计数来自网络,不假设它干净);`>99` → `99+`。
- **两侧**(底栏 `MainPage.NavItem` + 宽屏 `WideSidebar.NavItemBuilder`)都接上,
  且读**同一组** AppStorage 键 —— 两处各算一套,数字迟早对不上,而用户同时看得到它们。
- 计数发布走 `AppStorage`(单向:窗格写、导航栏读),与 `KEY_WINDOW_INSETS` 同一套机制。
  不把这两个数提到 `MainPage`:那样"从没进过通信页"也会去发请求。
- 徽标位置对齐 WebUI 的 `absolute top-0.5 right-1`(压在项的右上角)。
  ★ 第一版我排在文字**下面**,截图一眼可见那颗 3 掉到了「联系人」标签底下、
  还把 48vp 的项撑高了 —— 方阵节奏乱掉。

## 判据(harmony-widescreen 6 → 7)

第 ⑦ 条**直接执行**鸿蒙侧的纯函数,且期望值在测试里**独立算一遍**
(不复用被测函数,否则是"用实现验实现")。

★ 接线部分我写错过一次,变异测试当场拆穿:第一版只判
`assert.match(src, /navBadgeCount\(/)` —— 把**渲染处**的调用换成 `0`
(徽标永远不显示),文件里仍留着一处调用,判据照样全绿。
⇒ 改成判**把值交给 Text 的那一行**,并且认出两侧写法不同(底栏走 helper
`Text(this.navBadgeOf(key))`,侧栏就地内联 `Text(navBadgeText(navBadgeCount(...)))`)。

**4 个变异方向全咬**:通信漏算待决策 ⇒ 红;色调优先级写反 ⇒ 红;
侧栏渲染处换空 ⇒ 红;底栏渲染处换空 ⇒ 红。

设备实测:侧栏「联系人」显示红 **3**(3 个联系人),位置在图标右上角。
This commit is contained in:
2026-09-18 13:30:38 +08:00
parent 1832937016
commit d8277a5977
5 changed files with 229 additions and 6 deletions

View File

@ -187,3 +187,59 @@ export function navKeyAt(index: number): string {
export function navLabelAt(index: number): string {
return NAV_CONTENT_ITEMS[normalizeNavIndex(index)].label;
}
/* ─────────────────── 导航项徽标 ─────────────────── */
/**
* 导航项徽标的**取值**(纯逻辑 —— 判据直接执行这一层,不需要设备)。
*
* ★ 逐条对齐 WebUI `Sidebar.tsx:88-95` 的 `badge`:
* `isComm ? unread + pendingPerms : (contacts ? contacts.length : 0)`
* 三条口径都要跟:
* · **通信**把两类"要动手"合起来显示(未读 + 待决策)—— 用户同时看底栏/侧栏,
* 两处各算一套,数字迟早对不上;
* · **联系人**挂联系人数("有几个线索在手");
* · **日历/我的**没有徽标(日历的"有事"是某天的点标记,不是导航栏该喊的东西)。
*/
export function navBadgeCount(key: string, unread: number, pending: number, contactCount: number): number {
const k: string = key.trim();
if (k === 'comm') {
// 负数当 0:计数来自网络响应,不假设它一定干净
const u: number = unread > 0 ? unread : 0;
const p: number = pending > 0 ? pending : 0;
return u + p;
}
if (k === 'contacts') {
return contactCount > 0 ? contactCount : 0;
}
return 0;
}
/**
* 导航项徽标的**色调**:`'perm'`(待决策,橙)/ `'unread'`(未读,红)/ `'plain'`(中性)。
*
* 对齐 WebUI `Sidebar.tsx:95` 的 `badgeTone`:
* `isComm && pendingPerms > 0 ? 'perm' : isComm ? 'unread' : 'plain'`
*
* ★ 待决策用橙、未读用红**不是配色偏好**:未读是「有内容没看」,
* 待决策是「有 Agent 卡在那儿等我」—— 后者更急。WebUI 的注释把这条写明了,
* 两边用同一个优先级才不至于一个喊一个不喊。
*/
export function navBadgeTone(key: string, pending: number): string {
const k: string = key.trim();
if (k === 'comm') {
return pending > 0 ? 'perm' : 'unread';
}
return 'plain';
}
/** 徽标文字:0 → 空串(页面据此不渲染),>99 → '99+'(与 WebUI 同一写法) */
export function navBadgeText(n: number): string {
if (n <= 0) {
return '';
}
if (n > 99) {
return '99+';
}
return n + '';
}