鸿蒙原生应用实战(五):数据统计与个人中心——柱状图实现、统计计算与设置面板

本文是系列终篇,讲解快递追踪 App 中最后两个页面:包裹统计(PackageStatsPage)和个人中心(ProfilePage)。涵盖纯 ArkTS 柱状图实现、统计指标计算、Toggle 设置、主题色体系等完整内容,并总结全项目架构。


一、PackageStatsPage — 包裹统计页

1.1 功能需求

统计页是 App 的数据分析中心,需要展示:

  1. 总览卡片:总包裹数、已签收数、平均时效
  2. 月度趋势图:每月收发数量 + 柱状条可视化
  3. 快递公司分布:各公司使用数量 + 横向柱状条

1.2 数据模型

interface MonthlyStats {
  month: string;     // 月份标识,如 "2024-08"
  count: number;     // 当月包裹总数
  delivered: number; // 当月已签收数
  avgDays: number;   // 当月平均时效(天)
}

interface CompanyStats {
  name: string;      // 公司名
  count: number;     // 使用次数
  color: string;     // 柱状图颜色
}

注意 CompanyStatscolor 是字符串类型(如 '#FF4A90D9'),因为在 ArkTS 中动态颜色字符串可以通过 fill() 直接传入。

1.3 数据初始化

使用 aboutToAppear 生命周期进行数据初始化:

aboutToAppear(): void {
  // 月度数据
  let m1: MonthlyStats = { month: '2024-08', count: 3, delivered: 3, avgDays: 2.5 };
  let m2: MonthlyStats = { month: '2024-09', count: 5, delivered: 4, avgDays: 3.0 };
  // ...
  this.monthlyData = [m1, m2, m3, m4, m5, m6];

  // 公司分布
  let c1: CompanyStats = { name: '顺丰速运', count: 8, color: '#FF4A90D9' };
  // ...
  this.companyStats = [c1, c2, c3, c4, c5, c6];
}

为什么在 aboutToAppear 而不是 build 中初始化?

  • 分离数据准备和 UI 构建
  • 数据初始化只执行一次,而非每次渲染

1.4 ArkTS 严格对象字面量规则

在 ArkTS 严格模式下(arkts-no-untyped-obj-literals),不能直接写:

// ❌ 编译错误:对象字面量必须有显式类型
this.monthlyData = [
  { month: '2024-08', count: 3, delivered: 3, avgDays: 2.5 }
];

必须将每个对象字面量赋值给类型化变量后再引用:

// ✅ 正确做法:提取为独立类型变量
let m1: MonthlyStats = { month: '2024-08', count: 3, delivered: 3, avgDays: 2.5 };
this.monthlyData = [m1];

这个规则初看繁琐,但有助于提升代码可读性和类型安全。

1.5 总览卡片

Column() {
  Text('总览')

  Row() {
    Column() {
      Text(this.totalPackages.toString())
        .fontSize(32).fontWeight(FontWeight.Bold)
        .fontColor($r('app.color.primary'))
      Text('总包裹数')
    }
    Column() {
      Text(this.totalDelivered.toString())
        .fontSize(32).fontWeight(FontWeight.Bold)
        .fontColor($r('app.color.status_delivered'))
      Text('已签收')
    }
    Column() {
      Text(this.overallAvgDays + '天')
        .fontSize(24).fontWeight(FontWeight.Bold)
        .fontColor($r('app.color.rating_star'))
      Text('平均时效')
    }
  }
}

三个指标,三种颜色

指标 字体大小 颜色 含义
总包裹数 32fp 主题蓝 核心指标
已签收数 32fp 签收绿 正向指标
平均时效 24fp (稍小) 星标黄 参考指标

1.6 计算属性(getter)

get totalPackages(): number {
  let sum = 0;
  for (let m of this.monthlyData) {
    if (m) sum += m.count;
  }
  return sum;
}

get totalDelivered(): number {
  let sum = 0;
  for (let m of this.monthlyData) {
    if (m) sum += m.delivered;
  }
  return sum;
}

get overallAvgDays(): string {
  let sum = 0;
  let count = 0;
  for (let m of this.monthlyData) {
    if (m && m.delivered > 0) {
      sum += m.avgDays * m.delivered;
      count += m.delivered;
    }
  }
  return count > 0 ? (sum / count).toFixed(1) : '0';
}

加权平均计算avgDays 是按包裹数加权的,即 (avgDays1 * delivered1 + avgDays2 * delivered2 + ...) / totalDelivered,这样更准确。


二、纯 ArkTS 柱状图实现

没有使用第三方图表库,完全用 ArkTS 原生组件绘制。

2.1 月度趋势柱状图

ForEach(this.monthlyData, (month: MonthlyStats) => {
  Column() {
    // 行标签:月份 + 数量
    Row() {
      Text(month.month).width(70)
      Blank()
      Text(month.count + '件')
    }

    // 柱状条
    Row() {
      Column() {
        Row() {
          Column()
            .width((month.count / 8) * 100 + '%')  // ← 比例宽度
            .height(16)
            .backgroundColor($r('app.color.primary'))
            .borderRadius({ topLeft: 8, bottomLeft: 8 })
        }
        .width('100%')
        .backgroundColor('#FFF0F0F0')  // 灰色背景条
        .borderRadius(8)
      }
      .layoutWeight(1)
    }

    // 辅助信息
    Text('已签收: ' + month.delivered + ' | 平均 ' + month.avgDays + '天')
  }
}, (month: MonthlyStats) => month.month)

实现原理

  1. 设定最大值 8(数据中最大包裹数),作为 100% 基准
  2. 每个柱的宽度 = (count / 8) * 100%
  3. 灰色背景条(#FFF0F0F0)总是 100% 宽,蓝色前景条按比例填充
  4. borderRadius({ topLeft: 8, bottomLeft: 8 }) 让柱状条左侧圆角

2.2 快递公司分布图

ForEach(this.companyStats, (stat: CompanyStats) => {
  Row() {
    // 颜色圆点 + 公司名
    Circle().width(12).height(12).fill(stat.color)
    Text(stat.name).width(80).margin({ left: 8 })

    // 柱状条
    Column() {
      Row() {
        Column()
          .width((stat.count / 8) * 100 + '%')
          .height(14)
          .backgroundColor(stat.color)    // ← 使用公司专属颜色
          .borderRadius(7)
      }
      .width('100%')
      .backgroundColor('#FFF0F0F0')
      .borderRadius(7)
    }
    .layoutWeight(1)

    // 数量标签
    Text(stat.count + '件').width(40).textAlign(TextAlign.End)
  }
}, (stat: CompanyStats) => stat.name)

与月度趋势的不同

  • 柱状条使用公司专属颜色而非统一蓝色
  • 左侧多了颜色圆点 + 公司名
  • 右侧多了数量文字标签
  • 柱状条高度 14vp(稍细)

2.3 柱状图比例计算的风险

.width((stat.count / 8) * 100 + '%')

如果最大值 8 是硬编码的,当数据变化(比如某月有 10 个包裹),柱状条会超出 100%。更健壮的方式:

// 计算实际最大值
get maxCount(): number {
  let max = 1;  // 避免除以 0
  for (let m of this.monthlyData) {
    if (m.count > max) max = m.count;
  }
  return max;
}

// 动态计算宽度
.width((month.count / this.maxCount) * 100 + '%')

但为了示例简洁,当前版本使用了固定最大值。


三、ProfilePage — 个人中心

3.1 功能需求

  1. 用户信息卡片:头像 + 昵称 + 追踪数量
  2. 设置项:通知开关、常用快递公司、关于
  3. 包裹统计:运输中 / 已签收 / 异常 数量
  4. 快捷操作:反馈、评分、分享、同步

3.2 用户信息卡片

Column() {
  Circle()
    .width(64).height(64)
    .fill($r('app.color.primary'))
  Text('快递追踪用户')
  Text('已追踪 8 个包裹')
    .fontSize($r('app.float.small_font_size'))
    .fontColor($r('app.color.text_hint'))
}
.alignItems(HorizontalAlign.Center)

使用圆形头像 + 居中布局,简洁的用户信息展示。

3.3 设置项列表

Column() {
  // 通知设置(带 Toggle 开关)
  Row() {
    Text($r('app.string.notification_setting'))
    Blank()
    Toggle({ type: ToggleType.Switch, isOn: this.notifyEnabled })
      .onChange((value: boolean) => { this.notifyEnabled = value; })
  }
  .height(52)

  Divider().width('100%')

  // 常用快递公司(带跳转箭头)
  Row() {
    Text($r('app.string.courier_company'))
    Blank()
    Text('顺丰、圆通、中通 ...')
      .fontColor($r('app.color.text_hint'))
    Text('>').fontColor($r('app.color.text_hint'))
  }
  .height(52)

  Divider()

  // 关于
  Row() {
    Text($r('app.string.about_app'))
    Blank()
    Text('v1.0.0').fontColor($r('app.color.text_hint'))
  }
  .height(52)
}

设置项设计模式

  • 每行固定高度 52vp,保证点击区域足够大
  • Divider 分隔每一项
  • 右侧统一使用 text_hint 灰色显示辅助信息
  • > 箭头暗示可点击跳转

3.4 包裹统计卡片

Column() {
  Text('包裹统计')
    .fontWeight(FontWeight.Medium)

  Row() {
    Column() {
      Text('3').fontColor($r('app.color.primary'))
      Text('运输中')
    }
    Column() {
      Text('5').fontColor($r('app.color.status_delivered'))
      Text('已签收')
    }
    Column() {
      Text('1').fontColor($r('app.color.status_exception'))
      Text('异常')
    }
  }
}

与首页状态颜色保持一致:运输中(蓝色)、已签收(绿色)、异常(红色)。三个统计值之和为 3+5+1=9,与首页数据对应。

3.5 快捷操作区

Row() {
  Column() {
    Text('📋'); Text('问题反馈')
  }
  Column() {
    Text('⭐'); Text('评分')
  }
  Column() {
    Text('📤'); Text('分享')
  }
  Column() {
    Text('🔄'); Text('同步')
  }
}

四宫格布局设计

  • 使用 layoutWeight(1) 均匀分布
  • Emoji 图标 + 小字标签
  • 每个 Column 居中排列
  • 四列等宽,视觉平衡

四、颜色与主题体系总结

4.1 全项目颜色映射

资源名 色值 用途
primary #FF4A90D9 主题蓝:标题、按钮、选中态
background #FFF5F5F5 页面背景
card_bg #FFFFFF 卡片背景
text_primary #FF333333 主文字
text_secondary #FF666666 次要文字
text_hint #FF999999 提示文字
divider #FFE0E0E0 分割线
status_transit #FFFF8C00 运输中(橙色)
status_delivered #FF4CAF50 已签收(绿色)
status_exception #FFF44336 异常(红色)
rating_star #FFFFC107 星标黄

4.2 字号体系

资源名 用途
page_title_font_size 22fp 页面标题
body_font_size 16fp 正文/列表标题
small_font_size 13fp 次要信息
badge_font_size 11fp 状态标签

4.3 间距体系

资源名 用途
padding_small 8vp 小间距
padding_medium 16vp 卡片内边距
padding_large 24vp 大间距

五、全项目架构总结

5.1 页面关系图

Index (首页)
 ├── → AddPackagePage    (添加包裹)
 ├── → TrackDetailPage   (物流详情,带参数)
 ├── → SearchPage        (搜索页)
 ├── → PackageStatsPage  (统计页)
 └── → CompanyManagePage (公司管理)

ProfilePage (个人中心)
 ├── 通知设置 (Toggle)
 ├── 常用公司 → CompanyManagePage
 └── 快捷操作

SearchPage (搜索)
 ├── TextInput + 筛选标签
 └── → TrackDetailPage (搜索结果点击)

HistoryPage (历史记录)
 └── 清空 + 底部统计

5.2 技术栈一览

技术维度 使用方案
开发模型 Stage 模型
UI 语言 ArkTS(ArkUI 声明式语法)
状态管理 @State 装饰器
路由 @ohos.router(API 23)
资源管理 $r() 引用 string/color/float
构建工具 Hvigor
列表渲染 List + ForEach + ListItem
生命周期 aboutToAppear / build

5.3 关键设计决策

  1. 纯原生组件,零第三方依赖:所有 UI 均使用 ArkUI 内置组件,未引入 OHPM 第三方包
  2. 资源集中管理:颜色、字号、间距统一在 element/ JSON 中定义,通过 $r() 引用
  3. TypeScript 严格模式适配:对象字面量必须有显式类型,数组必须可推断
  4. 防御性编程:路由参数判空、默认值兜底
  5. 模拟数据驱动:所有数据使用内联 mock 数据,便于后续接入后端 API

5.4 可扩展方向

  • 数据持久化:当前数据在内存中,关闭应用即丢失。可集成 @ohos.data.preferences@ohos.data.relationalStore 实现持久化
  • 网络请求:接入真实快递查询 API,使用 @ohos.net.http 发起 HTTP 请求
  • 推送通知:集成通知服务,包裹状态变化时推送
  • 多端适配:当前仅适配 phone,可扩展支持 tablet、wearable
  • 动画优化:添加列表项入场动画、状态切换过渡动画

在这里插入图片描述

六、写在最后

6.1 本系列回顾

五篇文章完整记录了一个鸿蒙原生应用从零到一的开发过程:

篇目 核心内容 技术要点
第一篇 项目初始化与工程架构 Stage 模型、module.json5、路由注册
第二篇 首页与列表开发 List + ForEach、@State、空状态
第三篇 表单交互与搜索筛选 表单验证、多条件搜索、Toggle 开关
第四篇 物流时间线与历史记录 时间线 UI、router 传参、Line 组件
第五篇 数据统计与个人中心 柱状图、统计计算、设置面板

6.2 开发心得

  1. ArkTS 声明式语法非常直观。写过 React/Vue 的开发者上手极快,@State + 数据驱动 UI 的模式与前端框架一脉相承。

  2. 鸿蒙的资源管理值得学习。通过 $r() 引用资源,配合 JSON 配置文件,实现了设计 token 的集中管理。修改一个全局颜色只需改一处。

  3. List 组件性能优秀。相比 Scroll + Column,List 的虚拟化机制在大列表场景下优势明显。

  4. API 版本的兼容性需要注意。不同 API 版本的模块导入路径不同(如 @ohos.router vs @kit.AbilityKit),开发前要确认目标 API 版本。

  5. 严格模式是双刃剑arkts-no-untyped-obj-literals 等规则增加了代码量,但提升了运行时稳定性,值得接受。

6.3 下一步

鸿蒙生态正在快速发展,API 版本持续迭代,开发工具日趋完善。建议读者关注以下方向:

  • ArkTS 的新语法特性(如 @Prop@Link 双向绑定)
  • 鸿蒙元服务(Atomic Service)开发
  • 分布式能力(跨设备流转、数据同步)
  • 鸿蒙原生三方库生态的建设进展

项目源码:快递追踪 App(PackageTracker)
开发环境:DevEco Studio 6.x + HarmonyOS API 23/24
系列索引

  • 第一篇:项目初始化与工程架构
  • 第二篇:首页与列表开发实战
  • 第三篇:表单交互与搜索筛选
  • 第四篇:物流时间线与历史记录
  • 第五篇:数据统计与个人中心(本文·终篇)

感谢阅读!欢迎在评论区交流鸿蒙开发经验。

Logo

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

更多推荐