鸿蒙原生应用实战:HarmonyOS 6.0 进度条双按钮驱动志愿活动记录卡状态设计
2026/9/4 17:16:26 网站建设 项目流程

鸿蒙原生应用实战:HarmonyOS 6.0 进度条双按钮驱动志愿活动记录卡状态设计

App 17「志愿服务」记录 Tab(Func2Tab),是志愿活动的历史记录页。整页用 Header + 3 栏彩色统计(总数 28/进行中 3/已完成 25)+ 3 段下划线 Tab 筛选(全部/进行中/已完成)+ 5 条记录卡片(含进度条 + 双按钮:查看详情+更多)。本篇基于17-volunteer/entry/src/main/ets/pages/Func2Tab.ets(约 153 行)逐段拆解,附 4 张实机截图。

一、整体结构:Header + StatsCard + TabBar + 滚动记录

Func2Tab 的结构与 App 13 歌单记录页几乎完全同源——"固定 Header + StatsCard + TabBar + Scroll 装记录":

build() { Column() { this.Header() this.StatsCard() this.TabBar() Scroll() { Column({ space: 12 }) { ForEach(this.records, (item: RecordItem) => { this.RecordCard(item) }, (item: RecordItem) => item.id.toString()) Blank().height(this.safeBottom + 20) } .width('100%').padding({ left: D.pad, right: D.pad, top: 6 }) } .layoutWeight(1).scrollBar(BarState.Off).align(Alignment.Top) } .width('100%').height('100%').backgroundColor(C.bg) }

3 段固定 + 1 段滚动

  • Header / StatsCard / TabBar三段在 Scroll 外(不滚动,切换 Tab 时保持原位)
  • Scroll 容器只装记录卡 + 安全区缓冲

与 App 13 歌单记录页完全同源——只有主题色不同(紫→红)。

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

二、Header:单行标题 + 5 条记录副标题 + 搜索图标

Header 单行结构(与 App 13 同构):

@Builder Header() { Row({ space: 12 }) { Column({ space: 2 }) { Text('记录').fontSize(20).fontWeight(FontWeight.Bold).fontColor(C.text) Text(`${this.records.length} 条记录`).fontSize(10).fontColor(C.textDim) }.alignItems(HorizontalAlign.Start) Blank() Row() { Text('🔍').fontSize(18) } .width(36).height(36).backgroundColor(C.cardSoft).borderRadius(D.rSm).justifyContent(FlexAlign.Center) .onClick(() => { promptAction.showToast({ message: '搜索' }); }) } .width('100%').height(this.safeTop + 60) .padding({ top: this.safeTop, left: D.pad, right: D.pad }) .backgroundColor(C.card).alignItems(VerticalAlign.Bottom) .border({ width: { bottom: 1 }, color: C.stroke }) }

Text(\${this.records.length} 条记录`)` 动态副标题——this.records.lengthRecordItem[]数组长度(5)。@State一变(或数组长度变化)副标题自动更新为 "5 条记录"。

onClick(() => promptAction.showToast({ message: '搜索' }))搜索图标点击弹 Toast——"轻反馈"策略。

三、StatsCard:3 栏彩色统计(本页特色)

StatsCard 是 3 个统计数字(总数 28 / 进行中 3 / 已完成 25),每个数字带自己的颜色

@Builder StatsCard() { Row() { ForEach(this.stats, (s: StatItem, idx: number) => { Column({ space: 3 }) { Text(s.value).fontSize(20).fontWeight(FontWeight.Bold).fontColor(s.color) Text(s.label).fontSize(10).fontColor(C.textDim) }.layoutWeight(1) if (idx < this.stats.length - 1) { Divider().vertical(true).height(28).color(C.stroke) } }, (s: StatItem) => s.label) } .width('100%').height(64).backgroundColor(C.card).borderRadius(D.rMd) .border({ width: 1, color: C.stroke }).justifyContent(FlexAlign.SpaceAround) .margin({ top: 10, left: D.pad, right: D.pad }) }

3 色映射(数据自带 color 字段):

private stats: StatItem[] = [ { value: '28', label: '总数', color: C.primary }, { value: '3', label: '进行中', color: C.warn }, { value: '25', label: '已完成', color: C.ok } ];
  • 总数 28C.primary红色(中性聚合)
  • 进行中 3C.warn橙色(警示待办)
  • 已完成 25C.ok绿色(成功完成)

3 色 3 语义——color字段把"业务状态"映射到"视觉颜色"。"颜色 = 状态"是数据可视化的基础

"3 + 25 = 28"——数据自洽(进行中 + 已完成 = 总数)。这是 demo 的"隐性数据校验"——**"统计数据应该能加回去"**是检测数据一致性的简单方法。

Divider().vertical(true).height(28)竖向分隔线——vertical(true)让默认的横线变成竖线,限定高度 28vp 与数字大致对齐。if (idx < this.stats.length - 1)防止最后一条也加分隔线。

margin({ top: 10, left: D.pad, right: D.pad })让 StatsCard 距离 Header 10vp(视觉隔开),左右各 16vp(D.pad)。注意 StatsCard 用margin而非外层padding——因为它在父 Column 内独立占位。

四、TabBar:3 段下划线指示器

TabBar 是 3 个分类(全部/进行中/已完成)横向排列,选中态用 24×2 的下划线标记

@Builder TabBar() { Row({ space: 0 }) { ForEach(this.tabs, (t: string, idx: number) => { Column({ space: 6 }) { Text(t).fontSize(13) .fontColor(this.activeTab === idx ? C.primary : C.textDim) .fontWeight(this.activeTab === idx ? FontWeight.Bold : FontWeight.Normal) Column().width(24).height(2).borderRadius(1) .backgroundColor(this.activeTab === idx ? C.primary : 'transparent') } .layoutWeight(1).padding({ top: 8, bottom: 8 }) .onClick(() => { this.activeTab = idx; }) }, (t: string) => t) } .width('100%').backgroundColor(C.card).borderRadius(D.rSm) .border({ width: 1, color: C.stroke }) .margin({ top: 10, left: D.pad, right: D.pad }) }

下划线指示器的实现

Column().width(24).height(2).borderRadius(1) .backgroundColor(this.activeTab === idx ? C.primary : 'transparent')
  • 文字下面用Column()画一条 24×2vp 的横线
  • 选中时C.primary红色;未选时'transparent'透明(注意必须写字符串'transparent',不能省略

Row({ space: 0 })让 3 个 Tab 紧贴,配合每个Column自己的padding({ top: 8, bottom: 8 })——3 个 Tab 看起来是独立的可点击区,但视觉上是一整条 TabBar。

"下划线 vs 胶囊"对比:App 17 是下划线(适合 2-3 个 Tab),App 14/15/16 是胶囊(适合 4 个以上)。"Tab 数量决定 UI 形态":2-3 个用下划线(更轻),4+ 用胶囊(更清晰)。

注意 demo 没有联动ForEach(this.records, ...)过滤——点击 Tab 只切换下划线高亮,不是真筛选(与 App 13 歌单记录页相同的"假筛选"问题)。真实项目应加filteredRecords()

五、RecordCard:状态标签 + 进度条 + 双按钮

RecordCard 是 5 条记录卡片,每条都含状态标签、进度条(仅进行中)、双按钮

@Builder RecordCard(item: RecordItem) { Column({ space: 10 }) { Row({ space: 12 }) { Row() { Text(item.emoji).fontSize(24) } .width(44).height(44).backgroundColor(C.cardSoft).borderRadius(D.rSm).justifyContent(FlexAlign.Center) Column({ space: 4 }) { Row() { Text(item.title).fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text).layoutWeight(1) Text(item.status).fontSize(10).fontColor('#FFFFFF') .backgroundColor(item.statusColor) .padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(D.rSm) }.width('100%') Text(item.desc).fontSize(12).fontColor(C.textSub) Text(`⏱ ${item.time}`).fontSize(10).fontColor(C.textDim) }.alignItems(HorizontalAlign.Start).layoutWeight(1) Text('›').fontSize(20).fontColor(C.textDim) }.width('100%') if (item.status === '进行中') { Row() { Text('进度').fontSize(10).fontColor(C.textDim) Stack({ alignContent: Alignment.Start }) { Column().width('100%').height(4).backgroundColor(C.cardSoft).borderRadius(2) Column().width('75%').height(4).borderRadius(2).backgroundColor(C.warn) }.layoutWeight(1).margin({ left: 8, right: 8 }) Text('75%').fontSize(10).fontColor(C.warn).fontWeight(FontWeight.Bold) }.width('100%') } Row({ space: 10 }) { Button('查看详情') .fontSize(12).fontColor('#FFFFFF').backgroundColor(C.primary) .borderRadius(D.rSm).height(34).layoutWeight(1) .onClick(() => { promptAction.showToast({ message: item.title }); }) Button('更多') .fontSize(12).fontColor(C.textSub).backgroundColor(C.cardSoft) .borderRadius(D.rSm).height(34).layoutWeight(1) .onClick(() => { promptAction.showToast({ message: '更多操作' }); }) }.width('100%') } .width('100%').padding(12).backgroundColor(C.card).borderRadius(D.rMd) .border({ width: 1, color: C.stroke }) }

5.1 状态色 + 状态文字("色 + 文"双通道)

Row() { Text(item.title).fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text).layoutWeight(1) Text(item.status).fontSize(10).fontColor('#FFFFFF') .backgroundColor(item.statusColor) .padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(D.rSm) }

item.statusColor字段把颜色直接绑到数据上——5 条记录状态颜色:进行中 2 条(C.warn橙)/ 已完成 3 条(C.ok绿)。白字 + 彩色背景让"进行中"/"已完成"标签醒目。

5 条记录的状态分布

  • 📦 项目一(进行中 75%)
  • 🎯 项目二(已完成 5星)
  • ⭐ 项目三(已完成 4星)
  • 📊 项目四(进行中 40%)
  • 🏆 项目五(已完成 5星)

"2 条进行中 + 3 条已完成"是当前列表的 5 条样本;StatsCard 的 3/25 是全部 28 条记录的汇总——两者口径不同(样本 vs 全量),不矛盾。

5.2 条件渲染:进行中显示进度条,已完成不显示

if (item.status === '进行中') { Row() { Text('进度').fontSize(10).fontColor(C.textDim) Stack({ alignContent: Alignment.Start }) { Column().width('100%').height(4).backgroundColor(C.cardSoft).borderRadius(2) Column().width('75%').height(4).borderRadius(2).backgroundColor(C.warn) }.layoutWeight(1).margin({ left: 8, right: 8 }) Text('75%').fontSize(10).fontColor(C.warn).fontWeight(FontWeight.Bold) }.width('100%') }

if (item.status === '进行中')条件渲染——只有进行中显示"进度 75%"的进度条(橙色),已完成不显示。两种状态的卡片高度不同(进行中约 200vp、已完成约 130vp),这是"状态驱动布局"。

Stack进度条与首页同构(背景 + 前景叠加 + 百分比文字),唯一差异:颜色跟随状态(首页进度条是主色,本页进行中是C.warn橙色)。"颜色 = 状态"原则一致

潜在 bugwidth('75%')是硬编码——项目一 75%、项目四 40% 应该是不同百分比。真实项目应把宽度从数据item.progress读取width(\${item.progress}%`)),并在 RecordItem 加progress?: number` 字段。

5.3 双按钮:查看详情 + 更多

Row({ space: 10 }) { Button('查看详情') .fontSize(12).fontColor('#FFFFFF').backgroundColor(C.primary) .borderRadius(D.rSm).height(34).layoutWeight(1) .onClick(() => { promptAction.showToast({ message: item.title }); }) Button('更多') .fontSize(12).fontColor(C.textSub).backgroundColor(C.cardSoft) .borderRadius(D.rSm).height(34).layoutWeight(1) .onClick(() => { promptAction.showToast({ message: '更多操作' }); }) }

两个并排等宽按钮

  • 查看详情— 主操作,红色背景(C.primary),白字
  • 更多— 次操作,浅灰背景(C.cardSoft),灰字

两者都是layoutWeight(1)等宽——等宽双按钮的视觉平衡比"内容宽度"更稳定("更多"只有 2 个字,按内容宽度会非常窄)。

主次按钮的视觉权重差异通过"背景色 + 文字色"两个维度同时区分——比单靠"按钮大小"更细腻。**"主次 = 视觉权重"**是按钮设计的基本原则。

promptAction.showToast({ message: item.title })反馈——与首页、创建页一致。更多点击弹"更多操作"提示,"伪操作"占位(真实项目应弹操作菜单:删除/分享/复制/置顶等)。

六、数据模型与筛选(潜在改进点)

RecordItem接口有 7 个字段:

interface RecordItem { id: number; emoji: string; title: string; status: string; time: string; desc: string; statusColor: string; }

status: string+statusColor: string携带了视觉属性——这是"数据携带样式"的模式(demo 方便,真实项目应从 status 推 color)。改进方向

// 推荐:枚举 + 映射表 enum RecordStatus { InProgress = 1, Completed = 2 } const STATUS_META = { [RecordStatus.InProgress]: { text: '进行中', color: C.warn }, [RecordStatus.Completed]: { text: '已完成', color: C.ok } };

if (item.status === '进行中')用字符串判断也脆弱——真实项目应if (item.status === RecordStatus.InProgress)

Tab 筛选的真假——App 17 与 App 13 相同,Tab 切换没联动 ForEach 过滤(点击全部/进行中/已完成都显示全部 5 条)。真实项目应实现filteredRecords()

private filtered(): RecordItem[] { if (this.activeTab === 0) return this.records; const target = this.activeTab === 1 ? '进行中' : '已完成'; return this.records.filter(r => r.status === target); }

Tab id = 状态值(0 全部/1 进行中/2 已完成)——通过target变量将数字 id 映射为状态字符串再 filter(与 App 14 预约记录页相同的"id 映射"设计)。

七、@State 的克制

Func2Tab 只有1 个 @StateactiveTab: number = 0recordsprivate不可变数据。

4 个 Tab 的 @State 分布

页面@State 数量类型
首页2(activeCat/selectedBar)可交互
活动5(inputTitle/inputDesc/selectedType/selectedPriority/remindOn)表单
记录1(activeTab)筛选
我的0纯展示("小明同学 Lv.4"无 @State)

"@State 数量 = 交互点数"规律延续:表单 5、列表 1-2、纯展示 0。

注意:App 17 我的页(ProfileTab)0 个 @State——与 App 13 同款"个人中心 0 状态"(App 14/15/16 是 1 个checked)。ProfileTab 有"每日签到"按钮但无状态——点击只弹 Toast 不改状态(demo 不实现真正的签到逻辑)。"个人中心 0 状态"是一种简化(demo 不实现签到)。

八、@Builder 参数化的设计

RecordCard(item: RecordItem)接收参数(RecordItem接口定义在本文件第 4-12 行):

@Builder RecordCard(item: RecordItem) {

@Builder接收参数vs@Component接收 props

  • @Builder接收参数:轻量、无生命周期
  • @Component接收@Prop:更重、可挂载生命周期

App 17 选@Builder接收参数——因为 RecordCard 只是渲染逻辑(无内部状态、无交互),用最轻量的 @Builder + 参数即可。**"轻量渲染用 @Builder,需要状态用 @Component"**是设计原则。

ForEach 里的传参

ForEach(this.records, (item: RecordItem) => { this.RecordCard(item) }, (item: RecordItem) => item.id.toString())

回调拿到 item →this.RecordCard(item)传入——整个数据流:array → ForEach → RecordCard(item)一气呵成。item.id.toString()是稳定 key(id 唯一不变)。

九、与 App 13 歌单记录页的对比总结

维度App 13 歌单记录App 17 志愿记录
主题色紫 #8B5CF6红 #EF4444
Tab 状态全部/进行中/已完成全部/进行中/已完成
统计14/3/2(彩)28/3/25(彩)
记录5 条(项目一~五)5 条(项目一~五,完全同构
状态操作详情+更多详情+更多
进度条进行中显进行中显
状态色warn/okwarn/ok(同款)

App 17 与 App 13 几乎 100% 同构——除了 Theme.ets 里的几个颜色值不同(紫→红)+ 统计数字差异(14/3/2 vs 28/3/25)。这种"主题复刻"是系列化 demo 的极致表现——同套代码生成不同主题的应用。

十、模板复用的工程价值

App 13(紫)→ App 17(红)的"换肤"是 5 分钟的工程

  1. 复制13-music-playlist/17-volunteer/
  2. 改 Theme.ets 的 3 个颜色(primary/accent/primarySoft)
  3. 改 README 的 bundleName、文案、Tab 命名
  4. 改 ProfileTab 的小明同学小志愿者、Lv 数字、Slogan
  5. 重新构建部署

5 步换肤 = 5 分钟生成一个新主题应用——这是 ArkUI 模板设计的最高境界。

真实项目套用这个方法

  • 企业品牌定制:客户 A 要绿色、客户 B 要蓝色——同一个产品代码,Theme.ets 改 3 个颜色 = 两个品牌版
  • 节日活动:春节红、圣诞绿、中秋金——换主题色配合节日营销
  • 暗色/亮色双模式:light: {...} + dark: {...} 配合系统主题自动切换

"主题与代码分离"是 ArkUI 工程的最佳实践——App 13/17 就是这个实践的标准示范。

十一、记录数据的"生命周期"管理

App 17 记录页当前是"内存数据"——5 条硬编码的 records。真实项目应考虑记录的完整生命周期

1. 创建:用户在活动 Tab 创建 → 后端 API 生成记录 → 状态"进行中"(status=1)

2. 进行中:志愿者开始活动 → 记录显示"已参与 X 人"(可能从后端实时拉取)

3. 完成:活动结束 → 状态转"已完成"(status=2)→ 自动发放志愿时长/证书

4. 评价:用户打分/评论 → 记录 desc 字段更新

5. 归档:超过 90 天 → 移入"历史"区,不再显示在主列表

"状态机驱动 UI"——record 的 status 字段(1/2/3...)决定:

  • 显示什么文字("进行中"/"已完成")
  • 显示什么颜色(warn/ok/danger)
  • 显示什么操作按钮(取消/再次/查看)
  • 决定是否进首页推荐

**"数据生命周期 + 状态机 + UI 联动"**是真实记录页的核心模型,App 17 的简化版是 2 状态(进行中/已完成),真实项目通常 5+ 状态。

十二、"状态色 + 状态文字"双通道的极致应用

App 17 状态标签的"色 + 文"双通道设计值得深入分析:

Text(item.status).fontSize(10).fontColor('#FFFFFF') .backgroundColor(item.statusColor) .padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(D.rSm)

"色 + 文"双通道的 5 个好处

  1. 色弱用户友好:约 8% 人口色弱,靠文字识别
  2. 暗色模式兼容:某些深色背景下颜色对比度不够,文字兜底
  3. 国际化:不同国家"颜色语义"不同(红色在西方=危险,在中国=吉祥)
  4. 可访问性:屏幕阅读器读"状态:进行中",不靠颜色
  5. 打印场景:黑白打印只剩文字,依然能识别

"色 + 文"双通道是 ArkUI 状态显示的最佳实践真实项目应该无脑套用——所有"用颜色区分状态"的地方都该配文字兜底。

反例警示:只用颜色区分状态(没有文字),色弱用户完全无法使用——这是移动端的"可访问性死穴"。

十三、总结

App 17 活动记录页解析完毕。3 栏彩色统计 + 下划线 Tab + 状态色标签 + 进度条 + 双按钮是核心组件。与 App 13 的"模板复刻"展示了 ArkUI 主题与代码分离的工程价值——同一套 UI 框架,换 Theme.ets 3 个颜色 = 全新应用。"色 + 文"双通道 + "状态机 + UI 联动"是真实记录页的核心——读者在抄模板时记得补齐。

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

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

立即咨询