HarmonyOS 购物商城:消息中心页面

📅 2026/7/9 23:20:32
HarmonyOS 购物商城:消息中心页面
前言消息中心是 App 和用户沟通的窗口——订单状态变了通知一声有优惠活动推送一条物流更新提醒一下。用户不一定天天打开 App但消息推送能把用户拉回来。这篇文章拆解消息中心的实现消息列表、未读状态、角标计数、促销卡片。完整效果数据模型interfaceMsgItem{icon:string// emoji 图标title:string// 标题sub:string// 副标题/预览time:string// 时间unread:boolean// 是否未读color:string// 图标背景色count:number// 未读数量}每个消息项包含 7 个字段。color是图标背景色——不同类型的消息用不同颜色区分系统通知红色、物流蓝色、促销黄色。消息数据Statemessages:MsgItem[][{icon:,title:系统通知,sub:您的订单已发货...,time:10:32,unread:true,color:#FF6B6B,count:3},{icon:,title:优惠促销,sub:限时秒杀...,time:昨天,unread:true,color:#FECA57,count:5},{icon:,title:物流助手,sub:快递已签收...,time:昨天 14:20,unread:false,color:#5DADE2,count:0},// ...]7 条消息3 条未读。数据是硬编码的——实际项目中从 API 加载。消息类型的颜色编码 系统通知红色#FF6B6B——紧急 优惠促销黄色#FECA57——吸引 物流助手蓝色#5DADE2——中性❤️ 活动通知橙色#E8734A——温暖⭐ 评价邀请黄色#FECA57——轻量 客服消息绿色#00B894——友好 店铺动态紫色#8A5DFA——品牌头部Row(){Row(){SymbolGlyph($r(sys.symbol.chevron_left)).fontSize(20).fontColor([T1])}.width(34).height(34).borderRadius(17).backgroundColor(rgba(0,0,0,0.03)).justifyContent(FlexAlign.Center).onClick((){router.back()})Text(消息中心).fontSize(22).fontWeight(FontWeight.Bold).fontColor(T1).margin({left:10}).layoutWeight(1)Text(全部已读).fontSize(13).fontColor(A).fontWeight(FontWeight.Medium)}头部三个元素返回按钮 标题 全部已读按钮。全部已读的实现Text(全部已读).fontSize(13).fontColor(A).fontWeight(FontWeight.Medium)目前只是显示文字没有绑定onClick。实际项目中需要遍历所有消息把unread设为falsecount设为0。消息列表ForEach(this.messages,(msg:MsgItem,idx:number){Column(){Row(){// 图标角标// 消息信息// 箭头}if(idxthis.messages.length-1){Divider().strokeWidth(0.5).color(#F0EEF4).margin({left:78,right:16})}}.width(100%).backgroundColor(CW).onClick((){this.openChat(msg)})})每条消息是一个 Row点击后调用openChat。分割线的左边距Divider().strokeWidth(0.5).color(#F0EEF4).margin({left:78,right:16})分割线左边距 78px——跳过图标区域50px 图标 12px 间距 16px 内边距 ≈ 78px。这样分割线不会穿过图标视觉更整洁。图标角标Stack({alignContent:Alignment.TopEnd}){Column(){Text(msg.icon).fontSize(22)}.width(50).height(50).borderRadius(15).backgroundColor(msg.color15).justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)if(msg.count0){Row(){Text(msg.count99?99:msg.count.toString()).fontSize(9).fontWeight(FontWeight.Bold).fontColor(Color.White)}.width(18).height(18).borderRadius(9).backgroundColor(A).justifyContent(FlexAlign.Center).alignItems(VerticalAlign.Center).position({x:-2,y:-4})}}图标的结构Stack 叠加 [彩色背景圆角方块 emoji] 和 [红色圆形计数]。背景色的透明度处理.backgroundColor(msg.color15)msg.color是#FF6B6B拼上15变成#FF6B6B15——这是 8 位 hex 颜色后两位15是 alpha 通道约 8% 不透明度。效果是极淡的彩色背景。计数的position.position({x:-2,y:-4})计数徽章在图标右上角偏左 2px、偏上 4px。这个位置是手动调的——太右会超出容器太下会挡住 emoji。99 的处理msg.count99?99:msg.count.toString()超过 99 显示99——避免角标太宽撑坏布局。消息信息Column(){Row(){Text(msg.title).fontSize(15).fontWeight(msg.unread?FontWeight.Bold:FontWeight.Medium).fontColor(msg.unread?T1:T2).layoutWeight(1)Text(msg.time).fontSize(11).fontColor(T3)}Text(msg.sub).fontSize(12).fontColor(T2).maxLines(2).textOverflow({overflow:TextOverflow.Ellipsis}).margin({top:4})}.alignItems(HorizontalAlign.Start).margin({left:12}).layoutWeight(1)未读状态的视觉区分未读标题粗体 深色T1已读标题中等字重 灰色T2用户一眼就能看出哪些消息没看过。openChatprivateopenChat(msg:MsgItem):void{msg.unreadfalsemsg.count0router.pushUrl({url:pages/MessageDetail,params:{title:msg.title,sub:msg.sub,time:msg.time,icon:msg.icon,color:msg.color}asRecordstring,Object})}点击消息时把unread设为falsecount设为0——标记已读跳转 MessageDetail 页面传递消息信息直接修改msg.unread有效吗msg是this.messages数组中的对象引用。直接修改msg.unread会修改原数组——但State不会检测到对象内部的变化UI 不会自动更新。这是一个潜在的 bug——点击后标题不会从粗体变回普通字重。需要refresh()或者重新赋值this.messages [...this.messages]。促销卡片Column(){Row(){Column(){}.width(100%).height(80).borderRadius(16).linearGradient({angle:135,colors:[[A,0],[#FF9F43,1]]})Column(){Text( 618年中大促).fontSize(16).fontWeight(FontWeight.Bold).fontColor(Color.White)Text(全场低至1折 · 领券再减).fontSize(11).fontColor(rgba(255,255,255,0.7)).margin({top:4})}.position({x:16,y:16})}.width(100%).height(80)}.onClick((){router.pushUrl({url:pages/ProductList,params:{category:全部分类}asRecordstring,Object})})消息列表底部加了一个促销卡片——渐变背景 文案点击跳转商品列表。促销卡片的位置它在ForEach之后、Scroll之内——用户滚到消息列表底部才能看到。这是一个钩子——用户看完所有消息后看到促销卡片可能点进去逛逛。为什么用 Stack 结构的 Row渐变背景是一个空 Column文字用position({ x: 16, y: 16 })定位在左上角。这样文字浮在渐变背景上方视觉效果像一个完整的横幅。踩坑记录1. 消息已读的状态同步msg.unreadfalsemsg.count0直接修改对象属性不会触发State更新。需要privateopenChat(msg:MsgItem,idx:number):void{constupdated[...this.messages]updated[idx]{...msg,unread:false,count:0}this.messagesupdated router.pushUrl(...)}2. 分割线的左边距计算Divider().margin({left:78,right:16})78px 16pxRow padding-left 50px图标宽度 12px图标和文字间距。如果图标宽度或间距变了分割线的左边距也要跟着调。3. 消息列表的性能7 条消息用ForEach一次性渲染没问题。但如果消息数量多100需要考虑懒加载。4. 全部已读按钮的实现目前全部已读没有onClick。实现思路Text(全部已读).onClick((){for(leti0;ithis.messages.length;i){this.messages[i].unreadfalsethis.messages[i].count0}this.messages[...this.messages]// 触发 UI 更新})5. 促销卡片的点击跳转.onClick((){router.pushUrl({url:pages/ProductList,params:{category:全部分类}asRecordstring,Object})})促销卡片跳转到商品列表页——和首页底部导航的分类按钮一样的目标。用户从消息中心进入购物场景。写在最后消息中心的设计核心是信息分层——未读消息用粗体和角标吸引注意力已读消息退居二线。颜色编码帮助用户快速识别消息类型红色紧急蓝色物流黄色促销。促销卡片放在消息列表底部是一个巧妙的设计——用户看完所有消息后自然看到促销从查看通知无缝切换到浏览商品。消息中心不只是通知的容器也是流量的入口。