鸿蒙原生应用 ArkUI 实战:条件渲染驱动消息中心页四分类真切换与未读红点
App 18「校园社交匹配」消息 Tab(Func2Tab),是本系列交互最完整的消息中心——4 栏彩色统计 + 4 分类 Tab(全部/未读/好友申请/系统通知)真实切换不同列表(
if/else条件渲染)+ 通知卡片 + 聊天卡片(未读红点角标/在线状态)。本篇基于18-social-match/entry/src/main/ets/pages/Func2Tab.ets(约 261 行)逐段拆解,附 4 张实机截图。
一、整体结构:4 分类真切换(本页最大亮点)
Func2Tab 的骨架与系列记录页不同——不是"Scroll 装全部内容",而是"按 currentTab 切换不同列表":
build() { Column() { this.Header() this.StatOverview() this.TabBar() if (this.currentTab === 0) { this.AllList() } else if (this.currentTab === 1) { this.UnreadList() } else if (this.currentTab === 2) { this.FriendReqList() } else { this.SystemList() } } .width('100%').height('100%').backgroundColor(C.bg) }if/else if/else四分支条件渲染——@State currentTab决定显示哪个列表:
- currentTab === 0→ AllList(通知区 + 聊天区)
- currentTab === 1→ UnreadList(未读消息筛选)
- currentTab === 2→ FriendReqList(好友申请 + 通过/忽略)
- else(3)→ SystemList(系统通知)
这是本系列第一个"按 Tab 完全切换页面内容"的消息中心(App 13/14/15/16/17 的 Tab 只是"切换高亮"或"过滤同一个列表")。"真切换"比"过滤"更彻底——不同分类可以有不同的卡片形态(好友申请卡有通过/忽略按钮,聊天卡有未读红点)。
if/else if/else在 ArkUI build 里是合法的条件渲染(与ForEach一样是声明式渲染的一部分)。
项目源码开源:https://gitee.com/codenestFlow/HarmonyOSHub
二、Header + StatOverview:4 栏彩色统计
Header 单行标题"消息中心"。StatOverview 是 4 栏统计(未读消息 3/好友申请 5/本周互动 12/系统通知 2),每栏带颜色:
@Builder StatOverview() { Row() { ForEach(this.stats, (s: StatItem) => { Column({ space: 4 }) { Text(s.value).fontSize(18).fontWeight(FontWeight.Bold).fontColor(s.color) Text(s.label).fontSize(10).fontColor(C.textDim) }.layoutWeight(1) }, (s: StatItem) => s.label) } .width('100%').padding({ top: 14, bottom: 14 }) .backgroundColor(C.card).borderRadius(D.rMd).border({ width: 1, color: C.stroke }) .margin({ left: D.pad, right: D.pad, top: 12 }) }4 色映射:
private stats: StatItem[] = [ { value: '3', label: '未读消息', color: C.primary }, { value: '5', label: '好友申请', color: C.warn }, { value: '12', label: '本周互动', color: C.ok }, { value: '2', label: '系统通知', color: C.accent } ];- 未读消息 3 → 粉红(主色)
- 好友申请 5 → 橙(警示)
- 本周互动 12 → 绿(成功)
- 系统通知 2 → 浅粉红(accent)
"好友申请 5"用橙色警示——申请是"待处理"(需要行动),橙色强化。"本周互动 12"绿色——互动是"好状态"。"统计色 = 语义色"。
数据一致性:未读消息 3 = 4 个聊天里 2+1=3 个未读(小明 2 + 小华 1)✓;系统通知 2 = notices 里 2 个未读/待处理?——"未读消息 3"与聊天数据严格对应,好数据。
三、TabBar:4 段下划线
TabBar 是 4 个分类(全部/未读/好友申请/系统通知)的下划线指示器:
@Builder TabBar() { Row() { ForEach(this.tabs, (t: string, idx: number) => { Column({ space: 4 }) { Text(t).fontSize(13) .fontColor(this.currentTab === idx ? C.primary : C.textDim) .fontWeight(this.currentTab === idx ? FontWeight.Bold : FontWeight.Normal) Row() .width(24).height(3).borderRadius(2) .backgroundColor(this.currentTab === idx ? C.primary : 'transparent') }.layoutWeight(1).padding({ top: 12, bottom: 8 }) .onClick(() => { this.currentTab = idx; }) }, (t: string, idx: number) => t + idx) } .width('100%').backgroundColor(C.card) .margin({ top: 12 }) .border({ width: 0, color: C.stroke }) .border({ width: { bottom: 1 }, color: C.stroke }) }与 App 13/17 TabBar 的差异:
- 下划线 24×3(App 13 是 24×2,更粗)
border({ width: 0, ... }).border({ width: { bottom: 1 }, ... })双 border 写法——先全 0 再覆盖底部 1vp(底部横线分隔 TabBar 与列表)
onClick(() => { this.currentTab = idx; })切换 currentTab →if/else重新渲染列表——"Tab 真切换"。
四、AllList:通知区 + 聊天区
AllList(全部 Tab)是"通知区 + 聊天区"两个分区:
@Builder AllList() { Scroll() { Column({ space: 12 }) { this.NoticeSection() this.ChatSection() } .width('100%') .padding({ left: D.pad, right: D.pad, top: 12, bottom: D.pad + this.safeBottom + 20 }) } .layoutWeight(1).scrollBar(BarState.Off).align(Alignment.Top) }NoticeSection(4 条通知)+ChatSection(4 个聊天)——"通知 + 聊天"是消息中心的两大内容(微信的"服务通知" + "聊天")。
4.1 NoticeCard:通知卡片
@Builder NoticeCard(n: NoticeItem) { Row({ space: 12 }) { Row() { Text(n.icon).fontSize(24) } .width(44).height(44).backgroundColor(C.primarySoft).borderRadius(22).justifyContent(FlexAlign.Center) Column({ space: 4 }) { Row() { Text(n.title).fontSize(14).fontWeight(FontWeight.Bold).fontColor(C.text).layoutWeight(1) Text(n.status).fontSize(10).fontColor(n.statusColor) .padding({ left: 6, right: 6, top: 3, bottom: 3 }) .backgroundColor(C.cardSoft).borderRadius(4) }.width('100%') Text(n.desc).fontSize(12).fontColor(C.textSub).maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }) Text(n.time).fontSize(10).fontColor(C.textDim) }.alignItems(HorizontalAlign.Start).layoutWeight(1) } .width('100%').padding(12).backgroundColor(C.card).borderRadius(D.rMd).border({ width: 1, color: C.stroke }) .onClick(() => { promptAction.showToast({ message: n.title }); }) }4 条通知:新的好友申请(👋 待处理 橙)、活动提醒(🎉 未读 粉红)、匹配成功(⭐ 已查看 灰)、系统升级(📢 已读 灰)。
状态标签三态(status + statusColor):待处理(橙)/未读(粉红)/已查看已读(灰)——"通知状态"三色。maxLines(1)+ Ellipsis 描述截断。
4.2 ChatCard:聊天卡片(未读红点 + 在线状态)
@Builder ChatCard(c: ChatItem) { Row({ space: 12 }) { Stack({ alignContent: Alignment.TopEnd }) { Row() { Text(c.avatar).fontSize(32) } .width(50).height(50).backgroundColor(C.primarySoft).borderRadius(25).justifyContent(FlexAlign.Center) if (c.unread > 0) { Text(c.unread.toString()).fontSize(10).fontColor('#FFFFFF') .backgroundColor(C.danger).borderRadius(10) .padding({ left: 5, right: 5, top: 2, bottom: 2 }).minFontSize(8) } } Column({ space: 4 }) { Row() { Text(c.name).fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text).layoutWeight(1) Text(c.time).fontSize(10).fontColor(C.textDim) }.width('100%') Row() { Text(c.lastMsg).fontSize(12).fontColor(C.textSub).layoutWeight(1) .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }) Text(c.status).fontSize(10).fontColor(c.statusColor) .padding({ left: 6, right: 6, top: 2, bottom: 2 }) .backgroundColor(C.cardSoft).borderRadius(4) }.width('100%') }.alignItems(HorizontalAlign.Start).layoutWeight(1) } .width('100%').padding(12).backgroundColor(C.card).borderRadius(D.rMd).border({ width: 1, color: C.stroke }) .onClick(() => { promptAction.showToast({ message: '打开聊天: ' + c.name }); }) }聊天卡 4 要素(标准微信风格):
- 头像 + 未读红点(Stack TopEnd):
if (c.unread > 0) { Text(c.unread.toString()).fontSize(10).fontColor('#FFFFFF') .backgroundColor(C.danger).borderRadius(10) .padding({ left: 5, right: 5, top: 2, bottom: 2 }).minFontSize(8) }if (c.unread > 0)有未读才显示红点——小明 2、小华 1 有红点,小美/小雪无。红点 = 红色圆形(borderRadius 10)+ 白字数字。.minFontSize(8)数字过大时自动缩小(如 "99+")——"未读数字自适应"。
- 昵称 + 时间(首行左右):小明 + 10:30
- 最后消息 + 在线状态(次行):"周末一起去打篮球吧?" + "在线"(绿)
在线状态三态:在线(绿 ok)/离线(灰 textDim)/忙碌(橙 warn)——"状态色三态"(聊天对象 4 人:2 在线/1 离线/1 忙碌)。
Stack({ alignContent: Alignment.TopEnd })未读红点贴在头像右上角(区别于首页的在线状态点 BottomEnd)——"未读红点 TopEnd、在线状态 BottomEnd"两种角标位置。
五、UnreadList:未读筛选
UnreadList(未读 Tab)过滤出有未读的聊天 + 未读/待处理的通知:
@Builder UnreadList() { Scroll() { Column({ space: 10 }) { ForEach(this.chats, (c: ChatItem) => { if (c.unread > 0) { this.ChatCard(c) } }, (c: ChatItem) => c.id.toString()) ForEach(this.notices, (n: NoticeItem) => { if (n.status === '待处理' || n.status === '未读') { this.NoticeCard(n) } }, (n: NoticeItem) => n.id.toString()) } .width('100%') .padding({ left: D.pad, right: D.pad, top: 12, bottom: D.pad + this.safeBottom + 20 }) } .layoutWeight(1).scrollBar(BarState.Off).align(Alignment.Top) }两种过滤:
ForEach(this.chats, (c: ChatItem) => { if (c.unread > 0) { this.ChatCard(c) } }, (c: ChatItem) => c.id.toString())- 聊天:
c.unread > 0(小明/小华 2 条) - 通知:
n.status === '待处理' || n.status === '未读'(好友申请 + 活动提醒 2 条)
"ForEach 内 if 过滤"是条件渲染的精简写法——不需要额外filtered()方法,直接在 ForEach 回调里if判断。"内联过滤"适合简单条件(一个 if),复杂过滤(多条件)建议抽方法。
六、FriendReqList:好友申请(通过/忽略双操作)
FriendReqList(好友申请 Tab)只显示"新的好友申请"通知,每条带通过/忽略按钮:
@Builder FriendReqList() { Scroll() { Column({ space: 10 }) { ForEach(this.notices, (n: NoticeItem) => { if (n.title === '新的好友申请') { Column({ space: 12 }) { Row({ space: 12 }) { Row() { Text(n.icon).fontSize(28) } .width(50).height(50).backgroundColor(C.primarySoft).borderRadius(25).justifyContent(FlexAlign.Center) Column({ space: 4 }) { Text(n.title).fontSize(14).fontWeight(FontWeight.Bold).fontColor(C.text) Text(n.desc).fontSize(12).fontColor(C.textDim) Text(n.time).fontSize(10).fontColor(C.textDim) }.alignItems(HorizontalAlign.Start).layoutWeight(1) Text(n.status).fontSize(11).fontColor(n.statusColor) .padding({ left: 8, right: 8, top: 4, bottom: 4 }) .backgroundColor(C.cardSoft).borderRadius(4) }.width('100%') Row({ space: 10 }) { Button('忽略').fontSize(13).fontColor(C.textSub).backgroundColor(C.cardSoft) .borderRadius(D.rMd).height(36).layoutWeight(1) .onClick(() => { promptAction.showToast({ message: '已忽略' }); }) Button('通过').fontSize(13).fontColor('#FFFFFF').backgroundColor(C.primary) .borderRadius(D.rMd).height(36).layoutWeight(1) .onClick(() => { promptAction.showToast({ message: '已通过好友申请' }); }) }.width('100%') } .width('100%').padding(14).backgroundColor(C.card).borderRadius(D.rLg).border({ width: 1, color: C.stroke }) } }, (n: NoticeItem) => n.id.toString()) } .width('100%') .padding({ left: D.pad, right: D.pad, top: 12, bottom: D.pad + this.safeBottom + 20 }) } .layoutWeight(1).scrollBar(BarState.Off).align(Alignment.Top) }"新的好友申请"过滤(n.title === '新的好友申请'匹配)+双操作按钮:
- 忽略(灰底灰字次级)— 拒绝申请
- 通过(粉红底白字主操作)— 接受申请
"通过/忽略"是好友申请的经典双操作(微信同款)。真实项目应实现真逻辑:通过 → 状态变"已通过"、双方成为好友、聊天页新增会话;忽略 → 状态变"已忽略"。
好友申请卡比 NoticeCard 更丰富(50×50 大图标 + 3 行文字 + 双按钮)——"同一条数据,不同分类不同形态":全部 Tab 显示精简版(NoticeCard),好友申请 Tab 显示操作版(FriendReqCard)——"按场景裁剪卡片"。
七、SystemList:系统通知
SystemList(系统通知 Tab)显示全部通知(与 NoticeCard 同款):
@Builder SystemList() { Scroll() { Column({ space: 10 }) { ForEach(this.notices, (n: NoticeItem) => { this.NoticeCard(n) }, (n: NoticeItem) => n.id.toString()) } .width('100%') .padding({ left: D.pad, right: D.pad, top: 12, bottom: D.pad + this.safeBottom + 20 }) } .layoutWeight(1).scrollBar(BarState.Off).align(Alignment.Top) }系统通知 Tab 展示全部 4 条通知(好友申请/活动提醒/匹配成功/系统升级)——"系统通知" = 通知全集。
4 个 Tab 的内容矩阵:
| Tab | 聊天 | 通知 | 卡片形态 |
|---|---|---|---|
| 全部 | 4 条 | 4 条 | ChatCard + NoticeCard |
| 未读 | 2 条(unread>0) | 2 条(待处理/未读) | ChatCard + NoticeCard |
| 好友申请 | — | 1 条(好友申请) | FriendReqCard(双按钮) |
| 系统通知 | — | 4 条 | NoticeCard |
**"按 Tab 裁剪内容 + 按场景定制卡片"**是消息中心的完整设计。
八、@State 与辅助方法的克制
Func2Tab 只有1 个 @State:currentTab(当前分类 0-3)。chats/notices/stats/tabs全部private不可变。
"1 个 @State 驱动 4 个列表切换"——currentTab一变,if/else重新渲染对应列表。"状态驱动视图切换"是消息中心的核心(比"4 个列表都渲染、隐藏 3 个"高效得多——"按需渲染"优于"全部渲染 + 显隐控制")。
辅助方法 = 0——过滤逻辑全部内联在 ForEach 的if里(简单条件不需要抽方法)。
九、消息中心的可复用模板
App 18 消息页的模板价值:
Header + 4 栏彩色统计 + 4 分类下划线 Tab + if/else 4 分支: - 全部:通知区 + 聊天区 - 未读:过滤未读聊天 + 未读通知 - 好友申请:申请卡(通过/忽略双按钮) - 系统通知:通知列表核心可复用组件:
- ChatCard(头像 + 未读红点 TopEnd + 昵称时间 + 最后消息 + 在线状态)——任何聊天 App
- NoticeCard(图标 + 标题 + 状态标签 + 描述 + 时间)——任何通知中心
- FriendReqCard(大图标 + 双操作按钮)——任何好友/申请场景
- Tab 真切换(
if/else分支)——任何多分类中心
这套"消息中心模板"可复用到:聊天 App、社交 App、协作工具(钉钉/飞书)、邮件客户端——任何"通知 + 聊天 + 待处理"的场景。
十、聊天卡片的信息架构
App 18 的 ChatCard 是"微信式聊天列表"的完整范本,信息架构值得逐层拆解:
@Builder ChatCard(c: ChatItem) { Row({ space: 12 }) { Stack({ alignContent: Alignment.TopEnd }) { Row() { Text(c.avatar).fontSize(32) } .width(50).height(50).backgroundColor(C.primarySoft).borderRadius(25).justifyContent(FlexAlign.Center) if (c.unread > 0) { Text(c.unread.toString()).fontSize(10).fontColor('#FFFFFF') .backgroundColor(C.danger).borderRadius(10) .padding({ left: 5, right: 5, top: 2, bottom: 2 }).minFontSize(8) } } ...一条聊天卡片承载 6 个信息维度(从"最重要"到"最次要"):
- 头像(最直观)— 识别"和谁聊"
- 未读红点(最紧急)— "有新消息"
- 昵称(第一行主信息)— "是谁"
- 时间(第一行次信息)— "什么时候"
- 最后消息(第二行主信息)— "聊了什么"
- 在线状态(第二行次信息)— "现在在吗"
"左头像右内容 + 上行昵称/时间下行消息/状态"是聊天列表的黄金布局(微信/QQ/钉钉全用这套)——"视觉层级":
- 第一眼:头像(大)
- 第二眼:红点(红色最显眼)
- 第三眼:昵称(加粗)
- 第四眼:最后消息(正文)
- 第五眼:时间/状态(小字)
"信息密度"控制:maxLines(1)最后消息截断(不换行),Ellipsis省略——"单行截断"保持列表整齐(聊天列表每行高度一致)。
"未读红点"是聊天列表的"注意力引擎"——2 个红点(小明 2/小华 1)引导用户"先处理这 2 个"——"红点 = 待办清单"(处理完红点消失,下次再有又出现)。
真实项目的聊天列表进阶:
- 置顶会话(重要的人置顶)
- 免打扰(红点变"免打扰图标")
- 草稿(有输入未发送显示"草稿")
十一、消息状态机的"已读/未读"流转
App 18 消息页的通知有 4 种状态(待处理/未读/已查看/已读),背后是消息状态机:
新消息 → 未读(status: '未读',未读统计 +1) → 用户打开 → 已读(status: '已读',未读统计 -1) 好友申请 → 待处理(需要行动) → 通过/忽略 → 已处理(从"好友申请"列表移除)"状态决定展示":
- 未读统计(StatOverview"未读消息 3")= 所有
unread > 0的聊天 +status: '待处理'/'未读'的通知 - 未读 Tab 只显示未读的(过滤)
- 好友申请 Tab 只显示"待处理"的申请(通过/忽略后移除)
"已读回执"是消息系统的核心——用户打开聊天 → 对方看到"已读"(微信的"已读/未读")——"状态流转驱动 UI"。
真实项目的消息状态:
- 送达状态:发送中 → 已送达 → 已读(单/双勾)
- 通知状态:未读 → 已读(红点消失)
- 申请状态:待处理 → 已通过/已忽略
"消息状态机"是消息系统的灵魂——App 18 简化了状态流转(未读固定 3 不变),真实项目要"点开即已读"(onClick清 unread)。
十二、消息中心的"双列表分区"设计
App 18 全部 Tab 的"通知区 + 聊天区"双分区,是消息中心的经典信息架构:
@Builder AllList() { Scroll() { Column({ space: 12 }) { this.NoticeSection() this.ChatSection() } ..."通知在上、聊天在下"(NoticeSection 先渲染、ChatSection 后渲染)——**"系统消息优先、个人消息次要"**的阅读顺序(微信的"服务通知"置顶、聊天在下的同款逻辑)。
NoticeSection / ChatSection 两个分区标题("通知"/"聊天")——"分区标题 = 内容导航",用户滚动时知道"现在看到哪类"。
双分区的价值:
- 类型分离——系统通知(活动/升级)与个人聊天(小明/小美)分开放,不混在一起
- 注意力分配——通知区少(4 条)聊天区多(4 条),用户先看通知(可能有时效性)再看聊天
- 扩展性——未来加"群聊"区、"订阅号"区,只需再加一个 Section
"双分区 + 分区标题"模板可复用到:邮件客户端(收件箱/订阅)、电商 App(订单/优惠券)、工作台(待办/已办)——任何"多类型列表"场景。
十三、总结
App 18 消息中心页解析完毕。4 分类真切换(if/else 条件渲染)+ 未读红点角标(Stack TopEnd + minFontSize)+ 好友申请双操作 + 在线状态三色是四大亮点。"按场景裁剪卡片"(同一数据不同 Tab 不同形态)是本页最值得学习的设计。