用户三条反馈,逐条对应:
① 「底栏数字为什么显示在图标下面?」
WebUI 的徽标是 `absolute top-1 right-[22%]`(脱离文档流、浮在图标右上角),
我写成了 `Column` 的第三个子节点 ⇒ 参与竖向布局、掉到文字下面。
改用 `Stack({ alignContent: Alignment.TopEnd })` 锚在**图标**上。
(顺带撞了 skill 里明写的坑:Stack 没有 `.justifyContent()`。)
② 「一个横着过去的白条,我真的服了」/「期望:融进背景」
WebUI 的顶栏**自身没有底色** —— 只有 `border-b border-gray-200`
(`ContactPanel.tsx:64`、`CommTabs.tsx:40`、`CalendarView.tsx:295`),
底色由所在面板给;壁纸开启时那层面板是玻璃色(`index.css:876`)。
鸿蒙三个窗格顶栏写死了 `Theme.surface`(实心白)⇒ 无论壁纸开没开,
顶上都是一条不通明白带。改成与**页面底**同一口径
(`bgActive ? Transparent : surface`)+ 补下边框。
③ 「日历页面和webui布局完全不同」
WebUI 是**左右两栏**(`CalendarView.tsx:452-457`):左 `flex-1` 网格、
右 `400px` 常驻面板(日程 / 编辑器 / 小时网格三态互斥)。
鸿蒙原来是**单栏竖堆**。重搭为两栏,`paneWide` 由 `.onAreaChange`
量本页**自己的**宽度(不是屏幕宽度 —— 宽屏下这一页已被侧栏占掉一截);
编辑器改占右栏位置(不再整页盖掉正在看的那个月)。
④ 「最严重的动画问题你一点也不该改」
日历是**常驻挂载**(`visibility` 控制,因为它里面 today 要随时间重算、
也要保住"正在看哪个月"),而 `.transition()` 只在**挂载/卸载**时触发
(SDK 原话 \"when it **appears and disappears**\")⇒ 挂在它上面的
`.transition(paneRiseIn())` **一帧也不会播**,切过去是硬弹。
WebUI 踩过同一个坑并把错法写进了 `index.css:1305-1320`
(「只挂了类,却没让触发窗口出现 ⇒ 类挂着、动画永远不播」),
它的解法是 `html.view-switch` 重放窗口。ArkUI 对应物是
`animateTo` + 显式 `calPaneIn` 属性(`opacity` + `translate`)。
★ reset 必须在 `animateTo` **外**:写进回调里会与同帧的 1 相抵,
渲染层只看得见最终值 ⇒ 动画退化成一个瞬移。
顺带修:
· 宽屏侧栏 = 3 项(通信/日历/**联系**)+ 底部一簇(头像/主题/退出),
与底栏的四项(含「我的」)**不是同一份清单** —— WebUI 的 Sidebar 与
NarrowNav 本就不同(`Sidebar.tsx:26-44` vs `NarrowNav.tsx:37-40`+143)。
新增 `NAV_SIDEBAR_ITEMS` / `ME_PANE_INDEX` / `SIDEBAR_ITEM_*`。
· 退出登录抽成 `api/Logout.ets` 的 `performLogout()`:侧栏底簇与「我的」页
两个入口必须做同一件事(尤其"先注销推送 token"那一步),复制一份就会不一致。
· 徽标 `'plain'` 档底色:WebUI 是石板灰 `--c-chrome-600`(#475569),
我写成与未读共用红色 ⇒ 「联系」的徽标看起来像"有未读"。
· 主题快捷开关(侧栏底簇):对齐 `ThemeToggleButton` —— 从 `system` 翻转时
落到**当前生效值的反面**(不是回 system;那可能毫无变化、让按钮看起来坏了)。
· 「浓度」→「压暗」+ 数值带单位(原先屏上印 `56.000000`;WebUI 是 `suffix="%"`)。
判据(8 个套件全绿:logic 28 / nav 18 / widescreen 7 / window 9 /
arkts 5 / contacts 5 / calendar 30 / system-api 5):
· `harmony-widescreen` ② 重写:**回读 `Sidebar.tsx` 数 `short:` 的个数**
要求鸿蒙同数,并断言底部一簇三键真的被调用(`this.onToggleTheme()` ——
第一版写成 `/onToggleTheme/`,变异测试当场证明它不咬:属性**声明**还在,
正则照样匹上)。新增 ⑧:三档色调各自底色,期望值从 `--c-chrome-600` 读出。
· `harmony-nav` 动画条重写:改判**机制真的存在且被驱动**
(旧断言 `.visibility(...).transition(...)` 锁的正是那个 bug ——
判据引自己写的注释当依据,就会把错误锁死)。新增 reset-在-animateTo-外
这条断言(否则动画退化成瞬移)。
· `harmony-nav` 设备条:`navItemsOf` 的宽屏过滤从"左边缘靠左 1/6"
改成"**整个盒子在侧栏轨道内**" —— 旧条件把日历网格的格子
(实测 `[229,511][366,794]`)也当成导航项,一屏数出 11~12 个。
新增 `navRailItemsOf`:导航轨贴顶、底部簇在屏底,按位置切一刀。
· `harmony-logic` 两处:从 `NAV_ITEMS` / `NAV_SIDEBAR_ITEMS`
**各自的数组**里取 label —— 原先把全文件 `label:` 一网打尽,
得到 7 个(4+3 混在一起),任何一边改对了它都会红。
设备实测(HATriple 三折叠,3184×2232):侧栏 3 项 + 底簇 / 底栏徽标回到
图标右上角 / 日历左右两栏与 WebUI 并排同构。
server/go.mod:补 2da38bb 漏提交的 lunar-go 依赖。
302 lines
15 KiB
TypeScript
302 lines
15 KiB
TypeScript
/*
|
||
* 底部导航(P5:悬浮玻璃条)的**清单与尺寸**。
|
||
*
|
||
* 为什么数据放在 `.ts` 而不是写在 `MainPage.ets` 里:
|
||
* - 判据可以直接 `import` 它(`node --experimental-strip-types` 能跑纯逻辑 `.ts`),
|
||
* 于是"命中区 ≥44vp"这类要求判的是**数值本身**,而不是拿正则去源码里猜;
|
||
* - 「通信 → 联系人」的顺序与文案是信息架构,和 CommTabs 一样属于跨端要对齐的东西
|
||
* (WebUI 侧同样是两项,撤掉会话 tab 的时机见计划文档 §7.15)。
|
||
*/
|
||
|
||
/** 一个底部导航项。字段与 WebUI `Sidebar.tsx` 的项一一对应 */
|
||
export interface NavItem {
|
||
/** 稳定键(ForEach 的 key,也是判据认的名字) */
|
||
key: string;
|
||
/** 显示文案(与 WebUI 逐字一致) */
|
||
label: string;
|
||
/**
|
||
* 图标名 —— 指向 `common/Icons.ets` 的 `ICON_PATHS`,与 WebUI 的图标集**同几何**。
|
||
*
|
||
* 2026-09-15 之前这里是 emoji 字形(`✉️`/`📅`/`👤`),理由是"不用猜 sys.media 名字"。
|
||
* 那条理由只解释了"为什么不用系统资源名",却把 emoji 当成了唯一替代 —— 结果是:
|
||
* · WebUI 在 `icons.tsx` 第一行就写着「**纯 SVG 图标,全站不使用 emoji**」,
|
||
* 两边**同一个产品却两套图标语言**;
|
||
* · emoji 由系统字体渲染,颜色不受 `fontColor` 控制、随厂商与版本变形,
|
||
* 选中/未选中只能靠整体透明度示意,做不到 WebUI 那种"线性描边图标 + 品牌色"。
|
||
*
|
||
* 现在改用 ArkUI 原生 `Path`(`common/Icons.ets`,由 WebUI 的 TSX 自动生成),
|
||
* 既不需要猜 `sys.media` 名字,又能吃主题色(深色模式一处也不漏)。
|
||
*/
|
||
iconKey: string;
|
||
/**
|
||
* 外壳入口:点击后路由到独立 @Entry 页(`pages/xxx`),不在主界面挂内容。
|
||
* 留空 = 内容窗格(由 `MainPage` 的 currentIndex 分派挂内容)。
|
||
*
|
||
* 为什么单列这个字段而不是把「我的」塞进 currentIndex 分派:设置页是 **@Entry**
|
||
* (自带返回按钮、独立路由栈),不能当作可嵌入的 pane —— 在内容分派里给它
|
||
* 占一个 index 会出现"点了没内容、或每次重建都 pushUrl"的尴尬。外壳入口在
|
||
* NavItem 的 onClick 里直接路由,与 WideSidebar 底部那个设置按钮是同一行为。
|
||
*/
|
||
/**
|
||
* 外壳入口的路由目标。
|
||
*
|
||
* ★ 2026-09-17 之后**没有导航项再用它** —— 第 4 项「我的」已改为内容窗格
|
||
* (见 `NAV_ITEMS` 的说明)。字段保留是因为宽屏侧栏的“设置”按钮与
|
||
* `WideSidebar.onSettings` 仍走同一套路由语义;**新加的导航项不要再填它**,
|
||
* 否则又会出现“点了导航、导航条整个消失”的形状。
|
||
*/
|
||
route?: string;
|
||
}
|
||
|
||
/**
|
||
* 底栏四枚图标 = 三个内容窗格 + 一个外壳入口,与 WebUI `NarrowNav.tsx` 的四入口一一对应。
|
||
* 前**三项**是平级内容窗格:通信(内部三栏:收件箱/发件箱/授权)、日历、联系人。
|
||
* 第**四项**「我的」也是**内容窗格**(与前三项同一套机制,`currentIndex === 3`)——
|
||
* 它承载设置/主题/壁纸/账号。★ 2026-09-17 修正:原先它是外壳入口(`route` 走
|
||
* `pushUrl('pages/SettingsPage')` 推独立页),导致底部导航整条消失 —— 用户原话
|
||
* 「我的页面完全没有遵守 nav 的导航规则」。WebUI 的 `account` 只是一个 `viewMode`,
|
||
* 导航常驻,所以这里改成同一套窗格机制。
|
||
*
|
||
* 「会话」原先是个平级 tab,已撤 —— 它不是第三个地方,而是"同一批数据的另一种看法"
|
||
* (收件箱按会话折叠、联系人卡片视图就是会话的进度视角)。理由见 `MainPage.build()` 的注释。
|
||
*
|
||
* 「日历」原来写着"等 P6 内容做完再上入口 —— 不留点进去空着的页签"。P6 第一步
|
||
* (只读月视图:`pages/CalendarPage.ets`)已经做完,所以入口**现在**上:
|
||
* 空页签那条理由不再成立,而日历的纯逻辑(`model/Calendar.ts`)本来就是照着
|
||
* 「顶层是 通信/日历/联系人 三个平级 pane」写判据的。
|
||
*
|
||
* ★ `NAV_ITEMS` 是**底栏**的清单(四项)。宽屏侧栏的清单是 `NAV_SIDEBAR_ITEMS`
|
||
* (三项 + 底部一簇),两者**不可互相替代** —— 理由见那份的注释。
|
||
*/
|
||
export const NAV_ITEMS: NavItem[] = [
|
||
{ key: 'comm', label: '通信', iconKey: 'inbox' },
|
||
{ key: 'calendar', label: '日历', iconKey: 'calendar' },
|
||
{ key: 'contacts', label: '联系人', iconKey: 'contacts' },
|
||
/*
|
||
* ★ 2026-09-17:第 4 项**不再带 `route`** —— 它现在是**内容窗格**。
|
||
*
|
||
* 原形状是外壳入口:`route: 'pages/SettingsPage'`,点击 `pushUrl` 推一个
|
||
* 独立 @Entry 页 ⇒ 底部导航整条消失(用户:「我的页面完全没有遵守 nav 的导航规则」)。
|
||
* WebUI 的 `account` 只是一个 `viewMode`(与 inbox/calendar/contacts 同级),
|
||
* 底部导航**无条件渲染** —— 所以这里改成同一套窗格机制。
|
||
*/
|
||
{ key: 'me', label: '我的', iconKey: 'person' }
|
||
];
|
||
|
||
/** 命中区下限(vp)。**44 是可点区域的下限**,不是"看起来够大"的估计 */
|
||
export const NAV_ITEM_MIN_HIT: number = 44;
|
||
|
||
/** 浮动条自身高度(vp)。≥ NAV_ITEM_MIN_HIT,否则命中区内边距会互相挤压 */
|
||
export const NAV_BAR_HEIGHT: number = 56;
|
||
|
||
/** 悬浮:左右留白(vp)—— 贴边就不是"悬浮"了,WebUI 的底栏同样浮在内容之上 */
|
||
export const NAV_BAR_SIDE: number = 16;
|
||
|
||
/** 悬浮:离屏幕底部的留白(vp) */
|
||
export const NAV_BAR_BOTTOM: number = 12;
|
||
|
||
/** 圆角(vp)。用"胶囊"半径(= 高度一半),与 WebUI 的 `rounded-2xl` 观感一致 */
|
||
export const NAV_BAR_RADIUS: number = 28;
|
||
|
||
/**
|
||
* 内容与浮动条之间的余量(vp)。
|
||
*
|
||
* 单独起名而不是把 `8` 埋在算式里:它是这一族里**唯一**还需要人判断的数
|
||
* (条离底 12 + 条高 56 + 余量 8),命名之后"内容到底让多少"只剩一个数可争,
|
||
* 其余全部派生。pi 2026-09-14 提的"让位高度应从条高派生,不要写成并列常量",
|
||
* 落实在这两行上。
|
||
*/
|
||
export const NAV_CONTENT_GAP: number = 8;
|
||
|
||
/**
|
||
* 内容底部要让出的高度(vp):条高 + 离底留白 + 一点余量。
|
||
*
|
||
* **这是"点得到"的问题,不是美观问题**:条是浮在内容之上的,不让出这段高度,
|
||
* 列表最后一行就永远压在玻璃条底下 —— 看得见、点不到。
|
||
*
|
||
* ★ 让位方式:**加在滚动容器的内容末尾**(`contentEndOffset` / 末尾占位),
|
||
* **不是**加在窗格的 `padding({bottom})` 上。两者看起来等价,实际差很大:
|
||
* padding 会**缩短窗格本身** ⇒ 内容永远到不了条底下 ⇒
|
||
* 玻璃条背后只剩一张**已经被壁纸层模糊过**的壁纸,系统材质无东西可糊,
|
||
* 于是它看起来就是一块普通的浅色面板,而不是玻璃(2026-09-17 用户:
|
||
* 「底栏不是玻璃质感,滑动内容无法穿过底栏」)。
|
||
* WebUI 同构:`.narrow-shell` 是 `flex-col`,列表满高、`.narrow-nav` 作为兄弟
|
||
* 叠在上面(有自己的 margin),内容自然滑到条底下。
|
||
*
|
||
* **派生,不并列**:上面三个常量任何一个变大,这里自动跟着变;写死一个数(例如 76)
|
||
* 就会在改条高的那天悄悄失配。判据 `harmony-nav.test.mjs` 同时钉"派生关系"与
|
||
* "在算式中出现",防它退回字面量。
|
||
*/
|
||
export const NAV_CONTENT_RESERVE: number = NAV_BAR_HEIGHT + NAV_BAR_BOTTOM + NAV_CONTENT_GAP;
|
||
|
||
/** 内容窗格的数量(前三项:通信/日历/联系人)—— `MainPage` 的 currentIndex 分派按它穷尽,
|
||
* `NAV_CONTENT_ITEMS` 也按它切片 */
|
||
/** 内容窗格的数量(四项:通信/日历/联系人/我的)—— `MainPage` 的 currentIndex 分派按它穷尽 */
|
||
export const NAV_CONTENT_COUNT: number = 4;
|
||
|
||
/**
|
||
* 内容窗格的清单 —— 宽屏侧栏与 `currentIndex` 分派**都用它**。
|
||
*
|
||
* ★ 2026-09-18 修注释:这里原来写着「前三项……第 4 项「我的」是外壳入口,不在内」,
|
||
* 但代码写的是 `slice(0, NAV_CONTENT_COUNT)` 而 `NAV_CONTENT_COUNT = 4`
|
||
* ⇒ **四项全在**,注释与代码互相矛盾。宽屏侧栏因此多出一个「我的」,
|
||
* 与它下面那个"推页进设置"的入口并排(两个 person 图标),
|
||
* 而 `MainPage` 的 `currentIndex === 3` 分支本来就是窗格 —— 三处口径不一。
|
||
* 现在统一:**四项都是内容窗格**,注释按代码写。
|
||
*/
|
||
export const NAV_CONTENT_ITEMS: NavItem[] = NAV_ITEMS.slice(0, NAV_CONTENT_COUNT);
|
||
|
||
/* ─────────────────── 宽屏侧栏(与底栏**不是同一份清单**) ─────────────────── */
|
||
|
||
/**
|
||
* 宽屏**侧栏**的导航项 —— 三项,**不含「我的」**。
|
||
*
|
||
* ★★ 2026-09-19 修(用户:「你写的app和webui大面积不符,问题特别大」)。
|
||
*
|
||
* 之前侧栏直接用 `NAV_CONTENT_ITEMS`(四项,含「我的」)——
|
||
* **那是底栏的形状**,不是侧栏的。两套导航在 WebUI 里本来就不同,证据:
|
||
*
|
||
* · `Sidebar.tsx:26-44` 的 `navItems` 只有 **3 项**:通信 / 日历 / **联系**;
|
||
* 「我的」不在这里 —— 它是 `Sidebar.tsx:186` 底部那一簇里的**头像按钮**
|
||
* (`onClick={() => setViewMode('account')}`),与主题切换、退出并列;
|
||
* · `NarrowNav.tsx:37-40` 的 `items` 是 3 项 + 第 4 个「我的」按钮
|
||
* (`NarrowNav.tsx:143`),所以底栏**是**四项。
|
||
*
|
||
* 也就是说:**四项是对的,但只对底栏**。侧栏要三项 + 底部一簇。
|
||
* 我把底栏那份直接 `slice()` 给侧栏用,于是并排一看就多出一个「我的」,
|
||
* 而 WebUI 那一格是头像。
|
||
*
|
||
* ★ 第三项的文案也不同:侧栏是「**联系**」(`Sidebar.tsx:43` `short: '联系'`),
|
||
* 底栏是「联系人」(`NarrowNav.tsx:39`)。两处各是各的字面量,不是笔误。
|
||
*/
|
||
export const NAV_SIDEBAR_ITEMS: NavItem[] = [
|
||
{ key: 'comm', label: '通信', iconKey: 'inbox' },
|
||
{ key: 'calendar', label: '日历', iconKey: 'calendar' },
|
||
{ key: 'contacts', label: '联系', iconKey: 'contacts' }
|
||
];
|
||
|
||
/**
|
||
* 侧栏第三项在 `NAV_CONTENT_ITEMS` 里的下标(`contacts`)。
|
||
*
|
||
* 侧栏只画三项,但点击要选中 `currentIndex`(内容窗格的下标,四项那套)。
|
||
* 用具名常量而不是就地写 `2`:这一项的顺序要与 `NAV_SIDEBAR_ITEMS` 的构图一致,
|
||
* 改顺序时只改一处。
|
||
*/
|
||
export function sidebarContentIndex(key: string): number {
|
||
for (let i = 0; i < NAV_CONTENT_ITEMS.length; i++) {
|
||
if (NAV_CONTENT_ITEMS[i].key === key) {
|
||
return i;
|
||
}
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/** 「我的」内容窗格的下标(侧栏底部头像点它进账户页) */
|
||
export const ME_PANE_INDEX: number = 3;
|
||
|
||
/** 侧栏底色块宽度(vp)—— WebUI `Sidebar.tsx` 的 `w-12 h-12` */
|
||
export const SIDEBAR_ITEM_SIZE: number = 48;
|
||
|
||
/** 侧栏圆角(vp)—— WebUI 的 `rounded-lg` */
|
||
export const SIDEBAR_ITEM_RADIUS: number = 12;
|
||
|
||
/**
|
||
* 「我的」的路由目标(`SettingsPage` 仍然存在,供别处按需推页)。
|
||
*
|
||
* ★ 侧栏/底栏**不再**用它 —— 第 4 项是内容窗格(`currentIndex === 3`)。
|
||
* 留着这个常量不是死代码:`SettingsPage` 作为 @Entry 仍可被推到
|
||
* (例如将来做"从通知点开设置"),但**导航栏不许拿它当入口**
|
||
* (用户 2026-09-17:「我的页面完全没有遵守 nav 的导航规则」)。
|
||
*/
|
||
export const NAV_SETTINGS_ROUTE: string = 'pages/SettingsPage';
|
||
|
||
/**
|
||
* 滚动列表上下边缘的渐隐高度(vp)。
|
||
*
|
||
* 对应 WebUI `index.css` 的 `.overflow-y-auto { mask-image: linear-gradient(...) }`:
|
||
* 滚动条本身是“一刀切”,卡片滑到边缘被硬截断;加一层上下渐隐后,
|
||
* 切断处变成渐隐(2026-09-14 用户:「内容项上下滑动会直接被切断」)。
|
||
*
|
||
* ArkUI 的对应物是 `List.fadingEdge(true, { fadingEdgeLength })`(API 14+),
|
||
* **不要**手捧渐变色遮罩:那是拿背景色画一个不存在的“底色”,
|
||
* 在壁纸/玻璃主题下会露馅。
|
||
*
|
||
* 取 12:与 WebUI 的 `min(12px, 10%)` 同量级,也接近列表内边距(10vp),
|
||
* 静止时几乎看不出,滚动时才起作用。
|
||
*/
|
||
export const LIST_FADE_LENGTH: number = 12;
|
||
|
||
/** 把任意下标归一化到内容窗格的合法范围(点击/外部传值都过这里,别直接赋值) */
|
||
export function normalizeNavIndex(index: number): number {
|
||
if (index >= 0 && index < NAV_CONTENT_COUNT) {
|
||
return index;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/** 第 index 项的键(内容按它挂载,判据也认它) —— 越界回 0(内容窗格,不含外壳入口) */
|
||
export function navKeyAt(index: number): string {
|
||
return NAV_CONTENT_ITEMS[normalizeNavIndex(index)].key;
|
||
}
|
||
|
||
/** 第 index 项的标签(给判据与无障碍文案用) —— 越界回 0(内容窗格,不含外壳入口) */
|
||
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 + '';
|
||
}
|