鸿蒙原生应用 HarmonyOS ArkTS:社交匹配首页的环形进度与在线状态点
App 18「校园社交匹配」首页(HomeTab),主题色
#EC4899粉红,4 个 Tab 分别为首页(💞)、发现(🔍)、消息(💬)、我的(👤)。首页采用固定 Header + 粉红渐变推荐卡(今日推荐/同好匹配数/智能匹配说明)+独立搜索栏+ 3 栏统计 + 4 个圆形入口 + 本周匹配趋势条形图 +4 张推荐同好卡(匹配度环形进度 + 在线状态点 + 兴趣标签 + 跳过/打招呼双按钮)+ 兴趣排行榜(带涨跌箭头)。本篇基于18-social-match/entry/src/main/ets/pages/HomeTab.ets(约 244 行)逐段拆解,附 4 张实机截图。
一、整体结构:8 个 @Builder 的内容流
HomeTab 在系列首页模板基础上新增了 2 个模块(独立 SearchBar、PersonList),共 8 个 @Builder:
build() { Column() { this.Header() Scroll() { Column({ space: 14 }) { this.HeroCard() this.SearchBar() this.StatRow() this.QuickActionRow() this.WeekChart() this.SectionTitle('推荐同好') this.PersonList() this.SectionTitle('兴趣排行') this.RankList() } .width('100%') .padding({ left: D.pad, right: D.pad, top: 14, bottom: D.pad + this.safeBottom + 20 }) } .layoutWeight(1).scrollBar(BarState.Off).align(Alignment.Top) } .width('100%').height('100%').backgroundColor(C.bg) }8 个块按"品牌 → 搜索 → 数据 → 入口 → 图表 → 推荐 → 排行"叙事:
- Header— 校园交友 + Slogan(品牌区)
- HeroCard— 今日推荐渐变卡(情感化)
- SearchBar— 独立搜索栏(区别于嵌在 HeroCard 里的 App 14/15/16)
- StatRow— 3 栏统计(今日匹配/好友总数/互动次数)
- QuickActionRow— 4 个圆形入口(找同好/兴趣群/活动/匿名墙)
- WeekChart— 本周匹配趋势条形图
- SectionTitle('推荐同好') + PersonList— 4 张同好卡(本页核心)
- SectionTitle('兴趣排行') + RankList— 4 条兴趣排行(带涨跌)
对比 App 13/17(音乐/志愿)首页:App 13/17 是"Hero 兼搜索",App 18 是"独立 SearchBar"——独立搜索栏更醒目(搜索是社交 App 的高频操作,值得独立占位)。
项目源码开源:https://gitee.com/codenestFlow/HarmonyOSHub
二、Header + HeroCard:社交品牌区
Header 是"校园交友/找到兴趣相投的小伙伴 💞"两行品牌区(与 App 14/15/16 同构)。HeroCard 是横向布局的情感化推荐卡:
@Builder HeroCard() { Row({ space: 14 }) { Column({ space: 6 }) { Text('今日推荐').fontSize(12).fontColor('#FFFFFF').opacity(0.85) Text('4 位同好等你匹配').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF') Text('基于兴趣标签智能匹配').fontSize(11).fontColor('#FFFFFF').opacity(0.75) }.alignItems(HorizontalAlign.Start).layoutWeight(1) Text('💞').fontSize(44) } .width('100%').padding(20).borderRadius(D.rLg) .linearGradient({ angle: 135, colors: [[C.primary, 0.0], [C.accent, 1.0]] }) .onClick(() => { promptAction.showToast({ message: '刷新推荐' }); }) }"今日推荐 / 4 位同好等你匹配 / 基于兴趣标签智能匹配"三行——标题 + 主信息 + 说明的"情感化文案"结构。与 App 14 的"运动会报名"、App 15 的"减碳种树"、App 16 的"限时抽奖"一样——HeroCard 承载"这个 App 的核心价值主张"。
横向布局(Row 而非 Column):左 3 行文案 + 右 💞 大图标(44sp)。大 emoji 让 HeroCard 更活泼——社交 App 需要"亲和力"。
onClick(() => { promptAction.showToast({ message: '刷新推荐' }); })整卡可点("刷新推荐"交互占位)。
三、SearchBar:独立搜索栏
SearchBar 是独立于 HeroCard 的搜索栏(社交 App 的特色):
@Builder SearchBar() { Row({ space: 8 }) { Text('🔍').fontSize(16).margin({ left: 12 }) TextInput({ placeholder: '搜索昵称、兴趣、学院...', text: this.searchText }) .onChange((val: string) => { this.searchText = val; }) .backgroundColor('transparent').borderRadius(D.rMd).height(40) .layoutWeight(1).placeholderColor(C.textDim).placeholderFont({ size: 13 }) } .width('100%').backgroundColor(C.card).borderRadius(D.rMd).border({ width: 1, color: C.stroke }) }"🔍 图标 + TextInput 透明背景"的组合——TextInput 用backgroundColor('transparent')透明,🔍 图标放左边。占位 "搜索昵称、兴趣、学院..."明确搜索范围(3 个维度)——"占位符 = 搜索提示"。
@State searchText: string = ''双向绑定——输入实时更新。真实项目应联动 PersonList 过滤(按昵称/标签/学院模糊匹配)。
"独立搜索栏" vs "Hero 内嵌搜索":App 14/15/16 的搜索在 HeroCard 里(半透明白),App 18 的搜索独立成白色卡片——独立搜索栏更通用(微信/抖音的搜索都在顶部独立)。
四、StatRow + QuickActionRow:数据与入口
StatRow 是 3 栏统计(今日匹配 4/好友总数 56/互动次数 128),数字全部C.primary粉红加粗:
@Builder StatRow() { Row() { ForEach(this.stats, (s: StatItem) => { Column({ space: 4 }) { Text(s.value).fontSize(20).fontWeight(FontWeight.Bold).fontColor(C.primary) Text(s.label).fontSize(11).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 }) }与 App 14/15/16 的差异:App 14/15/16 统计是"3 栏彩色"(每个数字不同色),App 18 是"3 栏同粉红"——社交 App 用统一主题色(数字都是品牌色,强化"粉红=社交"的调性)。"统计色策略"跟随业务。
数据一致性:今日匹配 4 = HeroCard"4 位同好"、好友总数 56 = 我的页"好友 56"、互动次数 128 = 我的页"互动 128"——跨页数据一致✓。
QuickActionRow(4 个圆形入口:找同好🔍/兴趣群👥/活动🎉/匿名墙💌):
@Builder QuickActionRow() { Row() { ForEach(this.actions, (a: QuickAction) => { Column({ space: 8 }) { Row() { Text(a.icon).fontSize(22) } .width(48).height(48).backgroundColor(C.primarySoft).borderRadius(24).justifyContent(FlexAlign.Center) Text(a.label).fontSize(11).fontColor(C.textSub) }.layoutWeight(1).onClick(() => { promptAction.showToast({ message: a.label }); }) }, (a: QuickAction) => a.id.toString()) } .width('100%').padding({ top: 14, bottom: 14 }) .backgroundColor(C.card).borderRadius(D.rMd).border({ width: 1, color: C.stroke }) }48×48 正圆(borderRadius(24))+C.primarySoft浅粉红底——"4 个社交功能入口"(找同好/兴趣群/活动/匿名墙)覆盖社交 App 的核心玩法。
五、WeekChart:本周匹配趋势
WeekChart 与系列周图同构(7 天条形图),但柱体渲染更简洁:
@Builder WeekChart() { Column({ space: 10 }) { Row() { Text('本周匹配趋势').fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text) Blank() Text('共 500 次').fontSize(11).fontColor(C.textDim) }.width('100%') Row() { ForEach(this.weekBars, (b: BarItem) => { Column({ space: 6 }) { Column().width(14).height(b.value * 0.7).backgroundColor(C.primary).borderRadius(4) Text(b.day).fontSize(10).fontColor(C.textDim) }.layoutWeight(1) }, (b: BarItem) => b.day) }.width('100%').alignItems(VerticalAlign.Bottom).height(90) } .width('100%').padding(14).backgroundColor(C.card).borderRadius(D.rMd).border({ width: 1, color: C.stroke }) }差异点:
height(b.value * 0.7)直接用倍数(60×0.7=42vp)而非value / max * maxHeight归一化——**"固定倍率"**更简单(max=100 时 100×0.7=70vp)- 纯色柱(
C.primary单色)而非渐变柱——简化版 alignItems(VerticalAlign.Bottom)+ 容器height(90)让柱子从底部"长"起来
数据:一 60/二 80/三 45/四 90/五 70/六 100/日 55——周六 100 最高(周末社交高峰)、周三 45 最低(工作日低谷)——"周末匹配多"符合社交规律。
"共 500 次":60+80+45+90+70+100+55 = 500——汇总严格等于各柱之和✓。
六、PersonList:推荐同好卡(本页核心)
PersonList 是 4 张推荐同好卡,每张含匹配度环形进度、在线状态点、兴趣标签、跳过/打招呼双按钮:
@Builder PersonList() { Column({ space: 12 }) { ForEach(this.persons, (p: Person) => { Column({ space: 12 }) { Row({ space: 12 }) { Stack({ alignContent: Alignment.BottomEnd }) { Row() { Text(p.avatar).fontSize(36) } .width(56).height(56).backgroundColor(C.primarySoft).borderRadius(28).justifyContent(FlexAlign.Center) Row() { Text('').width(12).height(12).backgroundColor(p.online ? C.ok : C.textDim).borderRadius(6).border({ width: 2, color: C.card }) } } Column({ space: 4 }) { Row({ space: 6 }) { Text(p.name).fontSize(16).fontWeight(FontWeight.Bold).fontColor(C.text) Text('⭐' + p.rating + '.0').fontSize(10).fontColor(C.warn) } Text(p.dept).fontSize(12).fontColor(C.textDim) }.alignItems(HorizontalAlign.Start).layoutWeight(1) Stack({ alignContent: Alignment.Center }) { Progress({ value: p.match, total: 100, type: ProgressType.Ring }).width(44).height(44).color(C.primary).style({ strokeWidth: 4 }) Text(p.match + '%').fontSize(11).fontColor(C.primary).fontWeight(FontWeight.Bold) } }.width('100%') Row({ space: 8 }) { ForEach(p.tags, (t: string, idx: number) => { Text(t).fontSize(11).fontColor(C.primary) .padding({ left: 8, right: 8, top: 4, bottom: 4 }) .backgroundColor(C.primarySoft).borderRadius(4) }, (t: string, idx: number) => t + idx) }.width('100%') Row({ space: 12 }) { Button('跳过').fontSize(13).fontColor(C.textSub).backgroundColor(C.cardSoft) .borderRadius(D.rMd).height(38).layoutWeight(1) .onClick(() => { promptAction.showToast({ message: '已跳过' }); }) Button('打招呼').fontSize(13).fontColor('#FFFFFF').backgroundColor(C.primary) .borderRadius(D.rMd).height(38).layoutWeight(1) .onClick(() => { promptAction.showToast({ message: '打招呼: ' + p.name }); }) }.width('100%') } .width('100%').padding(14).backgroundColor(C.card).borderRadius(D.rLg).border({ width: 1, color: C.stroke }) }, (p: Person) => p.id.toString()) }.width('100%') }6.1 匹配度环形进度(ProgressType.Ring——本页最大亮点)
Stack({ alignContent: Alignment.Center }) { Progress({ value: p.match, total: 100, type: ProgressType.Ring }).width(44).height(44).color(C.primary).style({ strokeWidth: 4 }) Text(p.match + '%').fontSize(11).fontColor(C.primary).fontWeight(FontWeight.Bold) }ProgressType.Ring是环形进度——本系列第一次用环形(之前全是 Linear 线性):
value: p.match(匹配度 92/85/78/80)total: 100(百分比总量).style({ strokeWidth: 4 })— 环粗细 4vpStack中心叠Text(p.match + '%')—"环形 + 中间数字"双通道
匹配度 92% > 85% > 80% > 78%——4 个人匹配度递减,用户一眼看出"谁和我最合拍"。
环形 vs 线性:环形适合"单个百分比强调"(匹配度是卡片的核心指标,环形让它最醒目),线性适合"多条对比"(周图 7 根柱子)。"环形单点强调、线性批量对比"是图表选择原则。
6.2 在线状态点(Stack BottomEnd)
Stack({ alignContent: Alignment.BottomEnd }) { Row() { Text(p.avatar).fontSize(36) } .width(56).height(56).backgroundColor(C.primarySoft).borderRadius(28).justifyContent(FlexAlign.Center) Row() { Text('').width(12).height(12).backgroundColor(p.online ? C.ok : C.textDim).borderRadius(6).border({ width: 2, color: C.card }) } }头像右下角叠 12×12 状态点(Stack BottomEnd):
- 在线 →
C.ok绿色 - 离线 →
C.textDim灰色 border({ width: 2, color: C.card })白色描边让绿点"浮"在头像上
"绿点=在线、灰点=离线"是社交 App 的标准(微信/QQ 同款)。4 个人:3 在线 1 离线(小明/小美/小雪在线,小华离线)——数据有区分度。
6.3 姓名 + 星级 + 学院
Row({ space: 6 }) { Text(p.name).fontSize(16).fontWeight(FontWeight.Bold).fontColor(C.text) Text('⭐' + p.rating + '.0').fontSize(10).fontColor(C.warn) } Text(p.dept).fontSize(12).fontColor(C.textDim)⭐5.0用'⭐' + rating + '.0'字符串拼接(简化星级,不用 5 颗星)——**"数字星级"vs"星形星级"**两种写法(App 14 用 ★★★★☆,App 18 用 ⭐5.0)——App 18 更省空间(配合环形进度)。
6.4 兴趣标签 + 双按钮
兴趣标签(ForEach(p.tags)):粉红字 + 浅粉红底小胶囊——编程/动漫/篮球等,"兴趣即匹配依据"(用户一眼看到"我和他有哪些共同兴趣")。
双按钮:
Row({ space: 12 }) { Button('跳过').fontSize(13).fontColor(C.textSub).backgroundColor(C.cardSoft) .borderRadius(D.rMd).height(38).layoutWeight(1) .onClick(() => { promptAction.showToast({ message: '已跳过' }); }) Button('打招呼').fontSize(13).fontColor('#FFFFFF').backgroundColor(C.primary) .borderRadius(D.rMd).height(38).layoutWeight(1) .onClick(() => { promptAction.showToast({ message: '打招呼: ' + p.name }); }) }- 跳过(灰底灰字次级)— "不喜欢这个人"
- 打招呼(粉红底白字主操作)— "主动建立联系"
"跳过/打招呼"是交友 App 的核心双操作(Soul/探探的"左滑/右滑"实体按钮版)。"主操作 + 次操作"的视觉权重差异(粉红 vs 灰)引导用户点"打招呼"。
七、RankList:兴趣排行(带涨跌)
RankList 是 4 条兴趣排行榜,带排名数字、人数、涨跌箭头:
@Builder RankList() { Column({ space: 0 }) { ForEach(this.ranks, (r: RankItem, idx: number) => { Row({ space: 12 }) { Text(r.rank.toString()).fontSize(16).fontWeight(FontWeight.Bold) .fontColor(r.rank <= 3 ? C.primary : C.textDim).width(28).textAlign(TextAlign.Center) Text(r.name).fontSize(14).fontColor(C.text).layoutWeight(1) Text(r.count + ' 人').fontSize(12).fontColor(C.textSub) Text(r.trend).fontSize(14).fontColor(r.trend === '↑' ? C.ok : C.danger).margin({ left: 8 }) } .width('100%').padding({ left: 14, right: 14, top: 12, bottom: 12 }) if (idx < this.ranks.length - 1) { Divider().color(C.stroke).strokeWidth(0.5) } }, (r: RankItem) => r.id.toString()) } .width('100%').backgroundColor(C.card).borderRadius(D.rMd).border({ width: 1, color: C.stroke }) }4 条兴趣:编程 234 人↑ / 动漫 198 人↑ / 篮球 156 人↓ / 音乐 142 人↑。
涨跌箭头:
Text(r.trend).fontSize(14).fontColor(r.trend === '↑' ? C.ok : C.danger).margin({ left: 8 })- ↑ 涨→
C.ok绿色 - ↓ 跌→
C.danger红色
"↑绿 ↓红"涨跌色是金融/榜单的标准(股票涨绿跌红在中国市场,涨红跌绿在国外——App 18 用"↑绿 ↓红"是国际惯例还是中国惯例?其实中国股市是"红涨绿跌",App 18 用了"绿涨红跌"(国际惯例)——值得注意的设计选择)。
排名数字 1-4 用r.rank字段(而非 idx+1)——"rank 字段 vs 下标":字段更灵活(可以跳号),下标更省事。前 3 名粉红加粗(r.rank <= 3),第 4 名灰色——"前三名高亮"。
Divider().color(C.stroke).strokeWidth(0.5)0.5vp 超细分隔线(比默认 1vp 更精致)——"细分隔线 = 高级感"。
八、@State 与数据一致性
HomeTab 只有1 个 @State:searchText(搜索框文字)。其余数据private不可变。
4 组数据跨页一致:
- 好友总数 56 = 我的页"好友 56"
- 互动次数 128 = 我的页"互动 128"
- 4 位同好 = 我的页"活动 12"?(不直接对应,但"今日匹配 4"= HeroCard"4 位同好")
- 小明/小美/小华/小雪 = 消息页聊天对象同名 ✓
"推荐同好的 4 个人(小明/小美/小华/小雪)与消息页的 4 个聊天对象同名同头像"——跨页人物一致是社交 App 的数据闭环(首页推荐的人 → 消息页可以聊天)。
九、推荐卡的数据编排与"匹配度"叙事
App 18 首页 4 张推荐同好卡的数据编排非常讲究"匹配度叙事":
private persons: Person[] = [ { id: 1, avatar: '🧑💻', name: '小明', dept: '计算机学院', tags: ['编程', '动漫', '篮球'], match: 92, rating: 5, online: true }, { id: 2, avatar: '👩🎨', name: '小美', dept: '艺术学院', tags: ['绘画', '音乐', '旅行'], match: 85, rating: 4, online: true }, { id: 3, avatar: '🧑🔬', name: '小华', dept: '化学学院', tags: ['摄影', '电影', '桌游'], match: 78, rating: 4, online: false }, { id: 4, avatar: '👩📚', name: '小雪', dept: '文学院', tags: ['阅读', '写作', '烘焙'], match: 80, rating: 5, online: true } ];4 人的"匹配度梯度":92% → 85% → 80% → 78% 递减——"最合拍的排最前"(推荐排序 = 匹配度排序)。用户从上往下看,匹配度从高到低,"最好的先看"是推荐列表的核心逻辑。
4 人的"差异化":
- 头像不同:🧑💻(程序员)/👩🎨(画家)/🧑🔬(科学家)/👩📚(阅读者)——"头像即人设"
- 学院不同:计算机/艺术/化学/文学院——"跨学院推荐"(社交 App 的推荐广度)
- 兴趣不同:编程/动漫/篮球 vs 绘画/音乐/旅行——"标签差异化"
- 状态不同:3 在线 1 离线——"状态差异化"
"匹配度"的算法暗示:92% 的小明(计算机学院 + 编程/篮球)与用户"匹配度最高"——**"匹配度 = 学院 + 兴趣标签的重合度"**的视觉暗示。真实项目里,匹配度由算法计算(兴趣重合/学院距离/活跃度加权),demo 用固定值演示"推荐排序"。
"匹配度叙事"的可扩展:真实项目应支持"排序切换"(按匹配度/按距离/按活跃度排序)、"筛选"(只看在线/只看同学院)、"滑动卡片"(左滑跳过/右滑打招呼的 Tinder 式交互)——"推荐列表"是社交 App 的核心,demo 是简化版。
十、排行榜的"涨跌"语义与数据治理
App 18 兴趣排行榜的"↑绿 ↓红"涨跌箭头,是**"趋势数据"的视觉表达**:
private ranks: RankItem[] = [ { id: 1, rank: 1, name: '编程', count: 234, trend: '↑' }, { id: 2, rank: 2, name: '动漫', count: 198, trend: '↑' }, { id: 3, rank: 3, name: '篮球', count: 156, trend: '↓' }, { id: 4, rank: 4, name: '音乐', count: 142, trend: '↑' } ];"编程/动漫/音乐"涨、"篮球"跌——3 涨 1 跌让排行"有变化感"(不全是↑也不全是↓)。"涨跌混合"是排行榜的真实形态(全部↑会显得假)。
trend: '↑'用箭头字符串做字段——渲染时r.trend === '↑' ? C.ok : C.danger判断颜色——"字符串枚举 + 三元着色"。真实项目应存布尔(isRising: boolean)或数字(trendDelta: +22)更严谨——"箭头字符串"是 demo 的简化(真实项目要存涨跌幅数值才能显示"+22%")。
排行榜的"数据驱动排序":真实项目排行应后端实时计算(按兴趣人数倒序),demo 是静态数组。"编程 234 人"是校园编程社的规模暗示——"排行即校园兴趣生态"。
注意:App 18 的排行是"兴趣排行榜"(编程/动漫/篮球/音乐),而 App 13-17 是"商品/场地/分类排行榜"——"排行对象 = 业务核心对象"(社交 App 排行兴趣,电商排商品)。**"排行组件模板复用、排行数据按业务换"**是本系列排行页的通用模式。
十一、首页与消息页的"人物闭环"
App 18 有一个前几代没有的跨页人物闭环:
首页推荐的 4 个人(小明/小美/小华/小雪)→ 消息页的 4 个聊天对象(同名同头像):
首页(推荐卡:小明 92% 匹配度 + 打招呼按钮) → 点击"打招呼" → 消息页(小明聊天:未读 2 条) → 进入聊天 → 好友关系建立 → 我的页(好友 56 / 互动 128 统计)"推荐 → 打招呼 → 聊天 → 好友"是社交 App 的完整用户旅程——App 18 用"同名同头像"在 3 个页面暗示这条旅程(首页推荐的人、消息页可聊天的人、我的页统计的人)。"跨页人物一致"让 demo 的社交生态"自洽可信"。
真实项目的实现:用户模型(UserProfile)全局共享——首页推荐列表、消息页会话列表、我的页好友统计都引用同一批用户数据,"单一数据源"原则。
十二、总结
App 18 首页解析完毕。匹配度环形进度(ProgressType.Ring)+ 在线状态点 + 兴趣标签 + 跳过/打招呼双按钮 + 涨跌排行是五大亮点。独立搜索栏(区别于系列内嵌搜索)和粉红社交调性让首页区别于 App 13-17。