鸿蒙原生应用实战(五):数据统计与个人中心——柱状图实现、统计计算与设置面板
鸿蒙原生应用实战(五):数据统计与个人中心——柱状图实现、统计计算与设置面板
本文是系列终篇,讲解快递追踪 App 中最后两个页面:包裹统计(PackageStatsPage)和个人中心(ProfilePage)。涵盖纯 ArkTS 柱状图实现、统计指标计算、Toggle 设置、主题色体系等完整内容,并总结全项目架构。
一、PackageStatsPage — 包裹统计页
1.1 功能需求
统计页是 App 的数据分析中心,需要展示:
- 总览卡片:总包裹数、已签收数、平均时效
- 月度趋势图:每月收发数量 + 柱状条可视化
- 快递公司分布:各公司使用数量 + 横向柱状条
1.2 数据模型
interface MonthlyStats {
month: string; // 月份标识,如 "2024-08"
count: number; // 当月包裹总数
delivered: number; // 当月已签收数
avgDays: number; // 当月平均时效(天)
}
interface CompanyStats {
name: string; // 公司名
count: number; // 使用次数
color: string; // 柱状图颜色
}
注意 CompanyStats 的 color 是字符串类型(如 '#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)
实现原理:
- 设定最大值
8(数据中最大包裹数),作为 100% 基准 - 每个柱的宽度 =
(count / 8) * 100% - 灰色背景条(
#FFF0F0F0)总是 100% 宽,蓝色前景条按比例填充 - 用
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 功能需求
- 用户信息卡片:头像 + 昵称 + 追踪数量
- 设置项:通知开关、常用快递公司、关于
- 包裹统计:运输中 / 已签收 / 异常 数量
- 快捷操作:反馈、评分、分享、同步
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 关键设计决策
- 纯原生组件,零第三方依赖:所有 UI 均使用 ArkUI 内置组件,未引入 OHPM 第三方包
- 资源集中管理:颜色、字号、间距统一在
element/JSON 中定义,通过$r()引用 - TypeScript 严格模式适配:对象字面量必须有显式类型,数组必须可推断
- 防御性编程:路由参数判空、默认值兜底
- 模拟数据驱动:所有数据使用内联 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 开发心得
-
ArkTS 声明式语法非常直观。写过 React/Vue 的开发者上手极快,
@State+ 数据驱动 UI 的模式与前端框架一脉相承。 -
鸿蒙的资源管理值得学习。通过
$r()引用资源,配合 JSON 配置文件,实现了设计 token 的集中管理。修改一个全局颜色只需改一处。 -
List 组件性能优秀。相比 Scroll + Column,List 的虚拟化机制在大列表场景下优势明显。
-
API 版本的兼容性需要注意。不同 API 版本的模块导入路径不同(如
@ohos.routervs@kit.AbilityKit),开发前要确认目标 API 版本。 -
严格模式是双刃剑。
arkts-no-untyped-obj-literals等规则增加了代码量,但提升了运行时稳定性,值得接受。
6.3 下一步
鸿蒙生态正在快速发展,API 版本持续迭代,开发工具日趋完善。建议读者关注以下方向:
- ArkTS 的新语法特性(如
@Prop、@Link双向绑定) - 鸿蒙元服务(Atomic Service)开发
- 分布式能力(跨设备流转、数据同步)
- 鸿蒙原生三方库生态的建设进展
项目源码:快递追踪 App(PackageTracker)
开发环境:DevEco Studio 6.x + HarmonyOS API 23/24
系列索引:
- 第一篇:项目初始化与工程架构
- 第二篇:首页与列表开发实战
- 第三篇:表单交互与搜索筛选
- 第四篇:物流时间线与历史记录
- 第五篇:数据统计与个人中心(本文·终篇)
感谢阅读!欢迎在评论区交流鸿蒙开发经验。
更多推荐



所有评论(0)