鸿蒙原生应用实战:不引图表库 —— 兴趣匹配匹配页三列统计卡与契合度列表

App 39「校园兴趣匹配」匹配页(Func2Tab),主题色 #FF6B9D 粉色(pink),4 个 Tab 分别为首页(💖)、兴趣(🎯)、匹配(🔗)、我的(👤)。匹配页采用"Header + 统计卡 + 匹配列表"三区布局——白色单行 Header("匹配结果"20 号加粗) + 三列统计卡(24 匹配的人 / 8 高契合度 / 12 已打招呼,粉色/浅粉/绿色三色数字) + SectionTitle(匹配结果)+ 4 条匹配用户列表(🎧 音乐小铃 95% 共同兴趣:民谣 摄影 / 📷 摄影阿满 89% 共同兴趣:摄影 旅行 / 🏀 球场小飞 84% 共同兴趣:篮球 健身 / 📖 书香小语 78% 共同兴趣:阅读 写作,每条含 52×52 浅粉 emoji 头像 + 姓名 + 共同兴趣标签 + 粉色匹配度 + 浅粉"打招呼"按钮)。本篇基于 39-interest-match/entry/src/main/ets/pages/Func2Tab.ets(共 104 行)逐段拆解,附 4 张实机截图。

一、整体结构:三区"Header + 统计卡 + 匹配列表"布局

匹配页是"品牌头部 + 数据总览 + 匹配明细"的三区布局:

build() {
  Column() {
    this.Header()
    Scroll() {
      Column({ space: 14 }) {
        this.SummaryCard()
        this.SectionTitle('匹配结果')
        this.MatchList()
      }
      .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)
}

3 块结构:

  1. Header — 单行品牌头("匹配结果")
  2. SummaryCard — 三列数据统计卡,用"24 / 8 / 12"三个数字总览全 App 的匹配成果
  3. MatchList — "匹配结果"标题 + 4 条匹配用户卡片,每条展示匹配度、共同兴趣与打招呼入口

与首页结构几乎同构(Header + Banner + 列表),只是把"今日最佳匹配"换成了"三列统计",两个页面共享同一套视觉语言,用户切换 Tab 时毫无陌生感。

项目源码开源:https://gitee.com/codenestFlow/HarmonyOSHub

二、Header:与兴趣页保持一致的品牌头

Header() {
  Row() { Text('匹配结果').fontSize(20).fontWeight(FontWeight.Bold).fontColor(C.text) }
  .width('100%').height(this.safeTop + 56).padding({ top: this.safeTop, left: D.pad, right: D.pad })
  .backgroundColor(C.card).alignItems(VerticalAlign.Bottom)
}

与兴趣页 Header 完全同款:20 号加粗、"安全区 + 56"高度、文字底部对齐、白底全宽。四个 Tab 中除首页外统一采用此样式,保证了二级页面的导航一致性——这也是移动端 App 的常见设计原则:品牌首页可个性,功能页必须统一。

39 匹配页首屏

三、SummaryCard:三列统计卡,layoutWeight 等分 + 三色数字

SummaryCard() {
  Row() {
    Column({ space: 4 }) {
      Text('24').fontSize(22).fontWeight(FontWeight.Bold).fontColor(C.primary)
      Text('匹配的人').fontSize(11).fontColor(C.textDim)
    }.layoutWeight(1)
    Column({ space: 4 }) {
      Text('8').fontSize(22).fontWeight(FontWeight.Bold).fontColor(C.accent)
      Text('高契合度').fontSize(11).fontColor(C.textDim)
    }.layoutWeight(1)
    Column({ space: 4 }) {
      Text('12').fontSize(22).fontWeight(FontWeight.Bold).fontColor(C.ok)
      Text('已打招呼').fontSize(11).fontColor(C.textDim)
    }.layoutWeight(1)
  }
  .width('100%').padding({ top: 16, bottom: 16 }).backgroundColor(C.card).borderRadius(D.rLg).border({ width: 1, color: C.stroke })
}

技术拆解:

  1. layoutWeight 三等分:三个 Column 各加 layoutWeight(1),父级 Row 无额外间距,三列自动平分整行宽度——无需计算像素,任何屏宽都完美三等分。
  2. 数字与标签分离:每个 Column 内 22 号加粗大数字在上、11 号浅灰小标签在下,space: 4 控制间距,形成"数据 + 说明"的标准组合。
  3. 三色语义化:主数字 C.primary(粉)表示总量、C.accent(浅粉)表示高契合、C.ok(绿)表示已行动——用颜色区分三类指标,一眼可读。
  4. 卡片容器:白底圆角 20 + 描边,padding({ top: 16, bottom: 16 }) 只留纵向内边距,三列内部横向由 layoutWeight 自行撑开。

数据自洽核对:这里的"24 匹配的人"与我的页 UserCard 副标题"已匹配 24 人"、统计行"24 匹配历史"完全一致;"12 已打招呼"与我的页"18 好友"的关系可理解为"打了招呼成为好友"的转化漏斗(12 打招呼 → 18 好友,数值上 Demo 未强绑定,但语义自洽)。

四、MatchList:匹配用户列表,共同兴趣 + 打招呼

MatchList() {
  Column({ space: 12 }) {
    ForEach(this.matches, (u: MatchUser) => {
      Row({ space: 12 }) {
        Row() { Text(u.emoji).fontSize(28) }
        .width(52).height(52).backgroundColor(C.primarySoft).borderRadius(26).justifyContent(FlexAlign.Center)
        Column({ space: 5 }) {
          Text(u.name).fontSize(15).fontWeight(FontWeight.Medium).fontColor(C.text)
          Row({ space: 6 }) {
            Text('共同兴趣:').fontSize(11).fontColor(C.textDim)
            ForEach(u.common, (c: string) => {
              Text(c).fontSize(10).fontColor(C.primary)
                .padding({ left: 6, right: 6, top: 1, bottom: 1 }).backgroundColor(C.primarySoft).borderRadius(4)
            }, (c: string) => c)
          }
        }.alignItems(HorizontalAlign.Start).layoutWeight(1)
        Column({ space: 4 }) {
          Text(u.match + '%').fontSize(16).fontWeight(FontWeight.Bold).fontColor(C.primary)
          Text('打招呼').fontSize(11).fontColor('#FFFFFF')
            .padding({ left: 10, right: 10, top: 4, bottom: 4 }).backgroundColor(C.accent).borderRadius(D.rSm)
            .onClick(() => { promptAction.showToast({ message: '向 ' + u.name + ' 打招呼' }); })
        }
      }
      .width('100%').padding(14).backgroundColor(C.card).borderRadius(D.rMd).border({ width: 1, color: C.stroke })
    }, (u: MatchUser) => u.id.toString())
  }.width('100%')
}

匹配数据(20-25 行)由 private matches: MatchUser[] 静态数组提供:

用户匹配度共同兴趣
🎧 音乐小铃95%民谣、摄影
📷 摄影阿满89%摄影、旅行
🏀 球场小飞84%篮球、健身
📖 书香小语78%阅读、写作

卡片细节:

  • 头像:52×52 浅粉圆底 + 28 号 emoji,与首页推荐列表完全一致的视觉语言。
  • 共同兴趣:这是本页的核心差异化信息。Row({ space: 6 }) 内先放 11 号浅灰"共同兴趣:"前缀,再用内层 ForEach 渲染 u.common 数组——每个标签 10 号粉色文字 + primarySoft 浅粉底 + 圆角 4 的迷你胶囊。共同兴趣 = 双方标签的交集,比首页整串标签更聚焦,直击"你们为什么合拍"。
  • 匹配度与动作:右侧 16 号粉色加粗百分比,下方是 11 号白字浅粉底"打招呼"小按钮,点击 Toast"向 XX 打招呼"。注意打招呼按钮挂在 Text 上而非 Button 组件——纯文本实现按钮样式,轻量且样式自由度高。
  • 列表 key:同样以 u.id.toString() 作为 ForEach key。

五、跨页数据自洽:从推荐到匹配的闭环

匹配页与前两页的数据形成严密的逻辑闭环:

  • 音乐小铃三页同现:首页今日最佳匹配 + 推荐第一位、匹配页第一位都是"🎧 音乐小铃 95%",共同兴趣"民谣、摄影"正是首页她三个标签中的两个——匹配度 95% 即高共同兴趣的量化体现。
  • 推荐 → 匹配的转化:首页的"为你推荐"(音乐小铃/游戏阿杰/书虫小雅/运动大伟)与匹配页的"匹配结果"(音乐小铃/摄影阿满/球场小飞/书香小语)人物既有重合又有差异,暗示产品有两种列表:泛推荐与高匹配,逻辑上自洽。
  • 统计贯穿:"24 匹配的人"贯穿匹配页统计卡、我的页 UserCard、我的页统计行三处,数值完全一致,演示时给用户"数据是真的"的信任感。

六、扩展思考:从"共同兴趣"到完整推荐策略

  1. Jaccard 相似度:匹配度最朴素的计算是"共同标签数 / 并集标签数",如音乐小铃(民谣/摄影/旅行)与我的 5 标签(民谣/摄影/旅行/篮球/阅读)交集 3、并集 5,相似度 0.6——真实产品会在加权后归一化到 0-100 分。
  2. 标签权重:不同兴趣应有不同权重,如"民谣"比"咖啡"更能代表性格契合,算法层面可引入 TF-IDF 或嵌入向量。
  3. 打招呼后的状态流转:点"打招呼"后该条应变为"已打招呼"并计入统计卡"12 已打招呼",Demo 只做了 Toast,产品化需引入状态字段与列表刷新。
  4. 双向匹配:真实社交产品需保证"TA 也对你感兴趣"才展示,避免单相思式推荐的尴尬,即"互选"机制。
  5. 增量更新:用户每次在兴趣页修改标签,匹配列表都应实时重排——把 matches 从静态数组改为 @State + 计算属性即可实现。

七、彩蛋细节:藏在匹配页的小心思

  1. 共同兴趣只取两个:common 数组刻意只存 2 个标签(民谣、摄影),而不是全量,UI 上更聚焦"关键契合点",避免信息过载。
  2. emoji 与匹配度呼应:📷 摄影阿满匹配度 89%、共同兴趣"摄影、旅行"——📷 即摄影,人物设定与兴趣完全对应;📖 书香小语同理,📖 = 阅读,天衣无缝。
  3. 打招呼按钮的圆角:borderRadius(D.rSm)(10)比首页交换按钮更圆润,暗示这是"轻动作"入口,降低用户心理压力。
  4. 统计数字的命名:"匹配的人 / 高契合度 / 已打招呼"恰好构成"认识 → 契合 → 行动"的漏斗三步,产品叙事非常完整。
  5. 颜色即语义:粉色主数字对应核心功能色,绿色"已打招呼"对应正向行动色,浅粉"高契合度"是中间过渡态——三种颜色三种层级,全部复用主题变量。

八、实机截图与交互演示

本节结合 4 张实机截图,逐张还原匹配页的视觉效果与交互过程。

1. 匹配页首屏:三列统计 + 匹配列表

第一张截图是匹配页默认首屏:顶部"匹配结果"单行 Header,下方是三列统计卡——粉色"24 匹配的人"、浅粉"8 高契合度"、绿色"12 已打招呼",三个数字 22 号加粗、标签 11 号浅灰,三等分布局工整。统计卡之下是"匹配结果"分区标题与第一条匹配用户卡(🎧 音乐小铃 95% + 共同兴趣"民谣 摄影" + 浅粉"打招呼"按钮)。

2. 点击"打招呼":向音乐小铃打招呼

第二张截图演示了点击第一位用户"音乐小铃"右侧"打招呼"按钮后的反馈:弹出"向 音乐小铃 打招呼"Toast。按钮虽由 Text 组件实现,但点击区域、视觉反馈与原生按钮无异,验证了"文本实现按钮"这一轻量方案的可用性。

3. 点击"打招呼":向球场小飞打招呼

第三张截图是点击第三位用户"🏀 球场小飞"的"打招呼"按钮后的反馈——Toast 显示"向 球场小飞 打招呼"。由于每位用户的按钮 onClick 都闭包捕获各自的 u.name,点击谁就向谁打招呼,参数传递正确无误,多人列表的点击差异化得到验证。

4. 匹配列表完整视图:四位用户全览

第四张截图是匹配列表的完整视图:四位用户全部可见——🎧 音乐小铃 95%(民谣/摄影)、📷 摄影阿满 89%(摄影/旅行)、🏀 球场小飞 84%(篮球/健身)、📖 书香小语 78%(阅读/写作)。每位用户"共同兴趣"标签与匹配度一一对应,匹配度随共同标签的"质量"递减,列表排序符合"高契合优先"的产品逻辑。

九、扩展细节:列表性能与懒加载优化

匹配页当前用 ForEach 渲染 4 条静态数据,若扩展到真实规模(数百名匹配对象),需要考虑:

  1. LazyForEach 按需渲染:替换为 LazyForEach + IDataSource,屏幕外卡片不创建节点,滚动时按需构建与回收,保证千条列表依然流畅。
  2. item 组件化:将每条匹配卡抽成 @Component 子组件(MatchItem),配合 @Reusable 装饰器实现组件复用池,进一步降低滚动卡顿。
  3. 稳定 key:现有 u.id.toString() 的 key 策略完全正确——id 唯一且不随排序变化,diff 时节点可精准复用,这是列表性能的根基,务必保持。
  4. 图片懒加载:若头像从 emoji 换成网络图片,应使用 Image 的懒加载 + 占位图,避免首屏一次性加载大量图片阻塞渲染。

39 匹配页音乐小铃打招呼

十、扩展细节:从"打招呼"到会话消息的产品演进

"打招呼"按钮是社交产品的起点,其背后可展开的产品链路非常丰富:

  1. 状态机:每次打招呼后,该用户的按钮状态应流转为"已打招呼"(灰置/禁用),并累计到统计卡"12 已打招呼"——Demo 只做了 Toast,产品化需引入 @State greetedIds 与列表刷新。
  2. 会话入口:打招呼成功即创建一条私聊会话,匹配页的打招呼按钮与聊天列表(如 App 38 的交流页)应共享同一数据源,打通"匹配 → 私聊"全流程。
  3. 双向确认:真实产品中,打招呼需对方回赞才算"匹配成功"(即互选),匹配列表应区分"单向/双向"两种状态,用不同底色或角标表达。
  4. 消息推送:与鸿蒙的 @ohos.push 推送服务对接后,对方打招呼可实时提醒,App 的活跃度与召回率都会显著提升。

十一、补充说明与终评

匹配页是全 App 的"成果页",用一屏三列统计 + 四人列表完成了"我们帮你找到了谁、为什么合拍、你可以做什么"的完整叙事。技术上,layoutWeight 三等分与嵌套 ForEach 渲染标签是最高频的两种 ArkUI 模式;设计上,共同兴趣标签与打招呼按钮的组合精准命中了社交产品的核心互动逻辑。整体代码 104 行,干净利落,与首页、兴趣页共同构成"采集画像 → 推荐人 → 促成连接"的完整体验闭环。

十二、常见问题与开发避坑

匹配页这类"统计卡 + 列表"页面,开发中也有几个容易踩的坑:

  1. layoutWeight 与固定宽度冲突:layoutWeight(1) 的子组件不要再显式设置 width,否则二者会冲突导致布局异常——本页三个统计 Column 只设 layoutWeight、不设宽度,是正确姿势。
  2. 嵌套 ForEach 的 key 冲突:外层 ForEach 以用户 id 为 key,内层 ForEach 以共同兴趣字符串为 key,两层的 key 命名空间独立、互不干扰;但如果把内层渲染抽成子组件,记得在子组件内部重新声明 key,避免层级混乱。
  3. 文本按钮的可点击性:本页"打招呼"是用 Text 实现的按钮,它天然具备点击能力,但没有 Button 的按压态反馈(水波纹/变色),用户感知不到"按下"动作。工程化时可加 onTouch 换色、或直接换回 Button({ type: ButtonType.Capsule }) 获得原生反馈。
  4. Toast 文案与状态一致性:点"打招呼"只弹 Toast,但统计卡"12 已打招呼"不会 +1——这是 Demo 的数据静态化导致的"文案与状态脱节"。产品化时务必让操作真实驱动数据变化,否则用户会发现"点了没变化"的违和感。
  5. 列表空态缺失:若 matches 为空数组,页面会只剩统计卡和标题,底部留白。工程化时应补充空态视图(如"暂无匹配,先去完善兴趣标签吧"+ 跳转按钮),引导用户回到兴趣页补全画像。

39 匹配页摄影阿满打招呼

十三、系列横向对比:列表页的信息密度设计

匹配页在信息密度控制上有独到之处,横向对比本系列的列表页可以看清设计取舍:

列表类型代表页信息量/条交互动作
精简列表App 39 匹配页头像+名+共同兴趣+匹配度打招呼
丰富列表App 38 首页技能列表头像+名+精通级+想学+按钮交换
两行列表App 32 首页雨伞列表名称+状态+距离借伞
图文列表App 37 资料列表封面+标题+简介+评分查看

匹配页刻意把"共同兴趣"压缩成 2 个迷你标签、把操作收敛为 1 个按钮,信息量在四者中属于"中等偏少",但每一比特信息都服务于"我要不要跟他打招呼"这一个决策——这是列表设计的黄金法则:去掉与核心动作无关的信息。相比 App 38 技能列表的"精通/熟练/想学"多标签轰炸,匹配页更克制,也更适合高频刷的场景。若想进一步提升转化,可在列表项底部加"查看详情"展开卡片,把完整兴趣、个性签名等次要信息折叠进二级视图——既保持列表清爽,又不损失信息深度。

39 匹配页球场小飞打招呼

十四、FAQ:关于匹配页的常见疑问

Q1:匹配页和首页的"为你推荐"有什么区别? 首页是"泛推荐"(全部用户按匹配度降序),匹配页是"高契合精选"(只展示共同兴趣明确的用户)。二者数据同源、展示口径不同:首页偏探索、匹配页偏转化,共同构成"先逛后聊"的引导路径。

Q2:共同兴趣标签为什么只有两个? common 数组刻意只存 2 个标签。UI 上两个迷你标签刚好一行、不换行、不遮挡姓名,信息密度最优;若塞 5-6 个标签,行会变高、列表变长、视觉变乱。真实产品同样遵循"关键信息 ≤ 3 条"的卡片设计准则。

Q3:"打招呼"用什么组件实现最合适? 本 Demo 用 Text 模拟按钮,胜在零额外组件;但缺按压态反馈。工程化推荐 Button({ type: ButtonType.Capsule }) + stateEffect: true,获得原生水波纹,视觉与手感都更专业。

Q4:统计卡的三个数字为什么不加单位? "24 匹配的人 / 8 高契合度 / 12 已打招呼"——"人、度、打招呼"已经由下方小标签说明,数字本身越干净越好。这是"数字与单位分离"的排版惯例,避免重复信息造成视觉噪音。

Q5:匹配度会变化吗? 会。它是"我的标签 × 对方标签"的计算结果:在兴趣页增删标签 → 重新计算所有匹配度 → 列表实时重排。Demo 静态演示了结果态,动态化只需把 matches 改为计算属性并在兴趣页保存时触发重算。

Q6:这条列表需要空态与加载态吗? 需要。工程化应补充三种状态:加载中(骨架屏)、空数据("暂无匹配,去完善兴趣标签吧" + 跳转按钮)、异常("网络开小差了" + 重试)。目前 4 条静态数据演示不了这些状态,但架构上应预留分支。

十五、一句话总结

匹配页是"成果展示页"的范本:三列统计用 layoutWeight 等分、列表用嵌套 ForEach 渲染共同兴趣、动作用轻量"打招呼"按钮收敛转化。104 行代码把"我们帮你找到谁、为什么合拍、你可以做什么"讲得明明白白,是全 App 转化链路最清晰的一环。

最后补充两点观察:其一,匹配页与首页在 Scroll 容器结构上完全同构(都是"Header + Scroll + Column(space:14)"),这种"页面骨架复用、内容区差异"的模式让四个 Tab 的代码风格高度统一,后续维护者只需关心每个页面的内容区即可;其二,"打招呼"的文案没有做男女/性别区分(统一"向 XX 打招呼"),在校园泛社交场景下是稳妥的中性表达,若产品细分到恋爱匹配场景,可扩展为"喜欢/想认识"等更情感化的措辞。整体来看,匹配页在设计上克制、在实现上简洁,既有数据说服力又有行动引导力,是"列表即转化"这一设计理念的完整体现,值得在同类社交产品中反复借鉴。

还有一个常被忽略的细节:匹配列表的"共同兴趣"迷你标签使用 borderRadius(4) 的近乎矩形的小圆角,与首页 #标签 的 borderRadius(10) 大圆角刻意区分——小圆角传达"数据性"(它是字段值),大圆角传达"交互性"(它是可点标签),两种视觉语言各司其职。若反其道而行(共同兴趣用大圆角),用户会误以为可点击,造成交互歧义。这种"圆角即语义"的细节把控,正是成熟的 UI 工程师与新手之间的分水岭,也是本页在同类列表页中显得专业的原因所在。同理,统计卡三个数字的字体色也做了语义分层:总量用主色、过渡用浅粉、行动用绿色,颜色既是装饰也是信息编码,多一层设计就多一分专业度。

配图

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐