鸿蒙原生应用实战(五):个人中心与数据可视化 —— 统计图表与成就徽章

前言

前四篇完成了首页、发现页、专辑详情页和歌单管理页。本篇作为系列的完结篇,将实现最后一个页面——个人中心(ProfilePage)

个人中心是App的"数据仪表盘",核心功能:

  1. 统计概览 —— 收藏歌曲数、创建歌单数、累计收听时长、听歌天数
  2. 音乐口味分析 —— 各类型占比的水平条状图
  3. 成就徽章 —— 已解锁/未解锁的成就系统
  4. 最近播放 —— 历史播放记录
  5. 功能菜单 —— 夜间模式、年度报告等入口

同时,本篇还将总结整个项目的构建经验与最佳实践。


一、页面功能分析

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严格模式经验总结

  1. 接口先行:每个页面顶部定义interface,对象字面量需要类型推断
  2. null安全:联合类型(AlbumDetail | null)使用前必须判空
  3. 类型断言as Record<string, Object>as number
  4. 数组重建:修改数组元素后重新赋值触发UI刷新
  5. key唯一:ForEach的第三个参数提供稳定key

9.4 可扩展方向

  1. 网络层:使用 @ohos.net.http 替换模拟数据
  2. 持久化:使用 @ohos.data.preferences 保存歌单和收藏数据
  3. 播放器:集成 @ohos.multimedia.media 实现音乐播放
  4. 搜索接口:对接第三方音乐API获取真实数据
  5. 深色模式:利用resources/dark/配置深色主题资源
  6. 数据同步:利用分布式数据管理实现多设备同步

在这里插入图片描述

写在最后

五篇连载到此结束。「乐迷笔记」这个从零开始的音乐App涵盖了鸿蒙原生开发的核心场景:

  • 布局:Column/Row/Stack/Grid/Scroll/List 全系列
  • 交互:点击/滑动/弹窗/展开收起/筛选
  • 状态:@State装饰器、数据驱动UI
  • 路由:Router传参、页面跳转、返回
  • 资源:颜色/尺寸/字符串集中管理
  • 构建:hvigor配置、严格模式、编译优化

动手实践是最好的学习方式——打开DevEco Studio,从仿写一个页面开始你的鸿蒙之旅吧!


应用名称: 乐迷笔记
SDK: API 23 (HarmonyOS 6.1.0)
框架: Stage模型 + ArkTS

全系列索引:

Logo

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

更多推荐