FEATURED · 精选文章

状态色与进度条驱动:ArkUI 声明式下,音乐歌单记录卡多状态设计

发布时间 / 2026/9/4 1:04:31
来源 / 创域科博编辑部
栏目 / 资讯中心
状态色与进度条驱动:ArkUI 声明式下,音乐歌单记录卡多状态设计 状态色与进度条驱动ArkUI 声明式下音乐歌单记录卡多状态设计App 13「音乐歌单」歌单 TabFunc2Tab是歌曲管理记录的看板页。整页用 Header 3 栏彩色统计 3 段 Tab 筛选 5 条带进度条/评分的记录卡片组成是 ArkUI 里数据列表 多状态场景的典型范本。本篇基于13-music-playlist/entry/src/main/ets/pages/Func2Tab.ets约 153 行逐段拆解附 4 张实机截图。一、整体结构固定 Header 统计 Tab 栏 滚动列表Func2Tab的骨架与Func1Tab类似但更紧凑——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) }与 Func1Tab 的差异没有 space: 14 的总间距——Header/StatsCard/TabBar 之间是紧贴的视觉上像一块大信息区Scroll 容器只包裹记录卡——5 条记录 安全区 Buffer 是唯一会滚动的部分顶部 top: 6 padding——给 Scroll 内容一点呼吸感避免卡片贴到 TabBar 下边线Header和TabBar不在 Scroll 内意味着用户切换 Tab 时全部/进行中/已完成Header/StatsCard/TabBar 都保持原位只有 Scroll 内容重新过滤。这种固定顶栏 滚动内容是列表页的黄金模式。项目源码开源https://gitee.com/codenestFlow/HarmonyOSHub二、Header固定标题 副标题 搜索图标Header 与 Func1Tab 类似但多了一行副标题5 条记录和右侧搜索图标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.length是RecordItem[]数组长度写死 5。如果未来增删记录副标题自动更新为 6 条记录 或 4 条记录。这种数据驱动的副标题是高质量 UI 的标志。onClick(() promptAction.showToast({ message: 搜索 }))搜索图标点击弹 Toast搜索——同首页 QuickActionRow 的轻反馈策略。真实项目应跳到搜索页或弹搜索框。三、StatsCard3 栏彩色统计数字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 }) }stats数据自带 color 字段private stats: StatItem[] [ { value: 28, label: 总数, color: C.primary }, { value: 3, label: 进行中, color: C.warn }, { value: 25, label: 已完成, color: C.ok } ];3 色映射总数 →C.primary紫色中性、聚合进行中 →C.warn橙色警示、待办已完成 →C.ok绿色成功、完成Divider().vertical(true).height(28)竖向分隔线。vertical(true)是关键参数让默认的横线变成竖线配合height(28)限定高度与数字大致对齐。if (idx this.stats.length - 1)防止最后一条也加分隔线——同首页 HeroCard 的处理。margin({ top: 10, left: D.pad, right: D.pad })让 StatsCard 距离 Header 10vp视觉上与 Header 隔开、距离屏幕左右各 16vpD.pad。注意整个页面其他卡片都是用padding({ left: D.pad, right: D.pad })统一左右内边距StatsCard 用 margin 是因为它要在父 Column 的范围内独立占位。justifyContent(FlexAlign.SpaceAround)让 3 个统计均匀分布。四、TabBar3 段下划线指示器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)画一条 24×2vp 的横线选中时backgroundColor(C.primary)紫色未选中时backgroundColor(transparent)透明space: 6让文字与下划线之间有 6vp 间距整条 TabBar 用Row({ space: 0 })让 3 个分类紧贴layoutWeight(1)等分space: 0的妙用space: 0让 Row 内子元素之间无间距配合每个Column自己的padding({ top: 8, bottom: 8 })3 个 Tab 看起来是独立的可点击区但视觉上是一整条 TabBar。如果用space: 1或更高3 个 Tab 之间会出现细缝破坏TabBar 是一体的感觉。backgroundColor(transparent)是个易错点——ArkUI 里透明必须写字符串transparent不能省略省略会变黑色背景。与首页 CategoryBar 的差异首页 CategoryBar 是胶囊形背景色边框圆角这里 TabBar 是下划线底部 24×2 紫线。前者适合分类筛选多选一强筛选感后者适合页面 Tab多选一轻切换感。两种 UI 形态在 demo 里都有展示可以对比学习。五、RecordCard状态色 进度条 评分的复合记录卡RecordCard是整页最复杂也最有看头的部分——每条记录根据进行中/已完成状态显示不同内容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 数据建模与状态色RecordItem接口有 6 个字段interface RecordItem { id: number; emoji: string; title: string; status: string; time: string; desc: string; statusColor: string; }statusColor字段把颜色直接绑到数据上让 5 条记录有不同状态对应不同颜色——不用在 UI 层用if/else判断状态再赋颜色。这种数据携带视觉属性在 demo 里很方便真实项目应封装成状态 → 颜色映射函数。5 条数据项目一(进行中 75%)、项目二(已完成 5星)、项目三(已完成 4星)、项目四(进行中 40%)、项目五(已完成 5星)。2 条进行中 3 条已完成正好对应 StatsCard 的 3/2810% 进行中、25/2890% 已完成实际是 2/5 进行中 40% 是 demo 数据不一致不影响演示。5.2 状态标签的白字 彩色背景Text(item.status).fontSize(10).fontColor(#FFFFFF) .backgroundColor(item.statusColor) .padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(D.rSm)Text直接套backgroundColorpaddingborderRadius就是一个胶囊形状态标签。#FFFFFF白字配彩色背景进⾏中橙C.warn、已完成绿C.ok——文字与背景对比度足够色弱用户也能识别。5.3 条件渲染进行中显示进度条已完成不显示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 进行中)是 ArkUI 的条件渲染。进行中显示进度 75%的进度条已完成不显示任何进度条demo 里已完成应该改成显示评分 5星才更合理但当前代码没做这层联动。Stack进度条背景Column().width(100%).height(4).backgroundColor(C.cardSoft)浅灰底前景Column().width(75%).height(4).backgroundColor(C.warn)橙色 75%alignContent(Alignment.Start)让前景对齐左侧起点注意这里进度条宽度是硬编码 75%没接item.progress字段数据里项目一是 75% 但代码没用。如果要做动态进度条应该把width(75%)改成width(\${item.progress}%)并在 RecordItem 加progress?: number 字段。bug 提示如果 status已完成demo 应该显示评分 5星等完成态信息但当前代码没有这层分支。desc字段写死了 评分 5星 / 评分 4星这部分文字是已完成的描述——但视觉上没有任何完成态标记区分。真实项目应在已完成时显示 ⭐⭐⭐⭐⭐ 评分图标。5.4 双按钮行查看详情 更多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%)两个并排按钮查看详情— 主操作紫色背景C.primary白字更多— 次操作浅灰背景C.cardSoft灰字两者都是layoutWeight(1)等宽。height(34)比主按钮的 48vp 矮次级操作不需要那么显眼。主次按钮的视觉权重差异通过背景色 文字色两个维度同时区分——比单靠按钮大小更细腻。promptAction.showToast({ message: item.title })反馈与首页一致弹项目一作为我点了项目一反馈。六、数据驱动的筛选逻辑待实现当前代码的activeTab只控制 TabBar 的下划线高亮没有联动记录过滤——Scroll 里始终渲染全部 5 条。要实现点击 Tab 筛选// 伪代码 private get filteredRecords(): RecordItem[] { if (this.activeTab 0) return this.records; const target this.activeTab 1 ? 进行中 : 已完成; return this.records.filter(r r.status target); } // 在 build 里改用 filteredRecords ForEach(this.filteredRecords, ...)State activeTab改变 →filteredRecords重算 → ForEach 重渲染 → Scroll 显示过滤后的列表。这是 ArkUI 声明式数据流的完整闭环。但 demo 没做这层过滤可能是为了让 5 条记录都展示在 4 张截图里方便读者查看。真实项目务必实现过滤否则 Tab 切换没意义。七、Scroll 滚动的边界问题Scroll 容器装 5 条 RecordCard BlankColumn({ space: 12 }) { ForEach(this.records, (item: RecordItem) { this.RecordCard(item) }, (item: RecordItem) item.id.toString()) Blank().height(this.safeBottom 20) }每条 RecordCard 高度约 200-300vpemoji标题描述时间进度条按钮5 条约 1000-1500vp Blank 约 60-80vp ≈ 1100-1600vp。屏幕 Scroll 视口约 1700-2000vp2832 - Header 60safeTop - StatsCard 6410 - TabBar 5010 - TabBar 60所以5 条记录完整可见不用滚动。但如果加到 10 条或更多Scroll 必须能滚到底。Blank().height(this.safeBottom 20)保证了滚到底时最后一条卡片不会贴到 Tab 栏底部而是保留 20vp 缓冲。align(Alignment.Top)让 Scroll 内容从顶部对齐不会因为内容不足而垂直居中垂直居中会显得卡片漂在空中不专业。八、设计哲学状态色的克制整页用色非常克制3 种主色紫主题、橙警示、绿成功3 级灰度text/textSub/textDim2 级背景card/cardSoft bg没有引入第 4 种颜色红 danger——因为本页没有失败/异常状态强行加红色会污染视觉。这种用最少的颜色覆盖最多的状态是设计系统建设的核心原则。statusColor字段让 5 条数据自己携带颜色属性UI 层无if/else判断。这种数据即样式的模式可以扩展到任何多种状态场景订单状态、任务状态、消息状态。九、Builder RecordCard(item) 的参数化RecordCard(item: RecordItem)与其他 Builder 不一样——它接收参数Builder RecordCard(item: RecordItem) {Builder可以接收任意类型参数调用时直接this.RecordCard(item)传入数据。这与无参 Builder如Header()形成对比——后者渲染固定内容前者渲染参数化的内容。Builder 参数 vsComponent props 的取舍Builder接收参数轻量、不能挂载状态、不能复用aboutToAppear生命周期Component接收Prop item更重、可以有自己内部状态、可以挂载生命周期如果 RecordCard 只有渲染逻辑无交互状态用Builder 参数最简洁。如果未来 RecordCard 需要内部展开/折叠等局部状态应该升级为独立Component。item: RecordItem在 ForEach 里被传入——ArkUI 的 ForEach 第三个参数是 keyGenerator(item: RecordItem) item.id.toString()而第一个参数是回调(item: RecordItem) { this.RecordCard(item) }拿到 item 后传给 RecordCard。整个数据流array → ForEach → RecordCard(item)一气呵成。十、复用与扩展Func2Tab的可复用部分Header标题副标题搜索图标— 任何列表页都能套用StatsCard3 栏彩色数字— 任何数据看板都能套用TabBar下划线指示器— 任何页面内多状态切换都能套用RecordCardemoji状态时间进度条按钮— 任何任务/订单/项目列表都能套用把它们抽到components/目录下独立组件升级为 Component换数据源、换颜色、换文案就能拼出各种数据列表多状态页面。这是组件化设计的核心价值——把一次性页面沉淀成可复用模板。十一、状态色映射的工程化写法当前RecordItem.statusColor字段把颜色直接写在数据里{ id: 1, ..., status: 进行中, statusColor: C.warn, ... } { id: 2, ..., status: 已完成, statusColor: C.ok, ... }这种数据携带视觉属性在 demo 里很方便但真实项目应该把映射抽成函数// 推荐写法 const STATUS_COLOR_MAP: Recordstring, string { 进行中: C.warn, 已完成: C.ok, 已暂停: C.textDim, 已取消: C.danger, }; // 数据只存 status 字符串不存颜色 { id: 1, status: 进行中, ... } // 渲染时查表 Text(item.status).backgroundColor(STATUS_COLOR_MAP[item.status] ?? C.textDim)这种映射表代替分支的优势有三点新增状态只改一处未来加已暂停或已取消状态只需在 STATUS_COLOR_MAP 加一行不用改 RecordItem 数据结构或 UI 渲染代码颜色集中管理所有状态 → 颜色的映射在一处方便产品经理审查红色用得对不对、橙色覆盖了哪些场景数据可序列化RecordItem 只存业务字段status 是 进行中不存视觉属性statusColor 是 #FF9F1C方便后端接口下发、JSON 序列化、跨平台数据交换?? C.textDim是 TypeScript 的空值合并——当 status 不在映射表里时用默认灰色兜底避免undefined报错。这种防御性默认值是处理枚举/映射的必备。Recordstring, string是 TypeScript 的字典类型——key 和 value 的类型都明确编译期就能检查你传错类型了。十二、Stack 进度条 vs Progress 组件的取舍本页用了 2 处 Stack 进度条首页 WeekChartCard、Func2Tab 进行中记录都是背景条 前景条叠加的写法。但 ArkUI 官方其实有Progress组件Progress({ value: 75, total: 100, type: ProgressType.Linear }) .width(100%).height(4) .color(C.warn)什么时候用 Stack 自定义什么时候用 Progress 组件用 Progress 的场景标准进度0-100 线性、对数不需要自定义样式圆角、渐变、动画需要 ProgressType.Circular环形进度常见于加载中用 Stack 自定义的场景需要复杂样式如本周条形图的渐变色 圆角 高度按数据计算需要在同一进度条叠加额外内容如右侧的75%文字需要精准控制布局如本周图的 7 根柱子之间的间距、动画总结简单进度用 Progress复杂视觉用 Stack。本页的歌单记录进度条是简单进度背景 前景 文字其实可以用 Progress 简化代码但改成 Progress 会失去圆角 主题色的视觉一致性所以保留 Stack 写法更合适。Progress 组件不一定是银弹——很多新手一看进度条就用 Progress结果发现需要圆角/渐变/自定义高度时 Progress 不够灵活又得退回 Stack 自定义。先评估是否需要复杂样式再决定用哪个。5 条状态各异的记录 3 栏彩色统计 3 段下划线 Tab 复合卡片设计让一个 demo 页面同时承载了数据可视化 多状态切换 列表筛选 复合操作的完整场景。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻