鸿蒙原生应用实战(五):个人中心与数据可视化 —— 统计图表与成就徽章
·
鸿蒙原生应用实战(五):个人中心与数据可视化 —— 统计图表与成就徽章
前言
前四篇完成了首页、发现页、专辑详情页和歌单管理页。本篇作为系列的完结篇,将实现最后一个页面——个人中心(ProfilePage)。
个人中心是App的"数据仪表盘",核心功能:
- 统计概览 —— 收藏歌曲数、创建歌单数、累计收听时长、听歌天数
- 音乐口味分析 —— 各类型占比的水平条状图
- 成就徽章 —— 已解锁/未解锁的成就系统
- 最近播放 —— 历史播放记录
- 功能菜单 —— 夜间模式、年度报告等入口
同时,本篇还将总结整个项目的构建经验与最佳实践。
一、页面功能分析
ProfilePage
├── 顶部:返回 + 标题"个人中心" + 设置按钮
├── 用户信息:头像 + 昵称 + 加入天数 + 编辑按钮
├── 统计Grid:4个统计指标的2×2网格
├── 音乐口味:带进度条的类型占比(流行35%、民谣22%...)
├── Tab切换:统计概览 | 成就徽章 | 最近播放
├── 成就徽章:3列Grid展示6个徽章(已解锁/未解锁)
├── 最近播放:6条历史记录
└── 功能菜单:夜间模式 / 年度报告 / 数据管理 / 帮助反馈
1.1 数据结构
interface StatCard {
label: string; // 标签
value: string; // 数值
icon: string; // Emoji图标
color: string; // 数值颜色
}
interface GenrePref {
name: string; // 类型名
percent: number; // 百分比 0-100
color: string; // 颜色
count: number; // 歌曲数
}
interface Achievement {
icon: string; // 图标
title: string; // 成就名
desc: string; // 描述
unlocked: boolean;// 是否解锁
progress: string; // 进度文本
}
interface RecentPlay {
id: number;
title: string;
artist: string;
date: string; // 播放时间
duration: string;
}
二、状态变量与Tab管理
2.1 状态声明
@Component
struct ProfilePage {
@State statCards: StatCard[] = [];
@State genrePrefs: GenrePref[] = [];
@State achievements: Achievement[] = [];
@State recentPlays: RecentPlay[] = [];
@State currentTab: number = 0;
tabs: string[] = ['统计概览', '成就徽章', '最近播放'];
}
2.2 数据初始化
initStats(): void {
this.statCards = [
{ label: '收藏歌曲', value: '46', icon: '❤️', color: '#EF4444' },
{ label: '创建歌单', value: '4', icon: '📋', color: '#7C3AED' },
{ label: '累计收听', value: '128', icon: '⏱️', color: '#3B82F6' },
{ label: '听歌天数', value: '89', icon: '📅', color: '#10B981' }
];
}
initGenres(): void {
this.genrePrefs = [
{ name: '流行', percent: 35, color: '#FF6B6B', count: 16 },
{ name: '民谣', percent: 22, color: '#45B7D1', count: 10 },
{ name: '摇滚', percent: 17, color: '#96CEB4', count: 8 },
{ name: 'R&B', percent: 13, color: '#DDA0DD', count: 6 },
{ name: '其他', percent: 13, color: '#F0E68C', count: 6 }
];
}
initAchievements(): void {
this.achievements = [
{ icon: '🎧', title: '初级乐迷', desc: '累计收听超过50小时', unlocked: true, progress: '已完成' },
{ icon: '🎯', title: '歌曲收藏家', desc: '收藏超过30首歌曲', unlocked: true, progress: '已完成' },
{ icon: '📀', title: '专辑达人', desc: '完整听完10张专辑', unlocked: true, progress: '已完成' },
{ icon: '🏆', title: '资深乐迷', desc: '累计收听超过200小时', unlocked: false, progress: '128/200h' },
{ icon: '🔥', title: '连续打卡', desc: '连续听歌30天', unlocked: false, progress: '12/30天' },
{ icon: '💎', title: '全领域', desc: '收藏8种类型的歌曲', unlocked: false, progress: '5/8' }
];
}
三、用户信息头部
@Builder buildHeader() {
Column() {
// 顶栏
Row() {
Text('←').fontSize(20).fontColor('#1F1B2E')
.onClick(() => { router.back(); })
Blank()
Text('个人中心').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#1F1B2E')
Blank()
Text('设置').fontSize(14).fontColor('#7C3AED')
}
.width('100%').padding({ left: 20, right: 20, top: 12, bottom: 8 })
// 用户信息
Row() {
Stack() { // 圆形用户头像
Column().width(64).height(64).borderRadius(32)
.backgroundColor('#EDE9FE')
Text('🎵').fontSize(32)
}
Column() {
Text('乐迷').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#1F1B2E')
Row() {
Text('🎧').fontSize(12)
Text('已加入 30 天').fontSize(12).fontColor('#6B7280').margin({ left: 4 })
}.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start).margin({ left: 16 }).layoutWeight(1)
Text('编辑').fontSize(12).fontColor('#7C3AED')
.padding({ left: 14, right: 14, top: 6, bottom: 6 })
.backgroundColor('#EDE9FE').borderRadius(16)
}
.width('100%').padding({ left: 20, right: 20, top: 8, bottom: 16 })
}
.width('100%').backgroundColor('#FFFFFF')
}
四、统计Grid
@Builder buildStatCards() {
Grid() {
ForEach(this.statCards, (card: StatCard) => {
GridItem() {
Column() {
Text(card.icon).fontSize(28)
Text(card.value).fontSize(22).fontWeight(FontWeight.Bold)
.fontColor(card.color).margin({ top: 6 })
Text(card.label).fontSize(11).fontColor('#6B7280').margin({ top: 2 })
}
.width('100%').justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.padding({ top: 16, bottom: 16 })
.backgroundColor('#FFFFFF').borderRadius(12)
}
}, (card: StatCard) => card.label)
}
.columnsTemplate('1fr 1fr') // 2列等宽
.columnsGap(12)
.rowsGap(12)
.width('100%').padding({ left: 20, right: 20, top: 12 })
}
2×2 Grid:
❤️ 46 📋 4
收藏歌曲 创建歌单
⏱️ 128 📅 89
累计收听 听歌天数
每个格子包含:Emoji图标 + 大号彩色数值 + 灰色标签。
五、音乐口味水平条状图
@Builder buildGenrePrefChart() {
Column() {
Text('我的音乐口味').fontSize(16).fontWeight(FontWeight.Bold)
.fontColor('#1F1B2E').width('100%').padding({ left: 20 })
Column() {
ForEach(this.genrePrefs, (genre: GenrePref) => {
Row() {
// 圆点图例
Column().width(10).height(10).borderRadius(5)
.backgroundColor(genre.color)
Text(genre.name).fontSize(13).fontColor('#1F1B2E')
.margin({ left: 8 }).width(40)
// 进度条(Stack两层叠放)
Stack() {
Column().width('100%').height(16)
.backgroundColor('#F3F0FF').borderRadius(8) // 背景条
Column().width(`${genre.percent}%`).height(16)
.backgroundColor(genre.color).borderRadius(8) // 前景条
.alignSelf(ItemAlign.Start) // ← 从左填充
}
.layoutWeight(1).margin({ left: 8, right: 8 })
Text(`${genre.percent}%`).fontSize(12)
.fontColor('#6B7280').width(36).textAlign(TextAlign.End)
}
.width('100%').margin({ top: 8 })
}, (genre: GenrePref) => genre.name)
}
.width('100%').padding({ left: 20, right: 20 })
}
.width('100%').padding({ top: 20, bottom: 20 })
.backgroundColor('#FFFFFF').borderRadius(12)
.margin({ top: 12, left: 20, right: 20 })
.alignItems(HorizontalAlign.Start)
}
水平条状图的实现原理(再次强调):
Stack (100%宽度)
├── Column(100%) ← 背景轨道(浅色)
└── Column(percent%) ← 填充条(彩色)
└── alignSelf(ItemAlign.Start) → 从左开始填充
每一行结构:🔵 图例 | 类型名(40px) | 进度条(layoutWeight) | 百分比(36px)
六、三个Tab的设计
6.1 Tab切换栏
Row() {
ForEach(this.tabs, (tab: string, index?: number) => {
Column() {
Text(tab).fontSize(14)
.fontColor(this.currentTab === (index as number) ? '#7C3AED' : '#9CA3AF')
.fontWeight(this.currentTab === (index as number) ? FontWeight.Medium : FontWeight.Normal)
// 下划线指示器
Column().width('60%').height(2)
.backgroundColor(this.currentTab === (index as number) ? '#7C3AED' : 'transparent')
.borderRadius(1).margin({ top: 6 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
.onClick(() => { this.currentTab = index as number; })
}, (tab: string) => tab)
}
.width('100%').padding({ top: 20, left: 20, right: 20 })
6.2 内容切换
if (this.currentTab === 0) {
// 统计概览——已在上面展示,不需要重复展示
} else if (this.currentTab === 1) {
this.buildAchievements()
} else {
this.buildRecentPlays()
}
七、成就徽章系统
@Builder buildAchievements() {
Column() {
Text('成就徽章').fontSize(16).fontWeight(FontWeight.Bold)
.fontColor('#1F1B2E').width('100%')
Grid() {
ForEach(this.achievements, (badge: Achievement) => {
GridItem() {
Column() {
Text(badge.icon).fontSize(32)
.grayscale(badge.unlocked ? 0 : 1) // 未解锁变灰
.opacity(badge.unlocked ? 1 : 0.5) // 未解锁半透明
Text(badge.title).fontSize(12).fontColor('#1F1B2E')
.fontWeight(FontWeight.Medium).margin({ top: 6 })
Text(badge.desc).fontSize(9).fontColor('#9CA3AF')
.margin({ top: 2 }).maxLines(1)
Text(badge.progress).fontSize(9)
.fontColor(badge.unlocked ? '#10B981' : '#F59E0B')
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
.backgroundColor(badge.unlocked ? '#ECFDF5' : '#FFFBEB')
.borderRadius(8).margin({ top: 4 })
}
.width('100%').justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.padding({ top: 16, bottom: 12 })
.backgroundColor('#FFFFFF').borderRadius(12)
}
}, (badge: Achievement) => badge.title)
}
.columnsTemplate('1fr 1fr 1fr') // 3列等宽
.columnsGap(10).rowsGap(10)
.width('100%').padding({ left: 20, right: 20, top: 12 })
}
.width('100%').padding({ top: 20 })
}
已解锁 vs 未解锁的视觉差异:
| 元素 | 已解锁 | 未解锁 |
|---|---|---|
| 图标 | 彩色、不透明 | 灰度、半透明 |
| 标题 | 黑色 #1F1B2E | 黑色(不变) |
| 进度标签 | 绿色 (#10B981) + 浅绿背景 | 琥珀色 (#F59E0B) + 浅黄背景 |
| 文字 | “已完成” | “128/200h”(进度) |
grayscale 滤镜:设为1时图标完全变灰,设为0时恢复原始颜色,是实现解锁/锁定状态的最简单方式。
6个成就徽章设计:
| 图标 | 名称 | 解锁条件 | 初始状态 |
|---|---|---|---|
| 🎧 | 初级乐迷 | 收听超过50小时 | ✅ 已解锁 |
| 🎯 | 歌曲收藏家 | 收藏超过30首 | ✅ 已解锁 |
| 📀 | 专辑达人 | 听完10张专辑 | ✅ 已解锁 |
| 🏆 | 资深乐迷 | 收听超过200小时 | ❌ 128/200h |
| 🔥 | 连续打卡 | 连续听歌30天 | ❌ 12/30天 |
| 💎 | 全领域 | 收藏8种类型 | ❌ 5/8 |
八、最近播放与功能菜单
8.1 最近播放
@Builder buildRecentPlays() {
Column() {
Text('最近播放').fontSize(16).fontWeight(FontWeight.Bold)
.fontColor('#1F1B2E').width('100%').padding({ left: 20 })
Column() {
ForEach(this.recentPlays, (item: RecentPlay) => {
Row() {
Stack() {
Column().width(40).height(40).backgroundColor('#EDE9FE').borderRadius(8)
Text('🎵').fontSize(18)
}
Column() {
Text(item.title).fontSize(14).fontColor('#1F1B2E')
Text(`${item.artist} · ${item.date.slice(5)}`) // 只显示 MM-DD HH:mm
.fontSize(11).fontColor('#9CA3AF').margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).margin({ left: 10 })
Text(item.duration).fontSize(11).fontColor('#9CA3AF')
}
.width('100%').padding({ left: 20, right: 20, top: 8, bottom: 8 })
.backgroundColor('#FFFFFF').borderRadius(8).margin({ top: 4 })
}, (item: RecentPlay) => item.id.toString())
}.width('100%').padding({ left: 20, right: 20 })
}.width('100%').padding({ top: 12 })
}
date.slice(5) 从 "2025-01-15 22:30" 截取为 "01-15 22:30",去掉年份以节省空间。
8.2 功能菜单
@Builder buildMenuList() {
Column() {
Text('更多功能').fontSize(16).fontWeight(FontWeight.Bold)
.fontColor('#1F1B2E').width('100%').padding({ left: 20 })
Column() {
ForEach([
{ icon: '🌙', title: '夜间模式', desc: '切换深色主题' },
{ icon: '📊', title: '年度报告', desc: '查看你的音乐年度总结' },
{ icon: '🗂️', title: '数据管理', desc: '导入导出你的歌单数据' },
{ icon: '❓', title: '帮助与反馈', desc: '联系我们' }
], (item) => {
Row() {
Text(item.icon).fontSize(22)
Column() {
Text(item.title).fontSize(14).fontColor('#1F1B2E')
Text(item.desc).fontSize(11).fontColor('#9CA3AF').margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start).margin({ left: 12 }).layoutWeight(1)
Text('>').fontSize(16).fontColor('#D1D5DB') // 右箭头指示
}
.width('100%').padding({ left: 20, right: 20, top: 12, bottom: 12 })
.backgroundColor('#FFFFFF').borderRadius(8).margin({ top: 6 })
}, (item) => item.title)
}.width('100%').padding({ left: 20, right: 20 })
}.width('100%').padding({ top: 20 })
}
每个菜单项包含:Emoji图标 + 标题 + 描述 + 右箭头(>)指示可点击。
九、全项目总结
9.1 五篇回顾
| 篇次 | 页面 | 核心组件 | 知识点 |
|---|---|---|---|
| 一 | 项目初始化 | - | Stage模型、路由注册、资源配置 |
| 二 | 首页 | Grid、Scroll、Row | Grid 2×4网格、横向滚动、排行榜 |
| 三 | 发现页 + 专辑详情 | TextInput、Wrap、Stack | 三维筛选、Wrap标签、曲目收藏 |
| 四 | 歌单管理 | Modal、Stack | 弹窗创建、展开/收起、数组删除 |
| 五 | 个人中心 | Grid、Stack | 统计Grid、条状图、徽章系统 |
9.2 五个页面数据量
| 页面 | 数据类型 | 数据量 |
|---|---|---|
| Index | 8分类 + 6专辑 + 8歌曲 | 22条 |
| SearchPage | 30首歌曲 | 30条 |
| AlbumPage | 6专辑 × 10曲目 | 60条 |
| PlaylistPage | 4歌单 × 5歌曲 | 20条 |
| ProfilePage | 4统计 + 5类型 + 6徽章 + 6最近 | 21条 |
9.3 ArkTS严格模式经验总结
- 接口先行:每个页面顶部定义interface,对象字面量需要类型推断
- null安全:联合类型(
AlbumDetail | null)使用前必须判空 - 类型断言:
as Record<string, Object>、as number - 数组重建:修改数组元素后重新赋值触发UI刷新
- key唯一:ForEach的第三个参数提供稳定key
9.4 可扩展方向
- 网络层:使用
@ohos.net.http替换模拟数据 - 持久化:使用
@ohos.data.preferences保存歌单和收藏数据 - 播放器:集成
@ohos.multimedia.media实现音乐播放 - 搜索接口:对接第三方音乐API获取真实数据
- 深色模式:利用resources/dark/配置深色主题资源
- 数据同步:利用分布式数据管理实现多设备同步

写在最后
五篇连载到此结束。「乐迷笔记」这个从零开始的音乐App涵盖了鸿蒙原生开发的核心场景:
- 布局:Column/Row/Stack/Grid/Scroll/List 全系列
- 交互:点击/滑动/弹窗/展开收起/筛选
- 状态:@State装饰器、数据驱动UI
- 路由:Router传参、页面跳转、返回
- 资源:颜色/尺寸/字符串集中管理
- 构建:hvigor配置、严格模式、编译优化
动手实践是最好的学习方式——打开DevEco Studio,从仿写一个页面开始你的鸿蒙之旅吧!
应用名称: 乐迷笔记
SDK: API 23 (HarmonyOS 6.1.0)
框架: Stage模型 + ArkTS全系列索引:
- (一)项目初始化与Stage模型架构设计
- (二)首页开发 —— Grid分类网格与热歌排行榜
- (三)发现页与专辑详情 —— 多维筛选与曲目管理
- (四)歌单管理 —— 创建歌单与歌曲编排
- (五)个人中心与数据可视化 —— 统计图表与成就徽章 ← 当前
更多推荐


所有评论(0)