跨端: 导航项徽标两侧补齐(我上次"撤回"错了 —— 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

@ -29,7 +29,7 @@
import { Theme } from '../common/Theme';
import { AmIcon } from '../common/Icons';
import { NAV_CONTENT_ITEMS, NavItem } from '../model/NavItems';
import { NAV_CONTENT_ITEMS, NavItem, navBadgeCount, navBadgeText, navBadgeTone } from '../model/NavItems';
import { Insets, topInset } from '../model/WindowInsets';
/** 侧栏宽度(vp)。WebUI 的 `Sidebar` 用 `w-[60px]` */
@ -47,6 +47,15 @@ export struct WideSidebar {
* 窄屏的顶部窗格当时补了 `topInset`,侧栏这一支漏了;而宽屏此前从未真正运行过。
*/
@Prop windowInsets: Insets = new Insets();
/*
* 徽标计数 —— 由 `CommPage` / `MainPage` 经 AppStorage 发布(单向:页面写、导航栏读)。
*
* `@StorageProp` 而不是 `@StorageLink`:侧栏**只读**,不该往外写。
* 与底栏读的是**同一组键** —— 两处各算一套,数字迟早对不上(用户同时看得到它们)。
*/
@StorageProp('agentmail.nav.unread') navUnread: number = 0;
@StorageProp('agentmail.nav.pending') navPending: number = 0;
@StorageProp('agentmail.nav.contacts') navContacts: number = 0;
/** 背景是否开启:开着=玻璃面板(blur + 0.88 白),关着=不透明白 */
@Prop bgActive: boolean = false;
/** 选中回调 */
@ -59,7 +68,8 @@ export struct WideSidebar {
*/
@Builder
NavItemBuilder(key: string, label: string, iconKey: string, index: number) {
Column() {
Stack({ alignContent: Alignment.TopEnd }) {
Column() {
AmIcon({
iconName: iconKey,
iconSize: 22,
@ -72,13 +82,31 @@ export struct WideSidebar {
}
.width(48)
.height(48)
// 侧栏宽 60、项 48 ⇒ 两侧各 6vp,**不需要**再压缩;显式声明避免被父级按比例压掉
.flexShrink(0)
.borderRadius(12)
.justifyContent(FlexAlign.Center)
// 选中底块:WebUI `.nav-item[data-active='true']` 的 `--nav-active-bg`
.backgroundColor(this.currentIndex === index ? Theme.navActiveBg : Color.Transparent)
.onClick(() => { this.onSelect(index); })
/*
* 徽标(与底栏、与 WebUI `Sidebar.tsx:88-95` 同一套口径)。
* 取值/色调都在 `model/NavItems.ts` 的纯函数里 —— 判据直接执行那一层。
*
* ★ 位置对齐 WebUI 的 `absolute top-0.5 right-1`:**压在项的右上角**,
* 不是排在文字下面。放下面会把 48vp 的项撑高、方阵节奏乱掉
* (第一版就是这么错的,截图一眼可见:那颗 3 掉到了"联系人"文字下面)。
*/
if (navBadgeText(navBadgeCount(key, this.navUnread, this.navPending, this.navContacts)).length > 0) {
Text(navBadgeText(navBadgeCount(key, this.navUnread, this.navPending, this.navContacts)))
.fontSize(9).fontColor(Color.White)
.backgroundColor(navBadgeTone(key, this.navPending) === 'perm' ? Theme.warnFg : Theme.danger)
.borderRadius(8)
.padding({ left: 4, right: 4 })
.margin({ top: 2, right: 2 })
}
}
.width(48)
.height(48)
}
build() {