HarmonyOS 校园应用系列之HarmonyOS ArkTS实战:AI 单词我的页 —— VIP 徽章与等级进度条的组合技

源码位置entry/src/main/ets/pages/ProfileTab.ets(225 行)
核心主题:渐变用户卡片 + 等级进度条 + VIP 徽章 + 四维统计面板 + 成就徽章墙 + 菜单列表
技术关键词linearGradientScroll@Builder 组件拆分、条件 opacity、复杂布局嵌套

一、页面定位与技术复杂度

统计我的页是智能管理应用中 技术密度最高 的页面——225 行代码承载了 5 个独立 @Builder 组件,涵盖了渐变用户卡片(含等级系统和 VIP 徽章)、四维统计面板、游戏化成就徽章墙(含锁定态)、本周学习条形图、带箭头的设置菜单列表等多种 UI 模式。它不仅是一个"个人中心",更是一个微型的 “数据仪表盘 + 游戏化激励中心”

从架构角度看,这个页面展示了 ArkUI 复杂长页面的最佳实践:纯 Scroll 长页面 + @Builder 组件拆分 + Theme.ets 常量类样式复用 + interface 先行的数据建模。每个 @Builder 平均约 30 行代码,职责单一清晰。


应用效果图

统计我的页首屏

项目源码开源:https://gitee.com/codenestFlow/HarmonyOSHub

二、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")显示彩色角标——NEWC.ok 绿底,其他用 C.danger 红底。

6.2 菜单项布局模式

每个菜单项采用 “左图标 + 中文字 + 右箭头” 的标准列表项布局:

  • 左侧:36×36vp 图标容器,C.cardSoft 浅灰底 + D.rSm 圆角
  • 中间:标题(14fp Medium)+ 副标题(11fp C.textDim),左对齐,layoutWeight(1) 占据剩余空间
  • 右侧:可选 badge 角标 + 箭头字符(22fp C.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) ——人们越接近目标时,行动动力越强。因此成就系统的关键不在于设置多少个成就,而在于:

  1. 第一个成就要容易达成(如"词汇入门"只需学会100词),让用户快速获得正向反馈
  2. 中期成就要有节奏感(7天、100词、满分),避免长时间没有新成就导致的动力低谷
  3. 终极成就要有稀缺感(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 行)减少了组件间通信的复杂度。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐