鸿蒙原生应用实战(四):愿望单与个人统计 --- 数据聚合与可视化
一、前言
前文我们完成了首页、游戏库和详情页。本篇将开发最后两个页面------WishPage(愿望单) 和 StatsPage(统计中心)。
这两个页面代表了 App 数据维度的两个方向:
- 愿望单:面向未来的"想玩游戏",帮助用户规划购游决策
- 统计中心:面向过去的"游戏数据",通过可视化洞察玩家行为
二、愿望单页面 (WishPage)
2.1 页面功能
┌─────────────────────────────────┐
│ ← ⭐ 愿望单 5款 │
├─────────────────────────────────┤
│ 🔴 高 3 🟡 中 1 🟢 低 1 │ ← 优先级概览
│ 💰 总价估算 │
├─────────────────────────────────┤
│ 排序: 添加时间 ▼ [+ 添加] │ ← 排序 + 添加按钮
├─────────────────────────────────┤
│ ┌───────────────────────────┐ │
│ │ 🎮 ┌────────────────┐ │ │
│ │ │ 黑神话:悟空 ¥268│ │ │
│ │ │ PC · 动作 │ │ │
│ │ │ 🔴 高优先级 📅待定│ │ │ ← 愿望单卡片
│ │ └────────────────┘ │ │
│ └───────────────────────────┘ │
│ ┌───────────────────────────┐ │
│ │ 💡 愿望单小贴士 │ │ ← 小贴士区域
│ │ • 关注游戏折扣 │ │
│ │ • 设置优先级 │ │
│ │ • 查看评测后再购买 │ │
│ └───────────────────────────┘ │
└─────────────────────────────────┘
2.2 数据模型
愿望单数据比游戏库多了价格、优先级等专属字段:
typescript
interface WishItem {
id: number;
title: string;
platform: string;
genre: string;
price: string; // 价格,如 "¥268" 或 "待定"
releaseDate: string; // 发行日期,如 "2025-08-20" 或 "待定"
priority: string; // 优先级:高 / 中 / 低
addedDate: string; // 添加日期
coverColor: string; // 封面颜色
}
2.3 初始化数据
typescript
loadWishlist(): void {
this.wishItems = [
{ id: 6, title: '黑神话: 悟空', platform: 'PC', genre: '动作',
price: '¥268', releaseDate: '2025-08-20', priority: '高',
addedDate: '2025-01-10', coverColor: '#1A1A2E' },
{ id: 7, title: '空洞骑士: 丝之歌', platform: 'PC', genre: '类银河城',
price: '待定', releaseDate: '待定', priority: '高',
addedDate: '2025-01-08', coverColor: '#E91E63' },
{ id: 8, title: '歧路旅人2', platform: 'Switch', genre: 'JRPG',
price: '¥298', releaseDate: '2025-06-01', priority: '中',
addedDate: '2025-01-05', coverColor: '#FF6B35' },
{ id: 13, title: '女神异闻录6', platform: 'PS5', genre: 'JRPG',
price: '待定', releaseDate: '待定', priority: '高',
addedDate: '2024-12-20', coverColor: '#E74C3C' },
{ id: 14, title: '丝之歌', platform: 'PC', genre: '类银河城',
price: '待定', releaseDate: '待定', priority: '中',
addedDate: '2024-12-15', coverColor: '#9B59B6' },
{ id: 15, title: '动物森友会 新作', platform: 'Switch', genre: '模拟',
price: '待定', releaseDate: '待定', priority: '低',
addedDate: '2024-11-01', coverColor: '#2ECC71' }
];
}
2.4 Header --- 带数据反馈
typescript
@Builder buildHeader() {
Row() {
Text('←').fontSize(20).fontColor('#333333')
.onClick(() => { router.back(); })
Blank()
Text('⭐ 愿望单')
.fontSize(18).fontWeight(FontWeight.Bold).fontColor('#1A1A2E')
Blank()
Text(`${this.wishItems.length}款`)
.fontSize(14).fontColor('#FF6B35').fontWeight(FontWeight.Medium)
}
.width('100%').padding({ left: 16, right: 16, top: 12, bottom: 12 })
.backgroundColor('#FFFFFF')
}
右侧实时显示「N款」,给用户即时的数据反馈。
2.5 优先级概览卡片
typescript
@Builder buildSummary() {
Row() {
Column() {
Text('🔴').fontSize(18)
Text(`高 ${this.wishItems.filter(w => w.priority === '高').length}`)
.fontSize(12).fontColor('#E74C3C').margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('🟡').fontSize(18)
Text(`中 ${this.wishItems.filter(w => w.priority === '中').length}`)
.fontSize(12).fontColor('#F39C12').margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('🟢').fontSize(18)
Text(`低 ${this.wishItems.filter(w => w.priority === '低').length}`)
.fontSize(12).fontColor('#2ECC71').margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('💰').fontSize(18)
Text('总价估算').fontSize(11).fontColor('#999999').margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
}
.width('100%').padding(14).backgroundColor('#FFFFFF')
.margin({ top: 8, left: 16, right: 16 }).borderRadius(10)
}
设计思考 :使用 Emoji 圆点(🔴🟡🟢)配合颜色文字,比纯色块更生动。每个优先级用 filter 动态计数。
2.6 优先级颜色映射
typescript
getPriorityColor(priority: string): ResourceStr {
if (priority === '高') return '#E74C3C'; // 红色 - 紧迫
if (priority === '中') return '#F39C12'; // 橙色 - 适中
return '#95A5A6'; // 灰色 - 观望
}
2.7 排序功能
typescript
@Builder buildSortRow() {
Row() {
Text('排序:').fontSize(12).fontColor('#999999')
Row() {
Text(this.sortMode).fontSize(12).fontColor('#FF6B35')
Text(' ▼').fontSize(10).fontColor('#FF6B35')
}
.padding({ left: 8, right: 10, top: 3, bottom: 3 })
.backgroundColor('#FFF0E8').borderRadius(10).margin({ left: 6 })
.onClick(() => {
const modes: string[] = ['添加时间', '优先级', '发行日期'];
const idx: number = modes.indexOf(this.sortMode);
this.sortMode = modes[(idx + 1) % modes.length];
})
Blank()
Text('➕ 添加').fontSize(12).fontColor('#FF6B35')
.fontWeight(FontWeight.Medium)
}
.width('100%').padding({ left: 16, right: 16, top: 8 })
}
与游戏库页面的排序模式一致,保持交互统一性。
2.8 愿望单卡片
typescript
@Builder buildWishCard(item: WishItem) {
Row() {
Stack() {
Column().width(60).height(80).borderRadius(8)
.backgroundColor(item.coverColor)
.justifyContent(FlexAlign.Center)
Text('🎮').fontSize(24)
}.width(60).height(80)
Column() {
Row() {
Text(item.title).fontSize(15).fontWeight(FontWeight.Medium)
.fontColor('#1A1A2E').layoutWeight(1)
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
Text(item.price).fontSize(12).fontWeight(FontWeight.Bold)
.fontColor('#FF6B35')
}.width('100%')
Text(`${item.platform} · ${item.genre}`)
.fontSize(12).fontColor('#999999').margin({ top: 4 })
Row() {
Text(item.priority === '高' ? '🔴 高优先级'
: item.priority === '中' ? '🟡 中优先级'
: '🟢 低')
.fontSize(11).fontColor(this.getPriorityColor(item.priority))
Text(` 📅 ${item.releaseDate}`)
.fontSize(11).fontColor('#BBBBBB').margin({ left: 6 })
}.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start).margin({ left: 10 }).layoutWeight(1)
}
.width('100%').padding(12).backgroundColor('#FFFFFF')
.borderRadius(10).margin({ top: 8, left: 16, right: 16 })
.onClick(() => {
router.pushUrl({
url: 'pages/GameDetailPage',
params: { gameId: item.id }
})
})
}
与游戏库卡片的异同:
- 相同:左侧封面色块、右侧信息布局、整体尺寸
- 不同:展示价格(橙色显眼)、优先级(带emoji)、发行日期
- 点击同样跳转详情页
2.9 愿望单小贴士
typescript
@Builder buildEmptySuggestion() {
Column() {
Text('💡 愿望单小贴士')
.fontSize(14).fontWeight(FontWeight.Bold).fontColor('#1A1A2E')
.width('100%')
Column() {
Row() {
Text('•').fontSize(12).fontColor('#FF6B35')
Text(' 关注游戏折扣,愿望单中降价会通知你')
.fontSize(12).fontColor('#666666').margin({ left: 4 })
}.width('100%').margin({ top: 6 })
Row() {
Text('•').fontSize(12).fontColor('#FF6B35')
Text(' 设置优先级,帮你决定先入手哪款')
.fontSize(12).fontColor('#666666').margin({ left: 4 })
}.width('100%').margin({ top: 4 })
Row() {
Text('•').fontSize(12).fontColor('#FF6B35')
Text(' 查看游戏评测后再决定是否购买')
.fontSize(12).fontColor('#666666').margin({ left: 4 })
}.width('100%').margin({ top: 4 })
}.width('100%').margin({ top: 8 })
}
.width('100%').padding(16).backgroundColor('#FFFFFF')
.borderRadius(10).margin({ top: 12, left: 16, right: 16 })
}
这个小贴士模块既丰富了页面内容,又在 UI 上平衡了列表的视觉密度。
2.10 组装
typescript
build(): void {
Column() {
this.buildHeader()
Scroll() {
Column() {
this.buildSummary()
this.buildSortRow()
ForEach(this.wishItems, (item: WishItem) => {
this.buildWishCard(item)
}, (item: WishItem) => item.id.toString())
this.buildEmptySuggestion()
}.width('100%').padding({ bottom: 30 })
}
.scrollable(ScrollDirection.Vertical).layoutWeight(1).width('100%')
}
.width('100%').height('100%').backgroundColor('#F5F5F5')
}
三、统计中心页面 (StatsPage) --- 340行数据可视化
3.1 页面功能
┌─────────────────────────────────┐
│ ← 📊 游戏统计 分享 │
├─────────────────────────────────┤
│ 🎮 12 🏆 5 ⏱️ 979h 📊 42%│ ← 统计概览网格
├─────────────────────────────────┤
│ 🎯 类型分布 │ ← 横向条形图
│ 🟧 开放世界 ████████████ 3款 │
│ 🟦 动作RPG ████████ 2款 │
│ 🟩 JRPG ████████████ 3款 │
│ 🟥 动作 ████████ 2款 │
│ ⬜ 其他 ████████ 2款 │
├─────────────────────────────────┤
│ 📅 月度活跃 │ ← 柱状图
│ 1月 2月 3月 4月 5月 6月 ... │
├─────────────────────────────────┤
│ 🏅 游戏之最 │ ← 游戏之最
│ 🏆 巫师3:狂猎 210小时 │
│ 平均评分 47/50 | 完成度63% │
├─────────────────────────────────┤
│ ⏱️ 时间分布 │ ← 时间排名
│ 巫师3 ████████████████ 21% │
│ 艾尔登 ███████████████ 19% │
│ ... │
├─────────────────────────────────┤
│ 🏅 成就进度 │
│ 12 | 6 已解锁 | 50%完成率 │
├─────────────────────────────────┤
│ 📈 年度对比 │
│ 2024: 8款 420h | 2025: 4款 180h│
└─────────────────────────────────┘
3.2 数据模型
typescript
interface GameStat {
label: string; // 标签名
value: string; // 数值
color: ResourceStr; // 主题色
icon: string; // 图标
}
interface GenrePie {
name: string; // 类型名
count: number; // 数量
color: ResourceStr; // 颜色
}
3.3 状态定义
typescript
@State stats: GameStat[] = [];
@State genreStats: GenrePie[] = [];
@State topGame: string = '巫师3: 狂猎';
@State topHours: number = 210;
@State avgRating: number = 47;
@State avgCompletion: number = 63;
3.4 统计数据初始化
typescript
calcStats(): void {
this.stats = [
{ label: '游戏总数', value: '12', color: '#FF6B35', icon: '🎮' },
{ label: '通关数', value: '5', color: '#2ECC71', icon: '🏆' },
{ label: '总时长', value: '979h', color: '#3498DB', icon: '⏱️' },
{ label: '完成率', value: '42%', color: '#9B59B6', icon: '📊' }
];
this.genreStats = [
{ name: '开放世界', count: 3, color: '#FF6B35' },
{ name: '动作RPG', count: 2, color: '#3498DB' },
{ name: 'JRPG', count: 3, color: '#2ECC71' },
{ name: '动作', count: 2, color: '#E74C3C' },
{ name: '其他', count: 2, color: '#95A5A6' }
];
}
3.5 统计概览网格
typescript
@Builder buildStatGrid() {
Column() {
Row() {
ForEach(this.stats, (stat: GameStat) => {
Column() {
Text(stat.icon).fontSize(22)
Text(stat.value).fontSize(20).fontWeight(FontWeight.Bold)
.fontColor(stat.color as string).margin({ top: 4 })
Text(stat.label).fontSize(11).fontColor('#999999').margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
}, (stat: GameStat) => stat.label)
}
}
.width('100%').padding(14).backgroundColor('#FFFFFF')
.borderRadius(10).margin({ top: 8, left: 16, right: 16 })
}
使用 ForEach + layoutWeight(1) 实现 4 列等宽布局。
3.6 类型分布 --- 横向条形图 ⭐
这是我们用纯 ArkTS 组件实现的最典型的条形图:
typescript
@Builder buildGenreStats() {
Column() {
Text('🎯 类型分布')
.fontSize(15).fontWeight(FontWeight.Bold).fontColor('#1A1A2E')
.width('100%')
ForEach(this.genreStats, (genre: GenrePie) => {
Row() {
// 颜色圆点
Column().width(10).height(10).borderRadius(5)
.backgroundColor(genre.color as string)
// 类型名
Text(genre.name).fontSize(13).fontColor('#333333')
.margin({ left: 8 }).width(64)
// 条形图(Stack 实现精准宽度)
Stack() {
Column().width('100%').height(14)
.backgroundColor('#F0F0F0').borderRadius(7)
Column()
.width(`${(genre.count / 12 * 100).toString()}%`)
.height(14).backgroundColor(genre.color as string)
.borderRadius(7).alignSelf(ItemAlign.Start)
}.layoutWeight(1)
Text(`${genre.count}款`)
.fontSize(11).fontColor('#999999').width(30)
.textAlign(TextAlign.End)
}.width('100%').margin({ top: 8 })
}, (genre: GenrePie) => genre.name)
}
// ...
}
实现原理:
- 用两层
Column叠加:底层灰色(#F0F0F0)作为背景槽,上层彩色作为填充 - 填充宽度 =
(count / 总游戏数12) * 100%,使用字符串模板拼接 alignSelf(ItemAlign.Start)确保条形从左开始填充
3.7 月度活跃柱状图
typescript
@Builder buildMonthlyActivity() {
const months: string[][] = [
['1月', '3', '在玩3款'], ['2月', '2', '通关1款'],
['3月', '1', '新购1款'], ['4月', '0', '放置中'],
['5月', '2', '回归2款'], ['6月', '1', '新购1款'],
['7月', '0', '出差'], ['8月', '1', '通关1款'],
['9月', '2', '开坑2款'], ['10月', '3', '活跃'],
['11月', '1', '摸鱼'], ['12月', '2', '年终冲量']
];
Column() {
Text('📅 月度活跃')
.fontSize(15).fontWeight(FontWeight.Bold).fontColor('#1A1A2E')
.width('100%')
Scroll() {
Row() {
ForEach(months, (m: string[]) => {
Column() {
Text(m[0]).fontSize(10).fontColor('#999999')
const h: number = parseInt(m[1]) * 18;
Column().width(20).height(h > 0 ? h : 8)
.backgroundColor(h > 0 ? '#FF6B35' : '#F0F0F0')
.borderRadius(4).margin({ top: 6 })
Text(m[1]).fontSize(10).fontColor('#FF6B35')
.fontWeight(FontWeight.Medium).margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
}, (m: string[]) => m[0])
}.padding({ left: 8, right: 8 })
}
.scrollable(ScrollDirection.Horizontal).height(100).margin({ top: 8 })
}
// ...
}
设计亮点:
- 柱高 =
月份活跃数 × 18px,线性映射 - 活跃度为 0 时,显示 8px 灰色矮柱(表示该月无活动)
- 活跃度 > 0 时,显示橙色柱,高度随数值变化
Scroll(ScrollDirection.Horizontal)支持横向滚动查看 12 个月
3.8 游戏之最
typescript
@Builder buildTopGame() {
Column() {
Text('🏅 游戏之最')
.fontSize(15).fontWeight(FontWeight.Bold).fontColor('#1A1A2E')
Row() {
Stack() {
Column().width(56).height(56).borderRadius(28)
.backgroundColor('#FFD700').justifyContent(FlexAlign.Center)
Text('🏆').fontSize(28)
}
Column() {
Text('时长最长的游戏').fontSize(11).fontColor('#999999')
Text(this.topGame).fontSize(16).fontWeight(FontWeight.Bold)
.fontColor('#1A1A2E').margin({ top: 2 })
Text(`${this.topHours}小时`).fontSize(13).fontColor('#FF6B35')
.margin({ top: 2 })
}.alignItems(HorizontalAlign.Start).margin({ left: 12 }).layoutWeight(1)
}.width('100%').margin({ top: 10 })
// 三个平均指标
Row() {
Column() {
Text('平均评分').fontSize(11).fontColor('#999999')
Text(this.avgRating.toString()).fontSize(20)
.fontWeight(FontWeight.Bold).fontColor('#FF6B35')
Text('/50').fontSize(11).fontColor('#999999')
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('平均完成度').fontSize(11).fontColor('#999999')
Text(`${this.avgCompletion}%`).fontSize(20)
.fontWeight(FontWeight.Bold).fontColor('#2ECC71')
Text('已通关游戏').fontSize(11).fontColor('#999999')
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('最爱平台').fontSize(11).fontColor('#999999')
Text('PC').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#3498DB')
Text('7款游戏').fontSize(11).fontColor('#999999')
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
}
.width('100%').margin({ top: 14 }).padding({ top: 12 })
.border({ width: { top: 1 }, color: '#F0F0F0' })
}
// ...
}
使用 border({ width: { top: 1 }, color: '#F0F0F0' }) 实现顶部分隔线------ArkTS 中 BorderOptions 支持按边设置。
3.9 时间分布排名
typescript
@Builder buildTimeDistribution() {
Column() {
Text('⏱️ 时间分布(按游戏)')
.fontSize(15).fontWeight(FontWeight.Bold).fontColor('#1A1A2E')
const timeData: string[][] = [
['巫师3', '210h', '21%', '#FF6B35'],
['艾尔登法环', '186h', '19%', '#FFD700'],
['荒野大镖客2', '185h', '19%', '#E74C3C'],
['赛博朋克2077', '120h', '12%', '#3498DB'],
['只狼', '98h', '10%', '#E74C3C'],
['塞尔达', '72h', '7%', '#2ECC71'],
['其他', '108h', '12%', '#95A5A6']
];
ForEach(timeData, (data: string[]) => {
Row() {
Text(data[0]).fontSize(13).fontColor('#333333').width(64)
Text(data[1]).fontSize(11).fontColor('#999999').width(36)
.textAlign(TextAlign.Center)
Stack() {
Column().width('100%').height(10).backgroundColor('#F0F0F0')
.borderRadius(5)
Column().width(data[2]).height(10)
.backgroundColor(data[3] as string).borderRadius(5)
.alignSelf(ItemAlign.Start)
}.layoutWeight(1)
Text(data[2]).fontSize(11).fontColor('#999999').width(32)
.textAlign(TextAlign.End)
}.width('100%').margin({ top: 6 })
}, (data: string[]) => data[0])
}
// ...
}
每条数据包含:游戏名(64px固定宽)、时长(36px)、条形图(弹性宽度)、占比(32px)。
3.10 年度对比
typescript
@Builder buildYearComparison() {
Column() {
Text('📈 年度对比')
.fontSize(15).fontWeight(FontWeight.Bold).fontColor('#1A1A2E')
Row() {
Column() {
Text('2024').fontSize(14).fontWeight(FontWeight.Bold)
.fontColor('#999999')
Text('8款').fontSize(18).fontWeight(FontWeight.Bold)
.fontColor('#333333').margin({ top: 4 })
Text('420小时').fontSize(11).fontColor('#999999').margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
.padding(12).backgroundColor('#F5F5F5').borderRadius(8)
.margin({ right: 6 })
Column() {
Text('2025').fontSize(14).fontWeight(FontWeight.Bold)
.fontColor('#FF6B35')
Text('4款').fontSize(18).fontWeight(FontWeight.Bold)
.fontColor('#FF6B35').margin({ top: 4 })
Text('180小时').fontSize(11).fontColor('#FF6B35').margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
.padding(12).backgroundColor('#FFF0E8').borderRadius(8)
.margin({ left: 6 })
}.width('100%').margin({ top: 10 })
}
// ...
}
通过背景色和文字颜色区分:2024 年为灰色低调,2025 年为主色调橙色高亮。
3.11 组装
typescript
build(): void {
Column() {
this.buildHeader()
Scroll() {
Column() {
this.buildStatGrid() // 统计概览
this.buildGenreStats() // 类型分布
this.buildMonthlyActivity() // 月度活跃
this.buildTopGame() // 游戏之最
this.buildTimeDistribution() // 时间分布
this.buildAchievementStats() // 成就进度
this.buildYearComparison() // 年度对比
}
.width('100%').padding({ bottom: 30 })
}
.scrollable(ScrollDirection.Vertical).layoutWeight(1).width('100%')
}
.width('100%').height('100%').backgroundColor('#F5F5F5')
}
四、从数据到视觉:ArkTS 图表绘制技术总结
本项目所有图表均使用纯 ArkTS 组件实现,无需第三方库:
| 图表类型 | 实现方式 | 核心组件 |
|---|---|---|
| 统计数字 | Text + layoutWeight 均分 | Column, Row |
| 水平条形图 | 双层 Column 叠加 | Stack, Column |
| 月度柱状图 | 动态高度 Column | Column, parseInt() |
| 进度条 | ArkTS 内置 Progress | Progress |
通用模式:
Stack {
Column().width('100%') // 灰色背景槽
Column().width('80%') // 彩色填充条
.alignSelf(ItemAlign.Start) // 左对齐
}

五、小结
本篇完成了最后两个页面:
愿望单页面:
- ✅ 优先级分类(高/中/低)可视化概览
- ✅ 排序切换(添加时间/优先级/发行日期)
- ✅ 愿望单卡片(含价格、优先级标签)
- ✅ 愿望单小贴士模块
统计中心页面:
- ✅ 统计概览网格(4 个核心指标)
- ✅ 类型分布水平条形图
- ✅ 月度活跃柱状图
- ✅ 游戏之最 + 平均指标
- ✅ 时间分布排名
- ✅ 成就进度统计
- ✅ 年度对比卡片
下篇将进行 全项目工程优化:路由架构、代码组织、测试实践与构建配置。
系列目录:
- 第一篇:项目搭建与首页开发
- 第二篇:游戏库列表与筛选排序
- 第三篇:游戏详情页与交互功能
- 第四篇:愿望单与个人统计(本文)
- 第五篇:路由导航与工程优化