跨端: 导航项徽标两侧补齐(我上次"撤回"错了 —— 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:
@ -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 + '';
|
||||
}
|
||||
|
||||
@ -82,6 +82,9 @@ import {
|
||||
NAV_CONTENT_RESERVE,
|
||||
NAV_ITEM_MIN_HIT,
|
||||
NAV_ITEMS,
|
||||
navBadgeCount,
|
||||
navBadgeText,
|
||||
navBadgeTone,
|
||||
NavItem,
|
||||
normalizeNavIndex
|
||||
} from '../model/NavItems';
|
||||
@ -1149,6 +1152,14 @@ struct CommPage {
|
||||
@State commTab: string = 'inbox';
|
||||
@State unreadCount: number = 0;
|
||||
@State pendingCount: number = 0;
|
||||
/*
|
||||
* 徽标数的 AppStorage 键 —— 由刚才那个窗格写、由导航栏读(见发布处注释)。
|
||||
* 常量而不是字面量:拼错 `AppStorage.get('xxx')` 不报错、只会恒为 undefined,
|
||||
* 而那正好是"徽标永远不出现"这个症状 —— 与 windowInsets 同一个坑。
|
||||
*/
|
||||
private readonly KEY_NAV_BADGE_UNREAD: string = 'agentmail.nav.unread';
|
||||
private readonly KEY_NAV_BADGE_PENDING: string = 'agentmail.nav.pending';
|
||||
private readonly KEY_NAV_BADGE_CONTACTS: string = 'agentmail.nav.contacts';
|
||||
/** Navigation 路由栈:宽屏 Split 时列表+详情并排,窄屏 Stack 时详情 push 覆盖 */
|
||||
private navPathStack: NavPathStack = new NavPathStack();
|
||||
|
||||
@ -1253,6 +1264,17 @@ struct CommPage {
|
||||
}
|
||||
this.unreadCount = unread;
|
||||
this.pendingCount = pending;
|
||||
/*
|
||||
* ★ 同时**发布到 AppStorage** —— 底栏与侧栏(MainPage 的兄弟分支)要读它。
|
||||
*
|
||||
* 为什么不把这两个数提到 MainPage:它们是"通信"这个窗格的数据,
|
||||
* 由这个窗格自己拉(每账号一次 inbox + pendingPermissions)。
|
||||
* 提到父级会让 MainPage 在**从没进过通信页**时也去发请求。
|
||||
* 而徽标必须在导航栏上可见(那是它的意义),所以走 AppStorage 单向发布:
|
||||
* 窗格算 → 写 → 导航栏读。这与 `KEY_WINDOW_INSETS` 同一套机制。
|
||||
*/
|
||||
AppStorage.setOrCreate(this.KEY_NAV_BADGE_UNREAD, unread);
|
||||
AppStorage.setOrCreate(this.KEY_NAV_BADGE_PENDING, pending);
|
||||
} catch (e) {
|
||||
// 徽标拉不到就不显示(比显示一个错的数字好)
|
||||
}
|
||||
@ -1462,6 +1484,10 @@ struct ContactsTab {
|
||||
try {
|
||||
const resp = await m.contacts();
|
||||
this.contacts = resp.contacts;
|
||||
/* 联系人数也发布给导航栏徽标(口径见 model/NavItems.ts 的 navBadgeCount) */
|
||||
/* 键名与 MainPage 的 KEY_NAV_BADGE_CONTACTS 同值('agentmail.nav.contacts')——
|
||||
* 两个 struct 不能共享私有常量,所以这里写字面量并在两处注释里互指。 */
|
||||
AppStorage.setOrCreate('agentmail.nav.contacts', resp.contacts.length);
|
||||
} catch (e) {
|
||||
const ae = e as ApiError;
|
||||
this.error = ae.message.length > 0 ? ae.message : '加载失败';
|
||||
@ -2066,6 +2092,16 @@ struct MainPage {
|
||||
* 那正好是本轮要修的病症(避让永远 0 = 黑边照旧),不能用会静默失效的写法。
|
||||
*/
|
||||
@StorageLink(KEY_WINDOW_INSETS) @Watch('recomputeNavReserve') windowInsets: Insets = new Insets();
|
||||
/*
|
||||
* 导航项徽标计数 —— 由 `CommPage.loadBadges` 经 AppStorage 发布(单向:窗格写、导航栏读)。
|
||||
*
|
||||
* `@StorageProp` 而不是 `@StorageLink`:导航栏**只读**,不该往外写。
|
||||
* 键名与 `CommPage` 里的常量必须一致 —— 拼错不报错、只会恒为默认 0
|
||||
* (那正好是"徽标永远不出现"这个症状,与 windowInsets 同一个坑)。
|
||||
*/
|
||||
@StorageProp('agentmail.nav.unread') navUnread: number = 0;
|
||||
@StorageProp('agentmail.nav.pending') navPending: number = 0;
|
||||
@StorageProp('agentmail.nav.contacts') navContacts: number = 0;
|
||||
@State bgPlan: BackgroundPlan = new BackgroundPlan();
|
||||
/**
|
||||
* 背景是否开着 —— 传给每个页面,让它们把**页面底**让出来(变成透明)。
|
||||
@ -2340,6 +2376,34 @@ struct MainPage {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导航项徽标文字(空串 = 不渲染)。
|
||||
*
|
||||
* 计数来自 `@StorageProp`(`CommPage` 发布、导航栏只读)—— 单向:
|
||||
* 窗格算,导航栏读。取值/色调的规则都在 `model/NavItems.ts`(纯逻辑)。
|
||||
*/
|
||||
navBadgeOf(key: string): string {
|
||||
return navBadgeText(navBadgeCount(key, this.navUnread, this.navPending, this.navContacts));
|
||||
}
|
||||
|
||||
/**
|
||||
* 导航项徽标(独立 `@Builder`,便于在 `Column` 里就地条件渲染)。
|
||||
*
|
||||
* 取值/色调都在 `model/NavItems.ts`(纯逻辑,判据直接执行那一层)——
|
||||
* 与 WebUI `Sidebar.tsx:88-95` 的 `badge`/`badgeTone` 同口径。
|
||||
*/
|
||||
@Builder
|
||||
NavBadge(key: string) {
|
||||
if (this.navBadgeOf(key).length > 0) {
|
||||
Text(this.navBadgeOf(key))
|
||||
.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 })
|
||||
}
|
||||
}
|
||||
|
||||
@Builder
|
||||
NavItem(item: NavItem, index: number) {
|
||||
Column() {
|
||||
@ -2368,6 +2432,8 @@ struct MainPage {
|
||||
.lineHeight(12)
|
||||
.fontColor(this.currentIndex === index ? Theme.navFgActive : Theme.navFg)
|
||||
.margin({ top: 3 })
|
||||
/* 徽标:见 NavBadge(口径与 WebUI Sidebar 的 badge/badgeTone 一致) */
|
||||
this.NavBadge(item.key)
|
||||
}
|
||||
/*
|
||||
* 命中区:**显式给下限**(44vp),不靠"看起来够大"。
|
||||
|
||||
@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user