鸿蒙原生应用实战:四色占比进度条驱动的分类记录页三态操作设计

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+4个胶囊Tab+统计概览+四色占比进度条

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

配图

二、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 处应用:

  1. 占比进度条:4 条进度条各用各的颜色
  2. 时间线图标块r.catColor + '1A' 同色浅底(废电池🔋红色浅底、旧报纸📰蓝色浅底……)
  3. 分类标签catColor 文字 + 同色浅底
  4. (与首页联动):首页入口/指南卡也是四色

"一个颜色系统贯穿全 App"——用户在任何页面看到"蓝色+♻️"就知道是可回收物。"跨页颜色一致性"是品牌识别度的最高形式

实现catColor 字段存数据里('#EF4444' 红),渲染时 r.catColor + '1A' 拼浅底。"颜色进数据"让记录页零硬编码

智能垃圾分类分类页底部 · 最后一条记录+安全区留白+底部Tab栏

八、扩展建议

  1. 占比图联动筛选:点击"厨余 48%"进度条 → 自动切换到"厨余"筛选(真实项目可实现)
  2. 环形占比图:新增"占比总览"卡片用 ProgressType.Circular 环形图展示总量构成
  3. 分页加载:328 条记录应分页(.onReachEnd() 上滑加载)
  4. 详情页:"立即分类/重新分类"应跳转识别页并预填物品名(跨 Tab 联动)
  5. 删除操作:记录应支持左滑删除(SwipeAction 组件)

"记录 + 占比 + 操作"模板可复用到:垃圾分类、记账分类、消费构成、时间统计——任何"按类别记录 + 构成分析 + 状态操作"的场景

智能垃圾分类分类页中段 · 占比进度条完整+分类记录时间线前2条

九、分类占比的业务价值

"分类占比"模块不只是"好看",它承载 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 分类记录页解析完毕。四色占比进度条(构成分析)+ 分类色/状态色双编码 + 三态操作(绿处理/红纠错/绿确认)+ 状态机流转是四大亮点。

Logo

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

更多推荐