HarmonyOS 6.0实战:音乐歌单我的页 —— 经验条与成就徽章墙的组合技
HarmonyOS 6.0实战:音乐歌单我的页 —— 经验条与成就徽章墙的组合技
App 13「音乐歌单」我的 Tab(ProfileTab),是个人中心页。整页用渐变用户卡片(Lv.4 等级 + 74% 经验条 + 签到/数据报告)+ 4 栏彩色统计 + 本周使用条形图(周六高亮)+ 6 徽章成就墙(4 解锁 2 未解锁)+ 5 项菜单(带 NEW 角标)+ About 信息组成,是 ArkUI "个人中心"页的完整范本。本篇基于
13-music-playlist/entry/src/main/ets/pages/ProfileTab.ets(约 225 行)逐段拆解,附 4 张实机截图。
一、整体结构:6 个 @Builder 拼接
ProfileTab 整个组件用 6 个 @Builder 块拼接成完整的个人中心页:
build() {
Column() {
Scroll() {
Column({ space: 0 }) {
this.UserCard()
this.StatRow()
this.WeekChartCard()
this.AchievementWall()
this.MenuList()
this.About()
}
.width('100%').padding({ bottom: D.pad + this.safeBottom + 20 })
}
.layoutWeight(1).scrollBar(BarState.Off).align(Alignment.Top)
}
.width('100%').height('100%').backgroundColor(C.bg)
}
6 个块按"用户 → 数据 → 周图 → 成就 → 菜单 → 关于"的叙事顺序展开,与首页 HomeTab 的"头部 → 工具 → 数据 → 筛选 → 列表"不同,ProfileTab 是"人 → 数据 → 行为 → 资产 → 设置 → 关于"的纵向叙事。
整页用 Scroll 容器(与 HomeTab 一致),因为内容超过一屏,必须可滚。space: 0 让 UserCard 和 StatRow 紧贴——这是有意为之,UserCard 渐变 + StatRow 卡片 + margin({ top: -28 }) 让 StatRow "上浮"到 UserCard 底部,形成"卡片叠在用户卡上"的视觉效果(Material Design 风格的 elevation)。
padding({ bottom: D.pad + this.safeBottom + 20 }) 给 Scroll 内容底部留出 16vp + 安全区 + 20vp 缓冲,避免"关于"信息贴到 Tab 栏底部。
UserCard 是整页唯一一个不滚出视口始终可见的卡片(虽然它在 Scroll 内但因为最顶部),让用户随时看到自己的等级和经验。


二、UserCard:渐变用户卡 + Lv.4 徽章 + 经验条 + 签到/数据报告
UserCard 是整页的"个人门面",4 个信息层叠在紫色渐变上:头像 + Lv.4 徽章、姓名 + 认证标签、活跃天数 + 距下一级经验、签到按钮 + 数据报告按钮:
@Builder
UserCard() {
Column({ space: 12 }) {
Row({ space: 14 }) {
Stack({ alignContent: Alignment.BottomEnd }) {
Row() { Text('👤').fontSize(34) }
.width(60).height(60).backgroundColor('rgba(255,255,255,0.35)').borderRadius(D.rLg).justifyContent(FlexAlign.Center)
.border({ width: 2, color: 'rgba(255,255,255,0.7)' })
Row() { Text('Lv.4').fontSize(9).fontColor('#FFFFFF').fontWeight(FontWeight.Bold) }
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
.backgroundColor(C.warn).borderRadius(4).border({ width: 1.5, color: '#FFFFFF' })
}
Column({ space: 5 }) {
Row({ space: 8 }) {
Text('小明同学').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
Text('认证').fontSize(9).fontColor('#FFFFFF')
.backgroundColor('rgba(255,255,255,0.3)').padding({ left: 5, right: 5, top: 1, bottom: 1 }).borderRadius(3)
}
Text('活跃用户 · 已使用 28 天').fontSize(11).fontColor('rgba(255,255,255,0.85)')
Row({ space: 4 }) {
Text('距 Lv.5 还需').fontSize(9).fontColor('rgba(255,255,255,0.7)')
Text('120').fontSize(11).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
Text('经验').fontSize(9).fontColor('rgba(255,255,255,0.7)')
}
Stack({ alignContent: Alignment.Start }) {
Column().width('100%').height(4).backgroundColor('rgba(255,255,255,0.25)').borderRadius(2)
Column().width('74%').height(4).backgroundColor('#FFFFFF').borderRadius(2)
}.width(140)
}.alignItems(HorizontalAlign.Start).layoutWeight(1)
}.width('100%').padding({ left: D.pad, right: D.pad, top: this.safeTop + 30, bottom: 16 })
Row() {
Row({ space: 5 }) {
Text('🎁').fontSize(11)
Text('每日签到').fontSize(11).fontColor('#FFFFFF')
}
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.backgroundColor('rgba(255,255,255,0.2)').borderRadius(D.rSm)
.onClick(() => { promptAction.showToast({ message: '签到成功 +5 经验' }); })
Blank()
Row({ space: 5 }) {
Text('📊').fontSize(11)
Text('数据报告').fontSize(11).fontColor('#FFFFFF')
}
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.backgroundColor('rgba(255,255,255,0.2)').borderRadius(D.rSm)
.onClick(() => { promptAction.showToast({ message: '数据报告' }); })
}.width('100%').padding({ left: D.pad, right: D.pad, bottom: 14 })
}
.width('100%').linearGradient({ angle: 135, colors: [[C.primary, 0.0], [C.accent, 1.0]] })
}
2.1 Stack 双层:头像 + Lv.4 徽章
Stack({ alignContent: Alignment.BottomEnd }) 让 Lv.4 标签"贴"在头像右下角:
- 底层:60×60 头像块,半透明白色背景 (
rgba(255,255,255,0.35)) + 2vp 白色描边 - 上层:Lv.4 标签,橙底 (
C.warn) + 白字 + 1.5vp 白边
Stack 的 alignContent(Alignment.BottomEnd) 让上层对齐"右+下",正好把 Lv.4 标签定位到头像的右下角,形成"角色等级徽章"的经典样式(游戏 App 常用)。
2.2 经验值 + 进度条
Stack({ alignContent: Alignment.Start }) {
Column().width('100%').height(4).backgroundColor('rgba(255,255,255,0.25)').borderRadius(2)
Column().width('74%').height(4).backgroundColor('#FFFFFF').borderRadius(2)
}.width(140)
Stack 进度条 = 背景 + 前景叠在同一位置:
- 背景:100% 宽 4vp,半透明白色 (
rgba(255,255,255,0.25)) - 前景:74% 宽 4vp,纯白 (
#FFFFFF) - 外层
.width(140)限制进度条最大宽度 140vp
74% 表示"当前经验占 Lv.5 所需总经验的比例",120 经验 是"还差 120 经验升 Lv.5"。这两个数字(74% / 120)一起描述"成长进度",比单一百分比更清晰。
2.3 签到 + 数据报告双按钮
Row() 内左右各一个"伪按钮"(背景 + 文字 + emoji):
- 每日签到(左):
🎁+ 文字,半透明白底 - 数据报告(右):
📊+ 文字,半透明白底
中间 Blank() 撑开。两者都点击弹 Toast——"签到成功 +5 经验" 和 "数据报告"。"签到 +5 经验"是游戏化机制:用户每次签到都获得"经验值"反馈,激励每天打开 App。这是社区留存的核心机制。
2.4 紫色渐变
.linearGradient({ angle: 135, colors: [[C.primary, 0.0], [C.accent, 1.0]] })
与首页 HeroCard 同样的 135° 渐变(C.primary → C.accent)。UserCard 在页面最顶部,紫色延伸至状态栏背后(padding.top: this.safeTop + 30 让出安全区),形成顶部"沉浸式紫色块"。
三、StatRow:4 栏彩色统计数字
StatRow 是 4 个数字(历史订单 28 / 进行中 3 / 满意度 98% / 连续天数 15)横排,每个数字有自己的颜色:
@Builder
StatRow() {
Row() {
ForEach(this.stats, (s: StatItem, idx: number) => {
Column({ space: 3 }) {
Text(s.value).fontSize(18).fontWeight(FontWeight.Bold).fontColor(s.color)
Text(s.label).fontSize(10).fontColor(C.textDim)
}.layoutWeight(1)
if (idx < this.stats.length - 1) { Divider().vertical(true).height(32).color(C.stroke) }
}, (s: StatItem) => s.label)
}
.width('92%').height(76).backgroundColor(C.card).borderRadius(D.rMd)
.border({ width: 1, color: C.stroke })
.justifyContent(FlexAlign.SpaceAround).alignItems(VerticalAlign.Center).margin({ top: -28 })
}
4 色映射:
- 历史订单 28 →
C.primary紫 - 进行中 3 →
C.accent浅紫 - 满意度 98% →
C.ok绿 - 连续天数 15 →
C.warn橙
width('92%'):比父容器窄 8%,让卡片左右两侧各留 4% 空间——这是让 StatRow "缩进" 卡片内层,视觉上"嵌在 UserCard 下"。配合 margin({ top: -28 }),整张 StatRow 上浮 28vp,与 UserCard 底部重叠 28vp,形成"卡片叠在用户卡上"的悬浮效果。
alignItems(VerticalAlign.Center):让 4 个数字在 76vp 高度内垂直居中,比"靠顶"更平衡。
Divider().vertical(true).height(32) 是竖向分隔线,每两个数字之间用 1vp 浅灰竖线分隔。if (idx < this.stats.length - 1) 防止最后一条也有分隔线。
注意:Divider().vertical(true) 的语法是 ArkUI 14+ 才支持的,垂直分隔线比手动 Column().width(1).height(32).backgroundColor(C.stroke) 更简洁。
四、WeekChartCard:本周使用条形图(周六高亮)
WeekChartCard 是"本周使用"的可视化条形图,与首页 WeekChartCard 类似但默认高亮周六(idx=5):
@Builder
WeekChartCard() {
Column({ space: 12 }) {
Row() {
Text('📈 本周使用').fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text)
Blank()
Text('20 次').fontSize(13).fontWeight(FontWeight.Bold).fontColor(C.primary)
}.width('100%')
Row({ space: 4 }) {
ForEach(this.weekBars, (b: WeekBar, idx: number) => {
Column({ space: 6 }) {
Column()
.width(18)
.height(Math.max(6, b.value / 5 * 80))
.borderRadius(4)
.linearGradient({
angle: 180,
colors: idx === 5 ? [[C.primary, 0.0], [C.accent, 1.0]] : [[C.primarySoft, 0.0], [C.primary, 1.0]]
})
Text(b.day).fontSize(10).fontColor(idx === 5 ? C.primary : C.textDim)
.fontWeight(idx === 5 ? FontWeight.Bold : FontWeight.Normal)
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
}, (b: WeekBar) => b.day)
}.width('100%')
}
.width('100%').padding(14).backgroundColor(C.card).borderRadius(D.rMd)
.border({ width: 1, color: C.stroke })
.margin({ top: 14, left: D.pad, right: D.pad })
}
与首页 WeekChartCard 的差异:
| 差异点 | 首页 HomeTab | 我的 ProfileTab |
|---|---|---|
| 标题 | 📊 本周数据 | 📈 本周使用 |
| 总数 | 30 次 | 20 次 |
| 数据 | 3,5,2,6,4,7,3 | 2,3,1,4,3,5,2 |
| 最高柱(默认高亮) | idx=4("五",value=4) | idx=5("六",value=5) |
| 高度计算 | b.value / 7 * 80 | b.value / 5 * 80 |
| 可点击 | 是(onClick 切换高亮) | 否(静态) |
b.value / 5 * 80 vs b.value / 7 * 80:我的页最大值是 5(周六),所以分母用 5,让周六柱顶到 80vp 满高。首页最大值是 7,分母用 7。两页都按"当前数据最大值"做归一化,让最高柱永远是 80vp,给视觉一致感。
周六高亮的隐喻:周六是周末/休息日,用户有更多时间使用 App,所以"本周使用峰值在周六"符合用户心理预期,让图表"讲一个用户能理解的故事"——这是数据可视化的高阶原则。
不可点击的差异:首页的 WeekChartCard 是"可交互"(点击切高亮),我的页的 WeekChartCard 是"纯展示"。差异源于"用户角色"——首页是"看数据"(想探索),我的页是"看自己"(确认状态)。同一组件在不同上下文用不同交互模式,是 ArkUI 声明式 UI 的灵活性体现。
五、AchievementWall:6 徽章成就墙
AchievementWall 是 6 个徽章横向排列的"成就墙",4 个解锁 + 2 个未解锁:
@Builder
AchievementWall() {
Column({ space: 12 }) {
Row() {
Text('🏅 成就徽章').fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text)
Blank()
Text(`${this.achievements.filter(a => a.unlocked).length}/${this.achievements.length}`).fontSize(11).fontColor(C.primary)
}.width('100%')
Row() {
ForEach(this.achievements, (item: Achievement) => {
Column({ space: 6 }) {
Row() { Text(item.icon).fontSize(24) }
.width(48).height(48).borderRadius(D.rMd).justifyContent(FlexAlign.Center)
.backgroundColor(item.unlocked ? C.primarySoft : C.cardSoft)
.opacity(item.unlocked ? 1 : 0.4)
Text(item.title).fontSize(9).fontColor(item.unlocked ? C.text : C.textDim)
Text(item.desc).fontSize(7).fontColor(C.textDim).maxLines(1)
}.layoutWeight(1)
}, (item: Achievement) => item.title)
}.width('100%')
}
.width('100%').padding(14).backgroundColor(C.card).borderRadius(D.rMd)
.border({ width: 1, color: C.stroke })
.margin({ top: 14, left: D.pad, right: D.pad })
}
"解锁/未解锁"双态实现 3 件套:
- 图标块背景:
unlocked ? C.primarySoft : C.cardSoft— 解锁浅紫,未解浅灰 - 整体透明度:
opacity(unlocked ? 1 : 0.4)— 解锁 100%,未解 40%(关键!让"未解锁"有"灰蒙"感) - 文字色:
unlocked ? C.text : C.textDim— 解锁深色,未解灰色
opacity: 0.4 是关键的"未解锁"标记——比单纯换背景色更直接传达"这个还没拿到"。
6 个徽章的数据编排:
- 🎯 初次使用(已解锁,第一次下单)
- 🔥 活跃用户(已解锁,使用 7 天)
- ⭐ 好评达人(已解锁,5 星好评 10 次)
- 💰 省钱能手(已解锁,节省 100 元)
- 👑 资深用户(未解锁,使用 100 天)
- 💎 完美用户(未解锁,满意度 100%)
已解锁的 4 个都是"初级成就"(短期可达),未解锁的 2 个都是"高级成就"(长期挑战)。这种"近易远难"的成就编排是游戏化设计的关键——让用户一开始就能拿到几个徽章建立成就感,同时有"高级徽章"作为长期目标。
this.achievements.filter(a => a.unlocked).length 动态计数:右上角 "4/6" 通过 .filter() 实时计算已解锁数,数据更新时自动重渲染(因为 .filter 返回新数组,每次都重新求值)。
但这里没 @State achievements,achievements 是 private 不可变数据,所以 .filter() 只在初始化时算一次。如果未来要"解锁新成就后数字自动 +1",需要把 achievements 改为 @State,filter 才会响应式重算。
Text(item.desc).maxLines(1):描述文字("第一次下单")限制 1 行,超出省略号。maxLines(1) 是 ArkUI 的"行数限制"属性,避免长描述撑破卡片布局。

六、MenuList:5 项菜单(收藏带 NEW 角标)
MenuList 是 5 个菜单项的纵向列表,收藏项右上角带"NEW"绿色角标:
@Builder
MenuList() {
Column() {
ForEach(this.menus, (m: MenuItem, idx: number) => {
Column() {
Row({ space: 12 }) {
Row() { Text(m.emoji).fontSize(20) }
.width(36).height(36).backgroundColor(C.cardSoft).borderRadius(D.rSm).justifyContent(FlexAlign.Center)
Column({ space: 2 }) {
Text(m.title).fontSize(14).fontWeight(FontWeight.Medium).fontColor(C.text)
Text(m.desc).fontSize(11).fontColor(C.textDim)
}.alignItems(HorizontalAlign.Start).layoutWeight(1)
if (m.badge.length > 0) {
Text(m.badge).fontSize(9).fontColor('#FFFFFF')
.backgroundColor(m.badge === 'NEW' ? C.ok : C.danger)
.padding({ left: 5, right: 5, top: 1, bottom: 1 }).borderRadius(D.rSm)
}
Text('›').fontSize(22).fontColor(C.textDim)
}
.width('100%').padding(14)
.onClick(() => { promptAction.showToast({ message: m.title }); })
if (idx < this.menus.length - 1) { Divider().color(C.stroke).margin({ left: 58 }) }
}
}, (m: MenuItem) => m.title)
}
.width('100%').backgroundColor(C.card).borderRadius(D.rMd).border({ width: 1, color: C.stroke })
.margin({ top: 14, left: D.pad, right: D.pad }).clip(true)
}
几个细节:
第一,if (m.badge.length > 0) 条件渲染。m.badge 字段可能是空字符串 '' 或 'NEW',长度 > 0 时才显示角标。这种"空字符串作为'无角标'的占位"避免引入额外的 boolean 字段。
第二,badge === 'NEW' ? C.ok : C.danger:未来如果增加其他 badge(如 'HOT'),可设为红 (C.danger)。当前只有 'NEW' 一种,所以都是绿色。
第三,Divider().color(C.stroke).margin({ left: 58 }):每两个菜单项之间用 1vp 浅灰线分隔。margin.left: 58 让分隔线不贯穿整个卡片宽度,而是从"图标块+文字"的右侧开始——这是 iOS 设置项的经典样式(分隔线只覆盖"右侧部分",视觉上更精致)。
第四,.clip(true):让整张菜单卡的圆角 (D.rMd) 对内部 Divider 也生效——如果 Divider 比父容器宽,会被 clip 裁剪到圆角内。这是 ArkUI 里"父容器圆角 + 子内容裁剪"的标准做法。
5 个菜单项的编排:历史订单 / 消费统计 / 提醒设置 / 我的收藏 (NEW) / 设置——"按用户使用频次从高到低排序",收藏放第 4 位因为有 NEW 引导(用户最容易忽略的"次重要"功能靠角标引流)。

七、About:版本号 + 技术栈
@Builder
About() {
Column({ space: 4 }) {
Text('Version 1.0.0').fontSize(12).fontColor(C.textDim)
Text('HarmonyOS 6.0+ · ArkTS · API 23').fontSize(10).fontColor(C.textDim)
}
.width('100%').padding({ top: 24, bottom: 10 }).alignItems(HorizontalAlign.Center)
}
Version 1.0.0 和 HarmonyOS 6.0+ · ArkTS · API 23 是底部"关于"信息,10-12sp 灰色居中。
padding({ top: 24 }) 让 About 与上方 MenuList 拉开 24vp 间距,**"段间空隙大于段内空隙"**的视觉节奏(段间 24vp > 卡片间距 14vp)。alignItems(HorizontalAlign.Center) 让两行文字水平居中。

八、@State 的克制
整页只有 0 个 @State——所有数据都是 private 字段(stats/menus/achievements/weekBars),且本页没有任何交互(WeekChartCard 不可点击、MenuList 点击只弹 Toast、AchievementWall 不可交互)。
@State 数量 = 0 反映了一个事实:个人中心页是"展示型"页面,主要功能是"展示用户信息",而非"接收用户操作"。这与 Func1Tab(5 个 @State 表单输入)和 Func2Tab(2 个 @State 筛选/搜索)形成对比。
如果未来加交互(如"切换主题色"开关、"签到按钮"更新经验值),就新增 @State。"先看是否需要交互,再决定是否加 @State" 是健康的状态管理习惯。
九、设计哲学:紫色品牌 + 个人身份
整页用紫色 (#8B5CF6) 主导:
- UserCard 渐变紫底
- StatRow 紫色数字(历史订单)
- WeekChartCard 紫色渐变柱子
- AchievementWall 浅紫底(已解锁徽章)
- MenuList NEW 角标绿色(与紫色互补)
这种"主色 + 1 个辅色"的配色比"彩虹配色"高级得多——让整页"气质统一",不花哨。
个人中心的 4 个内容层:
- 身份层(UserCard)— 我是谁、我的等级、我的活跃度
- 数据层(StatRow / WeekChartCard)— 我的使用数据
- 成就层(AchievementWall)— 我达成过什么
- 资产层(MenuList)— 我能做什么
4 层结构把"自我认知(身份)→ 行为回顾(数据)→ 行为激励(成就)→ 行动入口(资产)"的闭环讲清楚。这是个人中心页的通用骨架,适用于任何"个人主页"场景(音乐 App、阅读 App、运动 App、社交 App)。
十、可访问性与扩展
可访问性上:
- 进度条配 "74%" 文字:色弱用户不靠颜色识别
- 徽章配"已解锁/未解锁"opacity + 文字色:色弱用户能靠"颜色深浅"识别
- 菜单项有"标题 + 副标题":屏幕阅读器能读出完整信息
扩展方向:
- 签到按钮可以加
@State checked: boolean+@State exp: number,点击翻转 checked 并 exp + 5 - 主题切换可以加
@State isDark: boolean,影响 Theme.ets - 成就解锁可以监听
exp变化,达到阈值时把achievements[i].unlocked = true - 菜单项点击应跳到对应页面(订单页 / 统计页 / 设置页),而非弹 Toast
更多推荐


所有评论(0)