鸿蒙原生应用实战:四色占比进度条驱动的分类记录页三态操作设计
鸿蒙原生应用实战:四色占比进度条驱动的分类记录页三态操作设计
App 15「智能垃圾分类」分类 Tab(Func2Tab),是分类历史记录页。整页用 Header + 4 个胶囊筛选 Tab(全部/已分类/待处理/错误分类)+ 3 栏统计概览 + 4 类彩色占比进度条(可回收 30% 蓝/厨余 48% 绿/有害 12% 红/其他 10% 灰)+ 6 条记录时间线(每条记录按状态显示不同操作:待处理→立即分类、错误分类→重新分类、已分类→✓ 已正确分类)组成。本篇基于
15-trash-classify/entry/src/main/ets/pages/Func2Tab.ets(约 248 行)逐段拆解,附 4 张实机截图。
一、整体结构:筛选 + 统计 + 占比 + 时间线
Func2Tab 与 App 14 预约页同构(Header + TabBar 固定、Scroll 内容流),但多了"分类占比"模块:
build() {
Column() {
this.Header()
this.TabBar()
Scroll() {
Column({ space: 16 }) {
this.StatsOverview()
this.CategoryProgress()
this.SectionTitle('分类记录')
this.RecordTimeline()
}
.width('100%')
.padding({ left: D.pad, right: D.pad, top: 16, bottom: D.pad + this.safeBottom + 20 })
}
.layoutWeight(1).scrollBar(BarState.Off).align(Alignment.Top)
}
.width('100%').height('100%').backgroundColor(C.bg)
}
4 个模块:StatsOverview(3 栏统计)→ CategoryProgress(4 类占比——本页特色) → SectionTitle('分类记录') → RecordTimeline(6 条时间线)。
与 App 14 预约页的差异:App 14 是"统计 → 时间线"两段,App 15 是"统计 → 占比 → 时间线"三段——多了一层"分类构成分析"。"占比图"让用户一眼看到"我分类的垃圾都是什么类型",是数据可视化的进阶用法。


二、Header + TabBar:4 个胶囊筛选
Header 单行标题"分类记录",TabBar 是 4 个胶囊(全部/已分类/待处理/错误分类):
@Builder
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)
}
@Builder
TabBar() {
Row() {
ForEach(this.tabs, (t: FilterTab) => {
Text(t.label)
.fontSize(13)
.fontColor(this.activeTab === t.id ? C.primary : C.textSub)
.fontWeight(this.activeTab === t.id ? FontWeight.Bold : FontWeight.Normal)
.padding({ left: 14, right: 14, top: 8, bottom: 8 })
.backgroundColor(this.activeTab === t.id ? C.primarySoft : 'transparent')
.borderRadius(16)
.onClick(() => { this.activeTab = t.id; })
}, (t: FilterTab) => t.id.toString())
}
.width('100%')
.padding({ left: D.pad, right: D.pad, top: 12, bottom: 12 })
.backgroundColor(C.card)
}
4 个 Tab 的 id 与状态值绑定(App 14 同款设计):
private tabs: FilterTab[] = [
{ id: 0, label: '全部' },
{ id: 1, label: '已分类' },
{ id: 2, label: '待处理' },
{ id: 3, label: '错误分类' }
];
id = status 值:id=1 已分类(records 里 status=1)、id=2 待处理(status=2)、id=3 错误分类(status=3)——筛选逻辑 r.status === this.activeTab 一行搞定。
与 App 14 的差异:App 14 的 Tab 顺序是"全部/待确认/已完成/已取消"(按"待办优先"排列),App 15 是"全部/已分类/待处理/错误分类"(按"数量优先"排列:已分类 302 条最多放第 2)——Tab 顺序反映业务重心。
三、StatsOverview:3 栏统计
StatsOverview 与 App 14 同构(3 栏数字 + 标签):
@Builder
StatsOverview() {
Row() {
ForEach(this.stats, (s: StatItem) => {
Column({ space: 4 }) {
Text(s.value).fontSize(22).fontWeight(FontWeight.Bold).fontColor(C.text)
Text(s.label).fontSize(11).fontColor(C.textDim)
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
}, (s: StatItem) => s.label)
}
.width('100%')
.padding({ top: 16, bottom: 16 })
.backgroundColor(C.card).borderRadius(D.rMd)
.border({ width: 1, color: C.stroke })
}
数据:总记录 328 / 已分类 302 / 待处理 26。302 + 26 = 328,数据自洽(已分类 + 待处理 = 总记录,错误分类记录在 302 里计数还是单独?demo 里"错误分类"是 status=3,如果算进"已分类"口径有歧义——但 328 = 302 + 26 至少"总 = 已 + 待"成立)。
3 栏同色(C.text 深色)——"颜色留给状态"原则(占比进度条和状态标签才是彩色的)。
四、CategoryProgress:四色占比进度条(本页特色)
CategoryProgress 是 4 类垃圾的占比进度条,每类一条彩色进度条:
@Builder
CategoryProgress() {
Column({ space: 12 }) {
Text('分类占比').fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text).width('100%')
Row({ space: 8 }) {
Text('♻️ 可回收').fontSize(11).fontColor(C.textDim).width(60)
Progress({ value: 30, total: 100 }).color('#3B82F6').backgroundColor(C.cardSoft).layoutWeight(1).height(6)
Text('30%').fontSize(11).fontColor(C.textSub)
}
Row({ space: 8 }) {
Text('🍎 厨余').fontSize(11).fontColor(C.textDim).width(60)
Progress({ value: 48, total: 100 }).color('#22C55E').backgroundColor(C.cardSoft).layoutWeight(1).height(6)
Text('48%').fontSize(11).fontColor(C.textSub)
}
Row({ space: 8 }) {
Text('⚠️ 有害').fontSize(11).fontColor(C.textDim).width(60)
Progress({ value: 12, total: 100 }).color('#EF4444').backgroundColor(C.cardSoft).layoutWeight(1).height(6)
Text('12%').fontSize(11).fontColor(C.textSub)
}
Row({ space: 8 }) {
Text('🗑️ 其他').fontSize(11).fontColor(C.textDim).width(60)
Progress({ value: 10, total: 100 }).color('#9CA3AF').backgroundColor(C.cardSoft).layoutWeight(1).height(6)
Text('10%').fontSize(11).fontColor(C.textSub)
}
}
.width('100%')
.padding(14)
.backgroundColor(C.card).borderRadius(D.rMd)
.border({ width: 1, color: C.stroke })
}
4.1 4 条进度条的数据
- 厨余 48%(绿色)——占比最高,符合"厨余垃圾是日常生活产生最多垃圾"的事实
- 可回收 30%(蓝色)
- 有害 12%(红色)
- 其他 10%(灰色)
30 + 48 + 12 + 10 = 100%,数据完美闭合。占比数据有现实依据(厨余最多、有害最少——中国家庭垃圾构成确实如此)。
4.2 四色进度条的实现
Row({ space: 8 }) {
Text('♻️ 可回收').fontSize(11).fontColor(C.textDim).width(60)
Progress({ value: 30, total: 100 }).color('#3B82F6').backgroundColor(C.cardSoft).layoutWeight(1).height(6)
Text('30%').fontSize(11).fontColor(C.textSub)
}
每行三要素:左侧"emoji + 分类名"(固定 60vp 宽,四行对齐)、中间 Progress(.color('#3B82F6') 每类颜色写死)、右侧"百分比数字"。
与 App 14 的差异:App 14 是"一组同色进度条",App 15 是"四色进度条"——颜色与分类一一对应(蓝/绿/红/灰),延续首页的四色编码系统。"四色占比图"本质是"堆叠条形图"的横排版——用 4 条独立进度条表达"各部分占总体的比例",比"一个环形图"实现简单得多(不用画弧形),视觉上也清晰。
真实项目可升级为"环形占比图"(ProgressType.Circular):
Progress({ value: 48, total: 100, type: ProgressType.Circular })
.color('#22C55E').width(80).height(80)
但 4 个环形图太占空间,"4 条横排进度条"是"构成占比"的最佳空间方案(一个卡片展示 4 类)。
每行的"百分比 + 进度条"双通道(30% 数字 + 蓝色进度条),色弱用户能读数字。
五、RecordTimeline:三态操作时间线
RecordTimeline 是 6 条分类记录的时间线,每条记录按状态显示不同操作:
@Builder
RecordTimeline() {
Column({ space: 0 }) {
ForEach(this.filtered(), (r: Record, idx: number) => {
Column({ space: 0 }) {
if (idx > 0) {
Row().width(2).height(12).backgroundColor(C.stroke).margin({ left: 18 })
}
Row({ space: 12 }) {
Column() {
Row() {
Text(r.emoji).fontSize(18)
}
.width(36).height(36)
.backgroundColor(r.catColor + '1A').borderRadius(18)
.justifyContent(FlexAlign.Center)
}
.width(36)
Column({ space: 8 }) {
Row({ space: 8 }) {
Text(r.item).fontSize(14).fontWeight(FontWeight.Medium).fontColor(C.text)
Text(r.amount).fontSize(11).fontColor(C.textDim)
Blank()
Text(r.statusText)
.fontSize(11).fontColor(r.statusColor)
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.backgroundColor(C.primarySoft).borderRadius(8)
}
.width('100%')
Row({ space: 6 }) {
Text(r.category)
.fontSize(11).fontColor(r.catColor)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor(r.catColor + '1A').borderRadius(4)
Text(r.date + ' ' + r.time).fontSize(12).fontColor(C.textDim)
}
if (r.status === 2) {
Row({ space: 8 }) {
Button('立即分类')
.fontSize(12).fontColor('#FFFFFF').backgroundColor(C.primary)
.borderRadius(D.rSm).height(30)
.onClick(() => { promptAction.showToast({ message: '处理 ' + r.item }); })
Text('待处理').fontSize(11).fontColor(C.warn)
}
.width('100%')
}
if (r.status === 3) {
Row({ space: 8 }) {
Button('重新分类')
.fontSize(12).fontColor('#FFFFFF').backgroundColor(C.danger)
.borderRadius(D.rSm).height(30)
.onClick(() => { promptAction.showToast({ message: '重新分类 ' + r.item }); })
Text('分类有误').fontSize(11).fontColor(C.danger)
}
.width('100%')
}
if (r.status === 1) {
Text('✓ 已正确分类').fontSize(11).fontColor(C.ok)
}
}
.alignItems(HorizontalAlign.Start).layoutWeight(1)
}
.width('100%')
.padding({ left: 14, right: 14, top: 14, bottom: 14 })
.backgroundColor(C.card).borderRadius(D.rMd)
.border({ width: 1, color: C.stroke })
}
.width('100%')
}, (r: Record) => r.id.toString())
}
.width('100%')
}
5.1 数据模型:分类色 + 状态色双通道
interface Record {
id: number;
emoji: string;
item: string;
category: string;
catColor: string;
date: string;
time: string;
status: number;
statusText: string;
statusColor: ResourceColor;
amount: string;
}
Record 同时携带 catColor(分类色)和 statusColor(状态色)——两类颜色语义:
- 分类色(catColor):垃圾属于哪类(蓝/红/绿/灰)→ 图标块背景 + 分类标签
- 状态色(statusColor):记录处理状态(绿已分类/橙待处理/红错误)→ 状态标签
"分类"和"状态"是两个维度,用两套颜色表达,互不干扰——**"多维度信息用多套颜色编码"**是数据可视化的关键。
5.2 双标签设计
Text(r.category)
.fontSize(11).fontColor(r.catColor)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor(r.catColor + '1A').borderRadius(4)
Text(r.date + ' ' + r.time).fontSize(12).fontColor(C.textDim)
一行两个标签:左侧"分类标签"(catColor 文字 + catColor + '1A' 同色浅底,如蓝字淡蓝底"可回收物")+ 右侧日期时间。分类标签让用户不用点进详情就知道"这是什么类型的垃圾"。
5.3 三态操作区
if (r.status === 2) {
Row({ space: 8 }) {
Button('立即分类')
.fontSize(12).fontColor('#FFFFFF').backgroundColor(C.primary)
.borderRadius(D.rSm).height(30)
.onClick(() => { promptAction.showToast({ message: '处理 ' + r.item }); })
Text('待处理').fontSize(11).fontColor(C.warn)
}
.width('100%')
}
if (r.status === 3) {
Row({ space: 8 }) {
Button('重新分类')
.fontSize(12).fontColor('#FFFFFF').backgroundColor(C.danger)
.borderRadius(D.rSm).height(30)
.onClick(() => { promptAction.showToast({ message: '重新分类 ' + r.item }); })
Text('分类有误').fontSize(11).fontColor(C.danger)
}
.width('100%')
}
if (r.status === 1) {
Text('✓ 已正确分类').fontSize(11).fontColor(C.ok)
}
3 个状态分支,3 种操作:
| 状态 | 按钮 | 颜色 | 右侧文字 |
|---|---|---|---|
| 待处理(status=2) | 立即分类 | 绿(C.primary) | "待处理" 橙 |
| 错误分类(status=3) | 重新分类 | 红(C.danger) | "分类有误" 红 |
| 已分类(status=1) | 无按钮 | — | "✓ 已正确分类" 绿 |
关键差异——"错误分类"的按钮是红色:
Button('重新分类')
.fontSize(12).fontColor('#FFFFFF').backgroundColor(C.danger)
App 14 预约页的所有操作按钮都是主题绿(C.primary),App 15 的"重新分类"用了危险红——"错误"用红色强化警示(用户看到红按钮知道"这条有问题要处理")。**"操作按钮颜色跟随状态严重性"**是"状态驱动 UI"的进阶——正常操作绿、错误修复红。
"✓ 已正确分类"是完成态标识(绿字勾),不需要操作按钮——"无需操作的状态只显示确认信息"。
5.4 数据与筛选
filtered() 与 App 14 同款(activeTab === 0 返回全部,否则 filter(status === activeTab))——真筛选。6 条记录:已分类 4 条(废电池/旧报纸/香蕉皮/大骨头)+ 待处理 1 条(塑料瓶)+ 错误分类 1 条(灯泡)——三态都有数据,切换 Tab 能看到不同列表。
6 条记录按日期倒序(08-14 → 08-09),时间线呈现"最近 → 最远"顺序。
六、与 App 14 预约记录页的对比
| 维度 | App 14 预约记录 | App 15 分类记录 |
|---|---|---|
| Tab | 待确认/已完成/已取消 | 已分类/待处理/错误分类 |
| 统计 | 3 栏 | 3 栏 |
| 特色模块 | 无 | 四色占比进度条 |
| 图标块 | 主题绿(同色) | 分类色(四色) |
| 分类标签 | 无 | 有(分类色标签) |
| 状态按钮 | 取消/再次预约(绿) | 立即分类(绿)/重新分类(红) |
App 15 的两个进步:① 新增"占比进度条"(构成分析);② 引入"分类色 + 状态色"双色编码系统。"记录列表"模板在系列中不断进化——每次加一个新能力(真筛选→时间线→占比→双色编码)。
七、四色编码系统在记录页的完整应用
App 15 的四色编码系统(蓝可回收/红有害/绿厨余/灰其他)在本页的 4 处应用:
- 占比进度条:4 条进度条各用各的颜色
- 时间线图标块:
r.catColor + '1A'同色浅底(废电池🔋红色浅底、旧报纸📰蓝色浅底……) - 分类标签:
catColor文字 + 同色浅底 - (与首页联动):首页入口/指南卡也是四色
"一个颜色系统贯穿全 App"——用户在任何页面看到"蓝色+♻️"就知道是可回收物。"跨页颜色一致性"是品牌识别度的最高形式。
实现:catColor 字段存数据里('#EF4444' 红),渲染时 r.catColor + '1A' 拼浅底。"颜色进数据"让记录页零硬编码。

八、扩展建议
- 占比图联动筛选:点击"厨余 48%"进度条 → 自动切换到"厨余"筛选(真实项目可实现)
- 环形占比图:新增"占比总览"卡片用
ProgressType.Circular环形图展示总量构成 - 分页加载:328 条记录应分页(
.onReachEnd()上滑加载) - 详情页:"立即分类/重新分类"应跳转识别页并预填物品名(跨 Tab 联动)
- 删除操作:记录应支持左滑删除(
SwipeAction组件)
"记录 + 占比 + 操作"模板可复用到:垃圾分类、记账分类、消费构成、时间统计——任何"按类别记录 + 构成分析 + 状态操作"的场景。

九、分类占比的业务价值
"分类占比"模块不只是"好看",它承载 3 层业务价值:
1. 反馈层——"我的分类构成"。用户看到"厨余 48%"会想"我厨余垃圾最多,下次多注意可回收"。占比图让用户建立"我每天都在分类什么"的自我认知——这是环保教育的第一步(知道自己分类的对象)。
2. 统计层——"社区/城市的分类结构"。真实项目里,占比数据可以聚合为"全校/全小区分类构成"(厨余 48% 说明该区域餐饮垃圾多),辅助环保部门调整垃圾分类设施(多放厨余桶还是可回收桶)。"个人占比 → 群体占比"是数据产品的增值路径。
3. 激励层——"行为引导"。如果"可回收 30%"偏低,App 可提示"回收利用率低于全国平均,试试把纸箱压扁再投递"——"占比 + 建议"驱动行为改变,比单纯记录高级得多。
实现注意:当前占比是硬编码(30/48/12/10),真实项目应从分类记录实时聚合:records.filter(r => r.category === '可回收物').length / records.length * 100——"占比跟着记录走",新增一条记录占比自动更新。**"展示数据永远从业务数据推导"**是数据架构的铁律。
十、时间线的"分类色 + 状态色"双通道无障碍
本页每一条记录同时携带两个颜色维度(分类色 catColor + 状态色 statusColor),这对色弱用户特别重要:
- 分类信息:图标块背景色(红浅底/蓝浅底…)+ 分类标签文字(红字/蓝字…)——即使分不清红蓝,也能读"有害垃圾""可回收物"文字
- 状态信息:状态标签文字("待处理"橙/"分类错误"红/"已分类"绿)+ 按钮文案("立即分类""重新分类")——颜色只是辅助,文字才是主通道
"颜色 + 文字双通道"是本系列反复强调的无障碍原则,本页是执行最彻底的:分类、状态、数量、时间全部有文字,颜色只是"锦上添花"的强化。色弱用户(约 8% 人口)不靠颜色也能完整使用本页。
对比反例:如果只靠"图标块颜色深浅"区分分类(不写"可回收物"文字),色弱用户就完全无法使用。"先文字后颜色"是数据展示的底线。

十一、状态机的"待处理 → 已分类 → 错误分类"流转
本页 3 个状态背后的业务状态机:
新记录 → 待处理 → 已分类(✓ 正确)
↘ 错误分类 → 重新分类 → 已分类
- 待处理(status=2):AI 识别后待人工确认 → "立即分类"按钮处理
- 已分类(status=1):人工确认完成 → 显示"✓ 已正确分类"
- 错误分类(status=3):AI 识别错误 → "重新分类"红色按钮纠错
"待处理 → 立即分类 → 已分类"和"错误分类 → 重新分类 → 已分类"两条路径都通向终态"已分类"——状态机有"汇合"(convergence),是真实业务的常见形态(AI 先识别、人工后确认)。
UI 的操作按钮就是状态机的"事件":立即分类(处理事件)、重新分类(纠错事件)。真实项目应做"状态流转校验":待处理不能直接变错误分类、错误分类不能变待处理——"UI 按钮只是入口,状态机逻辑才是真相"。
十二、小结
App 15 分类记录页解析完毕。四色占比进度条(构成分析)+ 分类色/状态色双编码 + 三态操作(绿处理/红纠错/绿确认)+ 状态机流转是四大亮点。
更多推荐




所有评论(0)