鸿蒙新特性:Badge 徽标组件——构建消息通知与未读提醒系统
消息通知是现代应用中最常见的交互模式之一。无论是社交应用的新消息提醒、电商平台的订单状态更新,还是协作工具的审批通知——"未读计数"这种小小红色徽标几乎无处不在。HarmonyOS NEXT 提供了 Badge 组件,专门用于在 UI 元素上叠加未读计数、状态标识等小型提示,无需手动绘制就能获得统一、美观的徽标效果。
然而,Badge 组件本身只是一个"显示层"——它解决了"怎么展示"的问题,但没有解决"怎么管理"的问题。真实的业务场景需要完整的状态管理:通知列表、已读/未读切换、分类筛选、批量操作等。本文将通过一个完整的"消息通知中心"实战案例,深入讲解 Badge 组件的用法,并在此基础上构建一套实用的通知管理系统。
关键词:HarmonyOS、ArkUI、Badge、徽标、未读计数、消息通知
一、Badge 组件基础
1.1 API 概述
Badge 是 ArkUI 提供的徽标组件,通过 .badge() 属性链式调用设置到任意组件上。它支持两种模式:
- 数字徽标:
badge({ count: number })— 显示具体未读数量 - 文字徽标:
badge({ value: string })— 显示自定义文字(如 “新”、“热门”、“99+”) - 位置控制:
badge({ position: BadgePosition })— 控制徽标位于宿主组件的哪个方位
数字徽标在 count 超过 99 时自动显示 “99+”,这是 Badge 组件内置的行为,无需开发者手动处理。
文字徽标适合不需要具体数量的场景,比如标记一个功能为"新"、一个活动为"热门"、一个审批流为"待处理"等。
1.2 基本用法
最简单的 Badge 用法——在一个图标右上角显示未读数:
Badge({
count: 5,
position: BadgePosition.RightTop
}) {
Image($r('app.media.message_icon'))
.width(32).height(32)
}
Badge 作为容器组件包裹目标元素,徽标自动出现在元素右上角。count 为 0 时徽标自动隐藏——这个默认行为在真实业务中很实用,不需要写 if (count > 0) 的条件渲染。
1.3 Badge 的位置控制
BadgePosition 支持六种位置:
| 位置 | 说明 |
|---|---|
| RightTop | 右上角(默认,最常用) |
| Right | 右侧居中 |
| RightBottom | 右下角 |
| LeftTop | 左上角 |
| Left | 左侧居中 |
| LeftBottom | 左下角 |
对于消息通知场景,右上角是约定俗成的位置,用户已经形成了视觉习惯。其他位置较少使用,但在特定场景下(如左侧标签栏)有意义。
1.4 Badge 的局限性
在实际项目中使用 Badge 组件时,我们遇到了几个限制:
- 样式不可定制:Badge 的背景色固定为红色、文字颜色固定为白色,无法通过属性修改。字号也固定,无法调整。
- 只能包在组件外层:Badge 必须作为容器组件使用,不能在已有的复杂布局中"插入"一个徽标。
- count 为 string 类型的数字:如果需要显示 “99+” 这种格式,在 count 值较小时(如 5)仍只显示 “5”,不会自动添加 “+”。
这些限制使得 Badge 适合简单场景(如底部 Tab 的未读计数),但在复杂场景(如通知列表中的可变大小徽标)中,手动用 Text + borderRadius 构建徽标反而更灵活。
因此,本文的实战案例采用混合策略:简单的 Tab 类场景用 Badge 组件,通知列表中的复杂徽标用手动构建的红色圆形 Text。
二、消息通知中心的设计
2.1 功能规划
一个实用的消息通知中心通常包含以下功能:
- 通知列表:展示消息的图标、标题、摘要、时间和未读状态
- 未读标识:未读消息显示红色徽标和高亮标题
- 点击已读:点击某条通知将其标记为已读
- 全部已读:一键将所有通知标记为已读
- 分类筛选:按通知类别(系统通知、消息通知、活动通知等)过滤
- 清除已读:删除所有已读消息,保持列表清爽
- 统计面板:展示未读数、总数、当前筛选数
2.2 数据结构设计
通知对象的核心字段:
interface NoticeItem {
id: number; // 唯一标识
title: string; // 通知标题
desc: string; // 通知摘要
time: string; // 发布时间(相对时间)
icon: string; // 图标(emoji)
color: string; // 图标背景色
unread: number; // 未读计数(0 或 N)
category: string; // 分类:系统 / 消息 / 活动
}
unread 字段使用数字类型而非布尔值,原因是某些聚合通知可能有多个未读项(如"3 条新评论"),数字可以承载更丰富的语义。当 unread 为 0 时等同"已读",大于 0 时等同"未读"。
2.3 三类通知的设计
样本数据覆盖三个常见类别:
系统通知:系统更新、存储空间、安全提醒——这类通知通常由系统自动触发,用户无法回复,适合只读展示。
| 标题 | 描述 | 类别 |
|---|---|---|
| 系统更新通知 | HarmonyOS NEXT 6.1.1 版本已发布 | 系统 |
| 存储空间提醒 | 云存储空间已使用 85% | 系统 |
| 安全提醒 | 检测到新设备登录 | 系统 |
消息通知:评论、关注、团队邀请——这类是社交/协作类通知,通常可点击跳转到具体内容。
| 标题 | 描述 | 类别 |
|---|---|---|
| 新评论 | 张三评论了你的文章 | 消息 |
| 团队邀请 | 李四邀请你加入团队 | 消息 |
| 新关注 | 王五关注了你的专栏 | 消息 |
活动通知:促销活动、直播预告——这类是营销/运营类通知,时效性强,通常有明确的开始/结束时间。
| 标题 | 描述 | 类别 |
|---|---|---|
| 限时优惠活动 | 618 大促,全场低至 5 折 | 活动 |
| 直播预告 | 今晚 8 点:ArkUI 深度解析 | 活动 |
2.4 状态管理设计
整个页面的状态由三个核心变量驱动:
@State notices: NoticeItem[] = []; // 通知列表(唯一数据源)
@State activeCat: string = '全部'; // 当前筛选分类
所有变化(已读、删除、筛选)都通过对 notices 数组进行不可变更新来实现。遵循我们之前总结的 ArkUI 状态管理原则:永远创建新数组和新对象引用,而非原地修改。
三、核心功能实现
3.1 分类筛选
分类筛选是通知管理的核心功能。我们用一组横向滚动的胶囊按钮实现:
private categories: string[] = ['全部', '系统', '消息', '活动'];
getFiltered(): NoticeItem[] {
if (this.activeCat === '全部') {
return this.notices;
}
let result: NoticeItem[] = [];
for (let i = 0; i < this.notices.length; i++) {
if (this.notices[i].category === this.activeCat) {
result.push(this.notices[i]);
}
}
return result;
}
筛选逻辑很简单:选中"全部"时返回原数组,否则遍历过滤。这个方法的返回值是一个动态计算出的数组——它不缓存、不额外存储,完全由 activeCat 和 notices 驱动。ArkUI 的响应式系统会在依赖变化时自动重新渲染使用 getFiltered() 的 UI。
每个分类胶囊的构建:
ForEach(this.categories, (cat: string) => {
Row() {
Text(cat)
.fontSize(13)
.fontColor(this.activeCat === cat ? '#FFFFFF' : '#666677')
.fontWeight(this.activeCat === cat ? FontWeight.Bold : FontWeight.Normal)
if (this.getCatUnread(cat) > 0) {
Text(' '.concat(this.getCatUnread(cat).toString()))
.fontSize(10)
.fontColor(this.activeCat === cat ? '#FFFFFFAA' : '#9999AA')
}
}
.padding({ top: 6, bottom: 6, left: 12, right: 12 })
.borderRadius(14)
.backgroundColor(this.activeCat === cat ? '#1677FF' : '#F2F3F5')
.margin({ right: 8 })
.onClick(() => { this.activeCat = cat; })
}, (cat: string) => cat)
每个胶囊包含两个信息维度:分类名称和该分类下的未读计数。选中状态和未选中状态有视觉上的对比——背景色(蓝 vs 灰)、文字色(白 vs 灰)、字重(粗 vs 常规)。未读计数在选中状态下使用半透明白色,未选中状态下使用灰色——视觉层次清晰:选中 > 未选中,文字 > 数字。
getCatUnread 方法计算每个分类下的未读总数:
getCatUnread(cat: string): number {
if (cat === '全部') {
return this.getTotalUnread();
}
let sum = 0;
for (let i = 0; i < this.notices.length; i++) {
if (this.notices[i].category === cat) {
sum += this.notices[i].unread;
}
}
return sum;
}
对于"全部"分类,返回全局未读总数;对于具体分类,遍历累加该分类下所有通知的 unread 值。
3.2 标记已读
点击通知将其标记为已读。这里的关键仍然是不可变更新:
markRead(id: number): void {
let copy = this.notices.slice();
for (let i = 0; i < copy.length; i++) {
if (copy[i].id === id && copy[i].unread > 0) {
copy[i] = {
id: copy[i].id, title: copy[i].title, desc: copy[i].desc,
time: copy[i].time, icon: copy[i].icon, color: copy[i].color,
unread: 0, category: copy[i].category
};
break;
}
}
this.notices = copy;
}
三步走:
slice()创建新数组引用- 找到目标通知,展开所有字段创建新对象,将
unread设为 0 - 将新数组赋值给
this.notices,触发@State更新
注意解构展开的写法:由于 ArkTS 不支持对象展开运算符 { ...obj, unread: 0 },需要逐个字段手动列出。这确实有些啰嗦,如果在有很多字段的场景下,可以写一个 copyNotice() 工厂函数来复用。
3.3 全部已读
一键标记所有通知为已读:
markAllRead(): void {
let copy = this.notices.slice();
let changed = false;
for (let i = 0; i < copy.length; i++) {
if (copy[i].unread > 0) {
copy[i] = {
id: copy[i].id, title: copy[i].title, desc: copy[i].desc,
time: copy[i].time, icon: copy[i].icon, color: copy[i].color,
unread: 0, category: copy[i].category
};
changed = true;
}
}
if (changed) {
this.notices = copy;
}
}
与 markRead 的区别在于遍历所有通知而非找到一个就 break。同时引入了一个 changed 标记——如果所有通知原本都已读,就不触发不必要的状态更新。这是一个小优化,但在频繁操作场景下(如用户反复点击"全部已读")可以避免无意义的重渲染。
"全部已读"按钮使用半透明样式,置于标题栏右侧:
Text('全部已读')
.fontSize(13)
.fontColor('#FFFFFFCC')
.padding({ top: 5, bottom: 5, left: 12, right: 12 })
.borderRadius(12)
.backgroundColor('#FFFFFF20')
.onClick(() => { this.markAllRead(); })
标题栏使用深色背景(#1a1a2e),"全部已读"按钮使用白色半透明底色(#FFFFFF20)和半透明白色文字——保持标题栏统一的视觉风格,同时有足够的可点击区分度。
3.4 清除已读
只保留未读通知,删除所有已读项:
deleteRead(): void {
let result: NoticeItem[] = [];
for (let i = 0; i < this.notices.length; i++) {
if (this.notices[i].unread > 0) {
result.push(this.notices[i]);
}
}
this.notices = result;
}
这个操作从筛选栏右侧触发,显示为红色文字"清已读"——只在当前筛选结果有通知时才显示:
if (this.getFiltered().length > 0) {
Text('清已读')
.fontSize(11)
.fontColor('#FF4D4F')
.padding({ top: 5, bottom: 5, left: 10, right: Spacing.LG })
.onClick(() => { this.deleteRead(); })
}
注意 deleteRead 删除的是全部已读通知,而非仅当前分类筛选下的已读通知。这是因为:
- “清除已读"是一个全局清理操作,用户的心理模型是"清掉所有我看过的”
- 如果只删除当前分类的已读,用户切换到其他分类后会疑惑"为什么已读的还在"
如果产品需求确实是"只清当前分类的已读",可以在 deleteRead 中加入分类筛选逻辑。但从用户体验角度,全局清除更直观。
3.5 统计面板
通知列表上方展示三个统计数字:
Row() {
Column() {
Text(this.getTotalUnread().toString())
.fontSize(28)
.fontColor('#1677FF')
.fontWeight(FontWeight.Bold)
Text('未读消息').fontSize(11).fontColor('#9999AA').margin({ top: 2 })
}
.alignItems(HorizontalAlign.Center).layoutWeight(1)
Column() {
Text(this.notices.length.toString())
.fontSize(28)
.fontColor('#1a1a2e')
.fontWeight(FontWeight.Bold)
Text('全部消息').fontSize(11).fontColor('#9999AA').margin({ top: 2 })
}
.alignItems(HorizontalAlign.Center).layoutWeight(1)
Column() {
Text(this.getFiltered().length.toString())
.fontSize(28)
.fontColor('#00B894')
.fontWeight(FontWeight.Bold)
Text('当前筛选').fontSize(11).fontColor('#9999AA').margin({ top: 2 })
}
.alignItems(HorizontalAlign.Center).layoutWeight(1)
}
三列布局,每列 layoutWeight(1) 等分。三列使用不同的颜色——蓝色(未读数)、深色(总数)、绿色(筛选数)——颜色本身就是信息编码,帮助用户快速区分三个指标的含义。
为什么是这三项?它们回答了用户可能关心的三个问题:
- 还有多少没看?(未读消息)→ 决定"是否需要立即处理"
- 一共有多少?(全部消息)→ 了解"整体信息量"
- 当前看到了多少?(当前筛选)→ 了解"筛选范围"
3.6 通知列表
通知列表是页面最重要的部分,每条通知是一个水平布局的行:
ForEach(this.getFiltered(), (item: NoticeItem) => {
Row() {
// 图标
Row() {
Text(item.icon).fontSize(22)
}
.width(44).height(44)
.justifyContent(FlexAlign.Center)
.borderRadius(12)
.backgroundColor(item.color.concat('15'))
// 内容
Column() {
Text(item.title)
.fontSize(14)
.fontColor('#1a1a2e')
.fontWeight(item.unread > 0 ? FontWeight.Bold : FontWeight.Normal)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(item.desc)
.fontSize(12)
.fontColor('#9999AA')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(item.time)
.fontSize(11)
.fontColor('#BBBBCC')
}
.layoutWeight(1).margin({ left: 12 })
// 徽标
if (item.unread > 0) {
Text(item.unread.toString())
.fontSize(11).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
.padding({ top: 3, bottom: 3, left: 7, right: 7 })
.borderRadius(10)
.backgroundColor('#FF4D4F')
}
}
.width('100%').padding({ top: 14, bottom: 14 })
.borderRadius(10)
.onClick(() => { this.markRead(item.id); })
}, (item: NoticeItem) => item.id.toString())
通知行的四个关键设计:
图标区:44x44 的圆角方块,背景色为各通知主色的 15% 透明度(item.color.concat('15'))。这个透明度值经过试验——太浅看不出颜色差异(< 10%),太深则 emoji 白色内容对比度不足(> 25%),15% 是一个合理的平衡点。
内容区:标题 + 描述 + 时间,三层纵向排列。标题在未读状态使用 Bold 字重,已读后变为 Normal——这种微小的视觉变化足够区分已读/未读而不喧宾夺主。一律使用 maxLines(1) + textOverflow 防止长文本撑破布局。
徽标区:手动构建的红色圆形 Text。为什么不用 Badge 组件?如 1.4 节所述,在复杂列表的每个行内部精确放置徽标,手动构建比 Badge 组件更灵活——可以精确控制大小、位置、padding 和内边距。这里使用的是固定样式:#FF4D4F 红色背景、白色粗体数字、圆角 10。
点击行为:整行可点击,点击后调用 markRead,徽标消失、标题由粗变细——用户获得即时的视觉反馈。
3.7 空状态处理
当筛选结果为空时,显示友好的空状态提示:
if (this.getFiltered().length === 0) {
Text('暂无消息')
.fontSize(14)
.fontColor('#BBBBCC')
.width('100%')
.textAlign(TextAlign.Center)
.padding({ top: 24, bottom: 24 })
}
空状态是容易被忽略但至关重要的 UX 细节。如果没有空状态处理,筛选后列表消失会让用户困惑"是 bug 了还是确实没数据?"。一个简单的"暂无消息"文字消除了这种困惑。
在实际产品中,空状态可以更丰富——配合插图、引导按钮等。但对于 Demo 级别来说,文字提示已足够。
3.8 关于 ArkUI Badge 组件的实际使用
回到 Badge 组件本身。虽然本文的通知列表徽标使用了手动构建,但 Badge 组件在更简单的场景下非常合适。例如,在底部 Tab 导航栏中给"消息"Tab 加未读徽标:
Badge({
count: this.getTotalUnread(),
position: BadgePosition.RightTop
}) {
Column() {
Image($r('sys.symbol.message'))
.width(24).height(24)
Text('消息')
.fontSize(10)
}
}
这个用法的优势是:一行代码搞定,不需要手动计算位置和样式。Badge 会自动处理:
- count 为 0 时隐藏
- count > 99 时显示 “99+”
- 精确的右上角定位
在以下场景中,使用 Badge 组件优于手动构建:
- 图标或按钮角标(如购物车、消息 Tab)
- 列表项的状态标记(如审批流的"待处理")
- 需要统一风格的全局徽标系统
而在以下场景中,手动构建更好:
- 需要自定义徽标样式的(如绿色徽标表示"安全"、橙色表示"警告")
- 徽标需要动画效果的
- 徽标位置不固定的(如内联在文字中的徽标)

四、交互流程
整个通知中心的操作流程如下:
4.1 初始状态
页面加载后,8 条通知全部展示。统计面板显示:未读消息 = 13(所有通知的 unread 之和),全部消息 = 8,当前筛选 = 8。分类筛选栏的"全部"胶囊高亮,旁边显示 “13”。
每条未读通知的右侧显示红色数字徽标,标题使用粗体。
4.2 点击某条通知
点击"新评论"通知,该条通知的 unread 变为 0,徽标消失,标题变为常规字重。统计面板的"未读消息"从 13 变为 10(该通知原本有 3 条未读),"全部消息"和"当前筛选"保持不变。
同时,"消息"分类胶囊的数字从原来的值减去 3,因为它所属的分类未读总数减少了。
4.3 切换分类
点击"系统"胶囊,列表只显示 3 条系统类通知。统计面板变为:未读消息 = 13(全局未读仍然不变)、全部消息 = 8(总数不变)、当前筛选 = 3。
此时如果点击"清已读",会删除全部已读消息(包括其他分类的已读项)。
4.4 全部已读
点击标题栏的"全部已读",所有通知的 unread 置为 0。所有徽标消失,所有标题变为常规字重。统计面板"未读消息"变为 0,分类胶囊全部归零。
4.5 清除已读
假设此时有 3 条已读消息和 5 条未读消息,点击"清已读"后,列表只保留 5 条未读消息。统计面板更新:全部消息 = 5,未读消息 = 对应值,当前筛选 = 对应值。
4.6 空状态
如果清除到某分类下没有任何通知,切换过去后显示"暂无消息"。
五、关键设计决策
5.1 为什么不用 Badge 组件做通知列表徽标
这是本文最值得讨论的设计决策。ArkUI 提供了 Badge 组件,理论上我们应该用它来展示通知列表中的未读计数。但在实际开发中,我们发现手动构建更合适,原因如下:
位置控制的精度:Badge 组件通过 BadgePosition 枚举控制位置,但这类位置是相对于宿主组件外层的。在一个复杂的列表行布局中,我们需要徽标准确出现在某个位置(行尾、垂直居中),Badge 组件的定位逻辑不如手动放置直观。
样式自定义:Badge 的样式(颜色、大小、圆角、字重)完全固定。真实产品中,徽标的样式可能需要根据上下文变化——比如:
- 系统通知用蓝色徽标(而非统一的红色)
- 紧急通知用脉冲动画的徽标
- 低优先级通知用小号灰色徽标
这些定制需求超出了 Badge 组件的能力范围。
布局灵活性:在通知行中,徽标只是三个水平元素之一(图标 → 内容 → 徽标),与图标和内容平级排列。Badge 组件的容器模型(包裹宿主组件)在这种场景下显得"用力过猛"——我们需要的是一个普通的 Text,不是包裹整个行的 Badge。
5.2 为什么 unread 是数字而非布尔
如果只关心"已读/未读"两种状态,布尔值似乎更合适。但数字类型有几个优势:
- 聚合通知:一条通知可能对应多条底层消息(如"5 条新评论")。数字直接体现在 UI 上。
- 渐进已读:可以逐步减少 unread(如"标记部分已读"),而非二值切换。
- 统计友好:
getTotalUnread()只需要 sum,不需要先转数字。
在当前的 Demo 中,unread 的实际使用方式是 “0 或 N”,等同于布尔值。但数据结构为未来扩展留了余地。
5.3 为什么筛选不缓存结果
getFiltered() 每次调用都重新计算,没有缓存。这种设计的好处是:
- 代码简单:不需要额外维护一个
filteredNotices状态变量。 - 不会过期:缓存需要手动失效,每次计算的方案天然保证数据一致性。
- 性能足够:对于 8 条通知的列表,遍历筛选的开销可忽略不计。
如果通知数量增长到数百条,可以考虑引入缓存。但对于消息通知这种通常分页加载的数据(每页 20 条左右),每次计算完全没有性能问题。
六、与其他组件的组合使用
6.1 Badge + Tabs
Tab 切换是 Badge 最常见的搭档。配合 Tabs 组件,可以实现带未读计数的标签页:
Tabs() {
TabContent() { /* 消息列表 */ }
.tabBar(this.tabWithBadge('消息', this.msgUnread))
TabContent() { /* 通知列表 */ }
.tabBar(this.tabWithBadge('通知', this.noticeUnread))
TabContent() { /* 我的 */ }
.tabBar(this.tabWithBadge('我的', 0))
}
@Builder
tabWithBadge(label: string, count: number) {
Badge({ count: count, position: BadgePosition.RightTop }) {
Text(label).fontSize(14)
}
}
6.2 Badge + Navigation
在侧边导航或底部导航中,Badge 用于提醒用户特定入口有新内容:
Navigation() {
Column() {
// 侧边菜单项
ForEach(this.menuItems, (item: MenuItem) => {
Badge({ count: item.unreadCount }) {
Row() {
Image(item.icon).width(24).height(24)
Text(item.title)
}
}
})
}
}
6.3 手动徽标 + 列表
如本文实现,手动构建的徽标更适合复杂列表场景。结合 SwipeAction 可以实现"滑动标记已读":
ListItem() {
// 通知行(含手动徽标)
}
.swipeAction({
end: {
builder: () => {
Row() {
Image($r('sys.symbol.message')).width(20).height(20)
Text('已读').fontSize(12).fontColor('#FFFFFF')
}
.backgroundColor('#1677FF')
.onClick(() => { this.markRead(item.id); })
}
}
})
七、进阶扩展方向
7.1 通知推送接入
当前 Demo 的通知数据是硬编码的。在实际应用中,通知数据来自服务端。可以通过 WebSocket 或 Push Kit 实时接收新通知,插入到 notices 数组头部:
onNewNotice(notice: NoticeItem): void {
let copy = this.notices.slice();
copy.unshift(notice); // 新通知放在最前面
this.notices = copy;
}
7.2 通知点击跳转
不同类别的通知点击后应跳转到不同的页面:
- 评论通知 → 跳转到文章详情页
- 团队邀请 → 跳转到团队申请页
- 系统更新 → 跳转到设置页
可以在 NoticeItem 中增加 linkType 和 linkId 字段,在 onClick 中根据类型调用不同的路由。
7.3 通知持久化
使用 PersistentStorage 或关系型数据库(RelationalStore)存储通知数据。在 aboutToAppear 中从本地加载,在 markRead / deleteRead 后同步写回。
7.4 通知分组
同类通知聚合显示,类似微信的"群聊通知"折叠。例如:
[系统] 3条新通知
├─ 系统更新通知
├─ 存储空间提醒
└─ 安全提醒
点击分组头部展开/折叠。这需要在 NoticeItem 外层增加一个 GroupItem 层级结构。
7.5 通知音效与振动
在收到新通知时播放系统提示音或触发振动反馈:
import { vibrator } from '@kit.SensorServiceKit';
onNewNotice(notice: NoticeItem): void {
// 更新数据
// ...
// 触发振动反馈
vibrator.startVibration({
type: 'time',
duration: 100
});
}
八、总结
本文通过"消息通知中心"这个实战案例,全面讲解了 Badge 徽标组件及其配套的通知管理功能。核心知识点包括:
- Badge 组件基础:数字徽标(count)和文字徽标(value)、六种位置控制、默认的 99+ 格式化行为
- Badge 的适用场景与局限:适合简单角标场景,复杂列表徽标推荐手动构建
- 通知数据结构设计:NoticeItem 接口、unread 数字类型的设计考量
- 分类筛选系统:动态计算 + 胶囊式 UI + 每分类未读计数
- 已读状态管理:arkRead / markAllRead / deleteRead 的不可变更新实现
- 统计面板:三列指标(未读 / 全部 / 筛选)的颜色语义编码
- 混合徽标策略:Badge 组件用于系统级角标,手动构建用于列表内徽标
- ArkUI 状态管理实践:slice() + 新对象引用的不可变更新模式
在实际项目中,Badge 组件和手动构建的徽标是互补而非替代关系。选择标准很简单:如果需要标准风格的角标(红点/数字),用 Badge;如果需要自定义样式或复杂布局中的徽标,手动构建。理解两者的优劣,才能在不同场景下做出正确的技术决策。
消息通知系统的复杂度不在于"展示"(那只是几个红色数字),而在于"管理"——已读/未读的状态流转、分类筛选的交互逻辑、列表的增删操作。希望本文的实战案例能为你的下一个通知系统提供参考。
更多推荐

所有评论(0)