鸿蒙原生应用 HarmonyOS ArkTS:社交匹配首页的环形进度与在线状态点
2026/9/7 18:10:44 网站建设 项目流程

鸿蒙原生应用 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 个块按"品牌 → 搜索 → 数据 → 入口 → 图表 → 推荐 → 排行"叙事

  1. Header— 校园交友 + Slogan(品牌区)
  2. HeroCard— 今日推荐渐变卡(情感化)
  3. SearchBar— 独立搜索栏(区别于嵌在 HeroCard 里的 App 14/15/16)
  4. StatRow— 3 栏统计(今日匹配/好友总数/互动次数)
  5. QuickActionRow— 4 个圆形入口(找同好/兴趣群/活动/匿名墙)
  6. WeekChart— 本周匹配趋势条形图
  7. SectionTitle('推荐同好') + PersonList— 4 张同好卡(本页核心
  8. 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 })— 环粗细 4vp
  • Stack中心叠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 个 @StatesearchText(搜索框文字)。其余数据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。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询