HarmonyOS 校园应用系列之HarmonyOS ArkTS实战:AI 单词我的页 —— VIP 徽章与等级进度条的组合技
HarmonyOS 校园应用系列之HarmonyOS ArkTS实战:AI 单词我的页 —— VIP 徽章与等级进度条的组合技
源码位置:
entry/src/main/ets/pages/ProfileTab.ets(225 行)
核心主题:渐变用户卡片 + 等级进度条 + VIP 徽章 + 四维统计面板 + 成就徽章墙 + 菜单列表
技术关键词:linearGradient、Scroll、@Builder组件拆分、条件 opacity、复杂布局嵌套
一、页面定位与技术复杂度
统计我的页是智能管理应用中 技术密度最高 的页面——225 行代码承载了 5 个独立 @Builder 组件,涵盖了渐变用户卡片(含等级系统和 VIP 徽章)、四维统计面板、游戏化成就徽章墙(含锁定态)、本周学习条形图、带箭头的设置菜单列表等多种 UI 模式。它不仅是一个"个人中心",更是一个微型的 “数据仪表盘 + 游戏化激励中心”。
从架构角度看,这个页面展示了 ArkUI 复杂长页面的最佳实践:纯 Scroll 长页面 + @Builder 组件拆分 + Theme.ets 常量类样式复用 + interface 先行的数据建模。每个 @Builder 平均约 30 行代码,职责单一清晰。


二、UserCard 渐变用户卡片 + 等级系统
UserCard 是整个页面最具视觉冲击力的组件——一个从左上到右下的蓝色渐变背景卡片,内含头像、用户信息、等级标签和经验值进度条:
@Builder
UserCard() {
Column({ space: 12 }) {
Row({ space: 14 }) {
// 头像 + Lv 角标(Stack 叠放)
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('VIP').fontSize(9).fontColor('#FFFFFF')
.backgroundColor('rgba(255,255,255,0.3)').padding({ left: 5, right: 5, top: 1, bottom: 1 }).borderRadius(3)
}
Text('CET-6 备考中 · 已学 386 词').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('114').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('76%').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 通栏无圆角的沉浸式头部
UserCard 没有任何圆角——渐变卡片通栏铺满屏幕宽度,直接从状态栏下方(padding({ top: this.safeTop + 30 }))延伸到快捷按钮行。这种 “Hero Header” 让卡片像是从屏幕边缘生长出来的,增强全屏沉浸感。
2.2 头像上的 Lv 角标
头像用 Stack({ alignContent: Alignment.BottomEnd }) 叠放两层:底层是 🧑🎓 emoji 头像(60×60、35% 半透明白底、D.rLg 圆角、白色描边),右下角叠放 Lv.4 等级角标(C.warn 橙底白字、白色描边)。等级信息直接"贴"在头像上,比单独一行文字更节省空间,也是游戏类 App 的经典手法。用户名右侧还有 VIP 徽章——半透明白底(rgba(255,255,255,0.3))+ 白字,与渐变背景融为一体。
2.3 白色经验条
| 属性 | 值 | 设计意图 |
|---|---|---|
| 填充色 | #FFFFFF 纯白 |
在蓝色渐变上对比度最高 |
| 轨道色 | rgba(255,255,255,0.25) |
半透明白融入渐变背景 |
| 当前进度 | 76% 宽(固定 140vp 总宽) | 距 Lv.5 还需 114 经验 |
经验条同样是 Stack 双层 Column 实现,高度 4vp。升级提示行"距 Lv.5 还需 114 经验"中,数字用 11vp 白色加粗、说明文字用 9vp 70% 白,形成小字号层级。UserCard 底部还有两个 玻璃拟态按钮——“🎁 每日签到"和"📊 词汇测试”,20% 半透明白底 + D.rSm 圆角,点击分别弹出签到和测试 Toast。
三、StatRow 四维统计面板
用户卡片下方是四个并排的统计数据,通过 ForEach(this.stats, ...) 内联渲染:
@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 })
}
数据来自 private stats: StatItem[]:386 已学单词、28 今日新词、92% 正确率、15 连续天数。
3.1 负 Margin 重叠效果
.margin({ top: -28 }) 让白色统计卡片向上侵入渐变 UserCard 区域 28vp,形成 卡片重叠 视觉效果。配合 width('92%')(两侧各留 4% 露出渐变底色)与 C.stroke 细边框,白色卡片仿佛"浮"在渐变背景之上。
3.2 彩色数字语义化
四个统计项的颜色由 数据驱动(s.color 字段,在 stats 数组中定义):
- 已学单词
C.primary(蓝色):主指标,使用主题色 - 今日新词
C.accent(浅蓝):次主指标 - 正确率
C.ok(绿色):正面指标,绿色=好 - 连续天数
C.warn(橙色):激励指标,橙色=活力/提醒
项间用竖向 Divider().vertical(true).height(32) 分隔(最后一项后不画),layoutWeight(1) 四等分。
四、AchievementWall 成就徽章墙
成就系统是这个页面最有趣的设计元素——它把枯燥的"单词学习"变成了一个有进度感和收集欲的游戏:
@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 })
}
4.1 Achievement 接口
interface Achievement {
icon: string; title: string; unlocked: boolean; desc: string;
}
unlocked: boolean 字段控制成就的解锁状态——这直接决定了图标容器的背景色和透明度、标题的颜色。icon 是 emoji 字符串(📖 🔥 🎯 ⭐ 👑 💎),渲染在 48×48vp 的圆角容器中,解锁项背景为 C.primarySoft(浅蓝),锁定项背景为 C.cardSoft(浅灰)。
4.2 锁定态的 opacity + 背景色双重区分
未解锁的成就使用 .opacity(0.4) + C.cardSoft 浅灰底——降低到 40% 不透明度并切换为灰色背景,双重视觉信号让锁定态一目了然。选择 opacity 而非隐藏或替换为锁图标是有意的设计决策——用户能看到"还有什么可以达成",这比神秘感更能激发行动欲望。同时 desc 文字(如 “学会2000词”、“连续30天”)清晰告知了解锁条件,消除了不确定性。标题文字也联动变化:解锁项 C.text,锁定项 C.textDim。
- 从心理学角度看,"部分可见的目标"比"完全隐藏的目标"更有驱动力
当前数据:6 枚成就中 4 枚已解锁(词汇入门、坚持7天、百词斩、满分通过),2 枚锁定(词汇大师、记忆之王)。4/6 = 67% 的解锁率意味着用户已经投入了大量时间,产生了足够的沉没成本不愿意放弃;而剩余 2 枚未解锁徽章又提供了明确的短期目标。
五、WeekChartCard 本周学习柱状图
个人中心的周数据图表与首页的 WeekChartCard 结构相同(含点击选中交互),但数据和标题不同:
@Builder
WeekChartCard() {
Column({ space: 12 }) {
Row() {
Text('📈 本周学习').fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text)
Blank()
Text('200 词').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 / 40 * 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 })
}
与首页的差异:数据源是 private weekBars(25/30/20/35/28/40/22),高度映射公式为 Math.max(6, b.value / 40 * 80)(以 40 为基准值),最大值 40 映射为 80vp 柱高。与首页不同的是,这里的柱状图 没有点击交互——高亮柱固定为 idx === 5(周六,值 40 即本周峰值),用 C.primary→C.accent 亮色渐变区分,其余柱用 C.primarySoft→C.primary 渐变。这说明两个页面的 WeekChartCard 虽然视觉相似,但交互模式不同:首页是可点击切换的,个人中心是固定高亮峰值的。
六、MenuList 设置菜单列表
页面底部是功能入口菜单列表:
@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)
}
6.1 MenuItem 接口
interface MenuItem {
id: number; emoji: string; title: string; desc: string; badge: string;
}
desc 是必选字段——每个菜单项都有副标题(如"词库管理"有"CET-4 / CET-6 / 考研")。emoji 是 emoji 字符串(📖 📊 🔔 🏆 ⚙️),渲染在 36×36vp 的 C.cardSoft 浅灰底圆角容器里。badge 字段为空字符串时不显示角标,非空时(如"NEW")显示彩色角标——NEW 用 C.ok 绿底,其他用 C.danger 红底。
6.2 菜单项布局模式
每个菜单项采用 “左图标 + 中文字 + 右箭头” 的标准列表项布局:
- 左侧:36×36vp 图标容器,
C.cardSoft浅灰底 +D.rSm圆角 - 中间:标题(14fp Medium)+ 副标题(11fp
C.textDim),左对齐,layoutWeight(1)占据剩余空间 - 右侧:可选 badge 角标 +
›箭头字符(22fpC.textDim)
所有菜单项被包裹在 同一个白色卡片容器 中(C.card + D.rMd + C.stroke),项间用 Divider().margin({ left: 58 }) 分隔(左缩进 58vp 对齐图标右侧),容器使用 .clip(true) 裁剪溢出内容。点击后通过 promptAction.showToast({ message: m.title }) 提示菜单标题。
当前数据:5 个菜单项(词库管理、学习统计、复习提醒、我的成就[NEW]、设置)。

七、数据模型全景
本页复用了全局的 StatItem/WeekBar 接口,自定义了 2 个 interface,无 @State 交互状态(纯展示页):
// 统计面板(private,颜色数据驱动)
private stats: StatItem[] = [
{ value: '386', label: '已学单词', color: C.primary },
{ value: '28', label: '今日新词', color: C.accent },
{ value: '92%', label: '正确率', color: C.ok },
{ value: '15', label: '连续天数', color: C.warn },
];
// 周数据(private)
private weekBars: WeekBar[] = [
{ day: '一', value: 25 }, { day: '二', value: 30 }, { day: '三', value: 20 },
{ day: '四', value: 35 }, { day: '五', value: 28 }, { day: '六', value: 40 },
{ day: '日', value: 22 }
];
// 成就(private)
private achievements: Achievement[] = [
{ icon: '📖', title: '词汇入门', unlocked: true, desc: '学会100词' },
{ icon: '🔥', title: '坚持7天', unlocked: true, desc: '连续学习7天' },
{ icon: '🎯', title: '百词斩', unlocked: true, desc: '日学100词' },
{ icon: '⭐', title: '满分通过', unlocked: true, desc: '练习全对' },
{ icon: '👑', title: '词汇大师', unlocked: false, desc: '学会2000词' },
{ icon: '💎', title: '记忆之王', unlocked: false, desc: '连续30天' },
];
// 菜单(private)
private menus: MenuItem[] = [
{ id: 1, emoji: '📖', title: '词库管理', desc: 'CET-4 / CET-6 / 考研', badge: '' },
{ id: 2, emoji: '📊', title: '学习统计', desc: '词汇量与记忆曲线', badge: '' },
{ id: 3, emoji: '🔔', title: '复习提醒', desc: '艾宾浩斯记忆法', badge: '' },
{ id: 4, emoji: '🏆', title: '我的成就', desc: '词汇达人徽章', badge: 'NEW' },
{ id: 5, emoji: '⚙️', title: '设置', desc: '账号与隐私', badge: '' },
];
interface 定义汇总:
| Interface | 用途 | 关键字段 |
|---|---|---|
Achievement |
成就徽章 | icon, title, unlocked, desc |
MenuItem |
菜单项 | id, emoji, title, desc, badge |
注意所有展示型数据(stats/weekBars/achievements/menus)都是 private 普通成员——本页是纯展示页,无任何 @State 交互状态。柱状图高亮固定为周六(idx===5),不可点击切换。这种"全 private + 无 @State"的设计让状态管理开销最小化。

八、@Builder 拆分粒度分析
本页共 6 个 @Builder 方法:
| @Builder | 行数(约) | 职责 | 复用次数 |
|---|---|---|---|
| UserCard | 50 | 用户信息+等级+快捷按钮 | 1 |
| StatRow | 14 | 四维统计面板 | 1 |
| WeekChartCard | 28 | 周数据柱状图 | 1 |
| AchievementWall | 25 | 成就徽章网格 | 1 |
| MenuList | 27 | 功能菜单列表 | 1 |
| About | 7 | 版本信息页脚 | 1 |
平均每个 @Builder 约 25 行代码(不含内部嵌套)。所有 @Builder 都是一次性组件(统计项和成就项通过 ForEach 内联渲染,未拆子 @Builder)。这种拆分粒度的合理性在于:
- 即使不被复用,将大段 UI 代码提取为命名 @Builder 也能提升 build() 方法的可读性
- 每个 @Builder 可以独立阅读和理解,不需要在 225 行代码中上下翻找
- 未来如果某个组件需要在其他页面复用(如 WeekChartCard),可以直接搬移

九、用户卡片的渐变与安全区交互
UserCard 的 linearGradient 渐变在沉浸式全屏模式下有一个独特的视觉效果:UserCard 内部 Row 设置了 .padding({ top: this.safeTop + 30 }),渐变背景会填充包括 padding 在内的整个卡片区域——也就是说 渐变色从状态栏下方开始渲染,一直延伸到卡片底部。
这是因为 ArkUI 的 background 和 gradient 会填充包括 padding 在内的整个组件区域。UserCard 没有圆角——渐变通栏铺满屏幕宽度,顶部直角贴住屏幕边缘,用户看到的视觉效果是蓝色渐变从屏幕顶部(状态栏区域)“流下来”,无缝过渡到白色内容区。这种 “无边框沉浸感” 正是全屏模式相比传统安全区模式的独特优势——传统模式下状态栏区域是系统保留的纯色/模糊背景,无法被应用内容利用;而全屏模式下整个屏幕画布都是应用的,可以自由发挥创意。
智能管理应用充分利用了这一特性,让 UserCard 的渐变背景成为整个页面最具识别度的视觉元素。在实际产品中,很多热门 App 都采用了类似的全屏渐变设计——如网易云音乐的个人页头部、Keep 的运动数据卡片、知乎的用户主页——它们都利用全屏模式将品牌色延伸到系统区域,创造出强烈的视觉归属感。这种设计手法被称为 “Hero Header” 或 “品牌沉浸头”,已成为现代 App 个人中心页的标准配置之一。
十一、成就系统的产品运营视角
从产品运营角度看,成就系统的核心指标是 解锁率 和 留存贡献度。本例中 4/6 = 67% 的解锁率意味着用户已经完成了大部分目标,产生了足够的沉没成本不愿意放弃;而剩余 2 枚未锁定徽章又提供了明确的短期目标(如"再连续 3 天就能拿到记忆之王")。
成就设计的心理学基础是 “目标梯度效应”(Goal Gradient Effect) ——人们越接近目标时,行动动力越强。因此成就系统的关键不在于设置多少个成就,而在于:
- 第一个成就要容易达成(如"词汇入门"只需学会100词),让用户快速获得正向反馈
- 中期成就要有节奏感(7天、100词、满分),避免长时间没有新成就导致的动力低谷
- 终极成就要有稀缺感(2000词、30天连续),成为用户的长期追求目标
十二、菜单列表的路由跳转与导航设计
MenuList 中的每个菜单项点击后通过 promptAction.showToast({ message: m.title }) 给出提示。在实际产品中,这里应该是页面路由跳转。HarmonyOS 推荐使用 Navigation 组件实现页面跳转(router.pushUrl 虽仍可用但官方推荐迁移至 Navigation):
.onClick(() => {
// 推荐使用 Navigation 组件的 NavPathStack 实现页面跳转
this.pathStack.pushPath({
name: m.id === 1 ? 'LibraryPage' : 'StatsPage',
params: { from: 'profile' }
})
})
HarmonyOS 的 Navigation 组件提供了完整的页面导航能力:
NavPathStack.pushPath()— 压入新页面(保留当前页面在栈中)NavPathStack.replacePath()— 替换当前页面(不保留)NavPathStack.pop()— 返回上一页NavPathStack.clear()— 清空页面栈(用于退出登录等场景)
菜单列表作为个人中心的导航枢纽,通常采用 pushPath 模式——这样用户可以通过返回按钮回到个人中心,形成清晰的层级导航结构。
十、写在最后
统计我的页以 225 行代码实现了功能完整的个人中心:沉浸式渐变用户卡片(含等级系统和 VIP 徽章)、四维彩色统计面板、游戏化成就徽章墙(含锁定态 opacity)、本周学习趋势图、功能入口菜单列表。它是学习 ArkUI 复杂长页面构建 和 @Builder 组件化拆分 的最佳实战案例之一。
特别值得学习的是其 "小而多"的 @Builder 拆分粒度——6 个组件平均 25 行,每个职责单一、可独立阅读、可单独测试。这种拆分方式比把 225 行全塞进一个巨型 build() 方法要容易维护得多,也比过度拆分(每个 5~10 行)减少了组件间通信的复杂度。
更多推荐




所有评论(0)