news 2026/9/7 17:25:47

鸿蒙原生应用 ArkUI 实战:条件渲染驱动消息中心页四分类真切换与未读红点

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
鸿蒙原生应用 ArkUI 实战:条件渲染驱动消息中心页四分类真切换与未读红点

鸿蒙原生应用 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 要素(标准微信风格):

  1. 头像 + 未读红点(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+")——"未读数字自适应"

  1. 昵称 + 时间(首行左右):小明 + 10:30
  2. 最后消息 + 在线状态(次行):"周末一起去打篮球吧?" + "在线"(绿)

在线状态三态:在线(绿 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 个 @StatecurrentTab(当前分类 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 分支: - 全部:通知区 + 聊天区 - 未读:过滤未读聊天 + 未读通知 - 好友申请:申请卡(通过/忽略双按钮) - 系统通知:通知列表

核心可复用组件

  1. ChatCard(头像 + 未读红点 TopEnd + 昵称时间 + 最后消息 + 在线状态)——任何聊天 App
  2. NoticeCard(图标 + 标题 + 状态标签 + 描述 + 时间)——任何通知中心
  3. FriendReqCard(大图标 + 双操作按钮)——任何好友/申请场景
  4. 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 个信息维度(从"最重要"到"最次要"):

  1. 头像(最直观)— 识别"和谁聊"
  2. 未读红点(最紧急)— "有新消息"
  3. 昵称(第一行主信息)— "是谁"
  4. 时间(第一行次信息)— "什么时候"
  5. 最后消息(第二行主信息)— "聊了什么"
  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 两个分区标题("通知"/"聊天")——"分区标题 = 内容导航",用户滚动时知道"现在看到哪类"。

双分区的价值

  1. 类型分离——系统通知(活动/升级)与个人聊天(小明/小美)分开放,不混在一起
  2. 注意力分配——通知区少(4 条)聊天区多(4 条),用户先看通知(可能有时效性)再看聊天
  3. 扩展性——未来加"群聊"区、"订阅号"区,只需再加一个 Section

"双分区 + 分区标题"模板可复用到:邮件客户端(收件箱/订阅)、电商 App(订单/优惠券)、工作台(待办/已办)——任何"多类型列表"场景

十三、总结

App 18 消息中心页解析完毕。4 分类真切换(if/else 条件渲染)+ 未读红点角标(Stack TopEnd + minFontSize)+ 好友申请双操作 + 在线状态三色是四大亮点。"按场景裁剪卡片"(同一数据不同 Tab 不同形态)是本页最值得学习的设计。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 17:24:11

Git冲突解决实战:从理解合并本质到从容处理代码分歧

1. 先别急着学命令,把「冲突」这件事想明白很多人一遇到 Git 冲突就条件反射地开始背命令,git merge --abort、git checkout --ours、git rebase --continue——仿佛冲突是个 Bug,只要命令用得够快,它就会消失。但我做了几年的代码…

作者头像 李华
网站建设 2026/9/7 17:23:32

LangChain多智能体实战:用LangGraph编排Agent构建婚礼策划系统

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 17:23:23

FlagOS深度解析:一套开源软件栈如何打通异构算力算子库

算力荒折腾了两年,我现在最深的体会是:大模型训练和部署的瓶颈早就不单是卡本身,而是软件栈被各家芯片厂商牢牢锁死。A卡一个生态,B卡一个生态,C卡又是一个半成品生态,每换一批卡就要把框架、算子、通信库重…

作者头像 李华
网站建设 2026/9/7 17:21:52

AI辅助FPGA开发实测:从Vivado报错到RTL生成的边界

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 17:21:48

Stable Diffusion与LoRA模型实现AI角色随机生成技术详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华