HarmonyOS 校园应用系列之状态机驱动:ArkTS 严格模式下,校园打印订单页双态卡片与进度条条件渲染

一、页面定位与功能概述

订单页(Func2Tab,标题显示为 "记录")是校园打印 App 的 "任务管理中心"——展示所有打印订单,支持按状态筛选(全部/进行中/已完成)。每个订单卡片包含项目名称、描述、评分、时间戳、完成百分比和操作按钮。

核心设计思想是 "状态机驱动渲染" —— 两种视图状态(进行中/已完成)下 RecordCard 展示不同样式:进行中显示橙色标签+进度条,已完成显示绿色标签+无进度条

1.1 页面状态定义

@State activeTab: number = 0          // 0=全部, 1=进行中, 2=已完成

private tabs: string[] = ['全部', '进行中', '已完成'];
private stats: StatItem[] = [
  { value: '28', label: '总数', color: C.primary },
  { value: '3', label: '进行中', color: C.warn },
  { value: '25', label: '已完成', color: C.ok },
];

private records: RecordItem[] = [
  { id: 1, emoji: '📦', title: '项目一', status: '进行中', time: '今天 14:30',
    desc: '描述信息 · 已完成75%', statusColor: C.warn },
  // ... 共 5 条记录
]

页面只有 1 个 @State 变量activeTab),统计数据与订单数据均为不可变的 private 数组。RecordItem 接口包含 7 个字段:id、emoji、title、status、time、desc、statusColor——其中 statusColor 将状态颜色直接存入数据,渲染时无需再写条件判断。


应用效果图

订单记录页首屏

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

配图

二、页面头部 —— 标题 + 统计 + 搜索

@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 })
}

头部是 "主标题+副标题"双行结构:左侧 20vp 加粗"记录"与 10vp 灰色副标题纵向排列。副标题 ${this.records.length} 条记录 用模板字符串动态统计记录数(当前为 5 条),让用户对数据规模有即时认知。右侧是 🔍 emoji 搜索按钮(C.cardSoft 底 36×36 方块),点击弹出"搜索" toast。头部高度 safeTop + 60 配合 alignItems(VerticalAlign.Bottom) 让内容沉底对齐,底部 1vp C.stroke 分隔线构成通栏吸顶效果。


三、StatsCard —— 三维统计数据卡片

@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 })
}
维度颜色语义
总数 28绿色 C.primary品牌主色,整体规模
进行中 3橙色 C.warn警示,有待处理
已完成 25绿色 C.ok正向反馈

三个统计项由 ForEach 数据驱动渲染,项与项之间用 竖向 Divider().vertical(true).height(28) 分隔(最后一项后不画),layoutWeight(1) 保证三等分。语义化着色 让用户在 0.5 秒内把握整体状态分布。


四、TabBar —— 筛选标签栏

@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 })
}

4.1 Indicator 设计规范

底部选中指示器遵循 Material Design Tabs 规范:

  • 固定宽度 24vp、高度 2vp、圆角 1vp:形成"浮动指示器"效果
  • 未选中时透明而非消失backgroundColor('transparent') 保持布局稳定,避免切换时跳动
  • 文字同步变化:选中项绿色加粗(C.primary + FontWeight.Bold),未选中灰色常规

整个 TabBar 是白底圆角卡片(C.card + D.rSm + C.stroke 边框),三个标签 layoutWeight(1) 三等分。

4.2 数据过滤逻辑

注意:当前源码中切换 Tab 只更新 activeTab 状态,列表并未实际过滤——5 条记录始终全部显示。若要实现真正的筛选,可配合计算:

// activeTab: 0→全部, 1→进行中, 2→已完成
let filtered = this.activeTab === 0 ? this.records
  : this.records.filter(r => r.status === (this.activeTab === 1 ? '进行中' : '已完成'));

订单页 · 状态筛选交互

五、Scroll 列表与 ForEach keyGenerator

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

列表直接内嵌在 build()Scroll 中(没有单独的 RecordList Builder),.layoutWeight(1) 让滚动区占满头部/统计/TabBar 以下的剩余空间,scrollBar(BarState.Off) 隐藏滚动条。keyGenerator 使用 item.id.toString() 作为唯一标识,确保数据变化时 ArkUI 能正确执行增量 DOM diff 而非重建整个列表。列表末尾的 Blank().height(this.safeBottom + 20) 为底部安全区留白。


订单页 · 记录卡详情交互

六、RecordCard —— 订单卡片的条件渲染

@Builder
RecordCard(item: RecordItem) {
  Column({ space: 10 }) {
    // 第一行:emoji 图标 + 标题/描述/时间 + 状态标签 + 箭头
    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 })
}

6.1 三种条件渲染方式对比

方式适用场景性能特点
if/else完全不同的 UI 结构销毁旧节点,创建新节点
visibility相同结构仅显隐保留 DOM,切换 display
opacity需要过渡动画保留 DOM,改透明度

本例使用 if/else 是正确的——"进行中"有进度条而"已完成"没有,UI 结构确实不同。

6.2 状态标签色彩系统

状态标签是 白字 + 语义色背景 的胶囊样式,颜色直接取自数据字段 item.statusColor

状态文字色背景色
进行中白色C.warn(琥珀)
已完成白色C.ok(绿色)

把颜色存进数据而非在模板里写三元表达式,是 "数据即样式" 的极简实践——新增状态(如"已取消"红色)只需扩展数据,无需改动渲染代码。

6.3 进度条:Stack 双层实现

Text('进度') → Stack[ 底层轨道 100% × 4vp 灰色 | 上层填充 75% × 4vp 橙色 ] → Text('75%')

进度条用 Stack 叠放两层 Column 实现:底层是 width('100%') 的灰色轨道(C.cardSoft),上层是 width('75%') 的橙色填充(C.warn),alignContent: Alignment.Start 保证填充从左端开始。轨道高度 4vp 极为纤细,清晰可见又不厚重。填充色用琥珀色 C.warn 与"进行中"标签呼应。


七、按钮组交互模式

每个 RecordCard 底部双按钮遵循 Material Design "Filled + Tonal Button" 规范:

属性主按钮(查看详情)次按钮(更多)
背景色绿色 C.primary浅灰 C.cardSoft
文字色白色 #FFFFFF灰色 C.textSub
视觉权重强(行动召唤)弱(辅助入口)

两者共享相同尺寸规格(fontSize(12), height(34), borderRadius(D.rSm), layoutWeight(1)),看起来像一个整体。两个按钮均已绑定点击事件:主按钮弹出订单标题 toast,次按钮弹出"更多操作" toast。


订单页 - 记录列表详情

八、"UI = f(state)" 数据驱动模式

整个订单页大量使用 "数据 → 样式" 动态映射:

// 状态颜色 → 标签背景(颜色存于数据字段)
.backgroundColor(item.statusColor)

// 状态 → 是否渲染
if (item.status === '进行中') { /* 进度条 */ }

// 记录数 → 副标题文案
Text(`${this.records.length} 条记录`)

所有视觉表现由数据状态唯一确定,不存在游离于状态之外的 UI 变量。这正是声明式 UI 的核心优势。


九、边界状态扩展思考

空状态:当过滤后的记录为空时展示引导内容(当前源码未实现,属扩展方向):

Column() {
  Text('📭').fontSize(48)
  Text('暂无订单记录').fontSize(16).fontColor(C.textSub)
  Button('立即创建').onClick(() => { /* 切换 tab */ })
}.justifyContent(FlexAlign.Center)

加载状态:请求期间显示 LoadingProgress()

精心设计的空状态能将"无数据"的负面体验转化为引导操作的正面动力。


十、订单状态机扩展

实际产品的完整状态机:

状态标签色操作
pending 待支付灰色支付/取消
queued 排队中橙色查看位置
printing 打印中蓝色实时进度
completed 已完成绿色评价/再打印
failed 失败红色重打/退款

关键原则:每种状态都有明确的视觉标识和可用的操作路径


总结

订单页展示了 ArkUI 列表开发的完整模式:StatsCard 提供数据概览、TabBar 实现状态筛选、RecordCard 通过条件渲染适配不同状态、Stack 双层进度条可视化完成进度。其中 if/else 条件渲染statusColor 数据驱动配色Stack 双层进度条 是最具复用价值的技术模式。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐