HarmonyOS 应用 · 校园宿舍拼单我的页深度解析:渐变用户卡与成就徽章的 ArkUI 实战

本文以 HarmonyOS 6.0(API 23)+ ArkTS 声明式 UI 为技术底座,逐行拆解「校园宿舍拼单」应用"我的"Tab(ProfileTab)的搭建思路——从渐变用户卡到签到系统,从成就徽章墙到菜单列表。所有截图均来自本人电脑正在运行的 HarmonyOS 模拟器真机画面。


一、项目缘起:为什么"我的"页是品牌感的载体

前 3 个 Tab(首页、拼单、订单)解决的是"用户能不能用"的问题,而"我的"页解决的是"用户愿不愿意用"的问题。一款应用的"我的"页是品牌调性最集中的体现——它不像首页那样追求信息密度,也不像拼单页那样追求流程效率,而是要展现"用户在这个 App 里是什么样的人"。

「校园宿舍拼单」的 ProfileTab 用 6 张卡片回答了这个问题:

  1. 用户卡:渐变背景 + 等级徽章 + 经验进度条 → 用户是 Lv5 拼单达人
  2. 统计行:拼单数 / 已省 / 好评 / 好友 → 用户的拼单履历
  3. 成就徽章:6 个徽章 3 解锁 3 未解锁 → 用户的"成就系统"
  4. 活跃度:本周拼单趋势 → 用户的活跃节奏
  5. 菜单列表:6 个常用入口 → 用户的下一步行动
  6. 版本信息:App 元数据 → 用户与开发者的连接

整个 ProfileTab 位于 entry/src/main/ets/pages/ProfileTab.ets,约 235 行 ArkTS 代码,由 6 个 @Builder 区块组成。


我的页首屏真机截图

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

二、整体架构:没有 Header 的全屏滚动

ProfileTab 的结构与前 3 个 Tab 不同——它没有 Header

build() {
  Column() {
    Scroll() {
      Column({ space: 14 }) {
        this.UserCard()
        this.StatRow()
        this.BadgeWall()
        this.WeekChart()
        this.MenuList()
        this.AboutInfo()
      }
      .width('100%').padding({ bottom: D.pad + this.safeBottom + 20 })
    }.layoutWeight(1).scrollBar(BarState.Off).align(Alignment.Top)
  }.width('100%').height('100%').backgroundColor(C.bg)
}

为什么没有 Header?因为 UserCard 本身就是 Header——它的渐变背景延伸到状态栏区域,承担了"顶部标识"的功能。这种"用一个内容组件兼任 Header"的设计,让整个 App 的"我的"页比传统 App 更沉浸、更整体。

整个滚动列表的 space 是 14,与首页一致——因为 ProfileTab 和首页一样是"内容展示型"页面,而非"表单型"页面。

下面这张图,是 HarmonyOS 模拟器里我的页首屏的真实运行效果:

可以看到顶部橙色渐变用户卡(“张同学”+“Lv5 拼单达人”+"签到"按钮+"EXP 68/100"经验条),下方是 4 栏统计卡(28 拼单 / ¥386 已省 / 96% 好评 / 15 好友),再下面是成就徽章墙(拼单达人 / 省钱专家 / 人气王已解锁,速拼之星 / 五星好评 / 团长之王未解锁)。


我的页首屏

三、UserCard:渐变背景 + 等级徽章 + 签到系统

@Builder UserCard() {
  Column({ space: 14 }) {
    Row({ space: 14 }) {
      Stack({ alignContent: Alignment.BottomEnd }) {
        Row() { Text('😀').fontSize(36) }
        .width(64).height(64).borderRadius(32)
        .backgroundColor('#FFFFFF44').justifyContent(FlexAlign.Center)
        .border({ width: 3, color: '#FFFFFF' })
        Row() { Text('Lv5').fontSize(9).fontColor('#FFFFFF').fontWeight(FontWeight.Bold) }
        .width(28).height(20).backgroundColor(C.warn).borderRadius(10)
        .border({ width: 2, color: '#FFFFFF' }).justifyContent(FlexAlign.Center)
      }.width(64).height(64)
      Column({ space: 5 }) {
        Text('张同学').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
        Row({ space: 8 }) {
          Text('🏆 拼单达人').fontSize(11).fontColor('#FFFFFFCC')
          Text('| ID: 2024001').fontSize(11).fontColor('#FFFFFF99')
        }.width('100%')
        Text('宿舍楼 3 号楼 402 室').fontSize(12).fontColor('#FFFFFFAA')
      }.alignItems(HorizontalAlign.Start).layoutWeight(1)
      Button(this.checkedIn ? '已签到' : '签到')
        .fontSize(13).fontColor('#FFFFFF')
        .backgroundColor(this.checkedIn ? '#33FFFFFF' : '#FFFFFF33')
        .borderRadius(16).height(32).enabled(!this.checkedIn)
        .onClick(() => {
          this.checkedIn = true;
          this.xp = Math.min(100, this.xp + 5);
          promptAction.showToast({ message: '签到成功 +5 经验' });
        })
    }.width('100%')
    Stack({ alignContent: Alignment.Start }) {
      Column().width('100%').height(8).backgroundColor('#33FFFFFF').borderRadius(4)
      Column().width(this.xp + '%').height(8).borderRadius(4).backgroundColor('#FFFFFF')
    }.width('100%')
    Row() {
      Text('EXP ' + this.xp + '/100').fontSize(11).fontColor('#FFFFFFCC')
      Blank()
      Text('距 Lv6 还差 ' + (100 - this.xp) + ' 经验').fontSize(11).fontColor('#FFFFFFAA')
    }.width('100%')
  }
  .width('100%')
  .padding({ top: this.safeTop + 20, left: D.pad, right: D.pad, bottom: 30 })
  .linearGradient({ angle: 135, colors: [[C.primary, 0.0], [C.accent, 1.0]] })
}

UserCard 是整个 ProfileTab 里代码量最大的 @Builder,承载了 5 个核心元素:用户头像 + 等级徽章、用户信息、签到按钮、经验进度条、底部 EXP 文字。

3.1 头像 + 等级徽章:双层 Stack 叠加

Stack({ alignContent: Alignment.BottomEnd }) {
  Row() { Text('😀').fontSize(36) }
  .width(64).height(64).borderRadius(32)
  .backgroundColor('#FFFFFF44').justifyContent(FlexAlign.Center)
  .border({ width: 3, color: '#FFFFFF' })
  Row() { Text('Lv5').fontSize(9).fontColor('#FFFFFF').fontWeight(FontWeight.Bold) }
  .width(28).height(20).backgroundColor(C.warn).borderRadius(10)
  .border({ width: 2, color: '#FFFFFF' }).justifyContent(FlexAlign.Center)
}.width(64).height(64)

头像 64×64vp,半径 32(完全圆形),背景 #FFFFFF44(26% 不透明白色,在橙黄渐变上呈"半透明白圆"效果),3vp 白边让它从背景中"浮"出来。

Stack({ alignContent: Alignment.BottomEnd }) 把右下角让出来给"Lv5"徽章——徽章 28×20vp,警告黄背景 + 白字 + 2vp 白边。整个"头像 + 徽章"的组合在视觉上是一个"戴勋章的运动员",既有个人身份感(emoji 头像),又有等级系统感(Lv5 徽章)。

这种"Stack + 不同对齐方式"实现"角标 / 徽章叠加"是 ArkUI 的基础模式,与首页 GroupCard 上的"热销 / 爆品"角标同源。

3.2 用户信息:3 行文字

Column({ space: 5 }) {
  Text('张同学').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
  Row({ space: 8 }) {
    Text('🏆 拼单达人').fontSize(11).fontColor('#FFFFFFCC')
    Text('| ID: 2024001').fontSize(11).fontColor('#FFFFFF99')
  }.width('100%')
  Text('宿舍楼 3 号楼 402 室').fontSize(12).fontColor('#FFFFFFAA')
}.alignItems(HorizontalAlign.Start).layoutWeight(1)

3 行信息:姓名、成就称号 + ID、宿舍地址。3 种不同的白色透明度——#FFFFFF(100%)、#FFFFFFCC(80%)、#FFFFFF99(60%)、#FFFFFFAA(67%)。这种"用透明度而非灰度"模拟层级,是大厂 Banner 设计的经典手法——避免在彩色背景上出现"脏灰色",保持渐变活力。

3.3 签到按钮:状态切换

Button(this.checkedIn ? '已签到' : '签到')
  .fontSize(13).fontColor('#FFFFFF')
  .backgroundColor(this.checkedIn ? '#33FFFFFF' : '#FFFFFF33')
  .borderRadius(16).height(32).enabled(!this.checkedIn)
  .onClick(() => {
    this.checkedIn = true;
    this.xp = Math.min(100, this.xp + 5);
    promptAction.showToast({ message: '签到成功 +5 经验' });
  })

@State checkedIn: boolean = false 控制按钮状态:

  • 未签到#FFFFFF33(20% 白)+ 文字"签到",可点击
  • 已签到#33FFFFFF(20% 白,更深)+ 文字"已签到",enabled(false) 禁用

点击触发 3 件事:

  1. this.checkedIn = true:切换按钮状态
  2. this.xp = Math.min(100, this.xp + 5):经验 +5,不超过 100 上限
  3. promptAction.showToast({ message: '签到成功 +5 经验' }):弹 toast 反馈

这是 ArkUI 表单交互的经典模式——单一动作触发多状态更新

3.4 经验进度条

Stack({ alignContent: Alignment.Start }) {
  Column().width('100%').height(8).backgroundColor('#33FFFFFF').borderRadius(4)
  Column().width(this.xp + '%').height(8).borderRadius(4).backgroundColor('#FFFFFF')
}.width('100%')

与首页、订单页的进度条同构:浅色轨道 + 进度填充。但这里的进度条是纯白色填充,因为背景已经是渐变色,再用主色填充会"撞色"。用白条 + 半透明白色轨道,是深色 Banner 上的经典进度条配色。

@State xp: number = 68 初始 68。每次签到 +5,最高 100。经验值达到 100 时,可以升级到 Lv6(虽然代码里没实现升级逻辑,但体验流程是完整的)。

3.5 渐变背景

.linearGradient({ angle: 135, colors: [[C.primary, 0.0], [C.accent, 1.0]] })

与首页 HeroCard 同款渐变(橙红 → 金黄)。这种"主色渐变 + 白色文字"的卡片,几乎是所有"会员中心 / 个人主页"类页面的标配——它在视觉上"宣告"了用户的归属。

padding({ top: this.safeTop + 20, left: D.pad, right: D.pad, bottom: 30 }) 让卡片顶部紧贴状态栏(safeTop 已经在 EntryAbility 里算好),底部 30vp 留出更多空间给接下来的 StatRow"叠"上来。


四、StatRow:4 栏统计卡(叠在 UserCard 上)

@Builder StatRow() {
  Row() {
    ForEach(this.stats, (s: StatCell, idx: number) => {
      Column({ space: 4 }) {
        Text(s.value).fontSize(22).fontWeight(FontWeight.Bold).fontColor(s.color)
        Text(s.label).fontSize(11).fontColor(C.textDim)
      }.layoutWeight(1)
      if (idx < this.stats.length - 1) {
        Divider().vertical(true).height(28).color(C.stroke)
      }
    }, (s: StatCell) => s.label)
  }
  .width('90%').padding({ top: 14, bottom: 14 })
  .backgroundColor(C.card).borderRadius(D.rLg)
  .border({ width: 1, color: C.stroke }).margin({ top: -28 })
  .justifyContent(FlexAlign.Center)
}

注意 margin({ top: -28 })——这是 StatRow 最巧妙的设计。整张卡片向上偏移 28vp,叠在 UserCard 上

.width('90%').padding({ top: 14, bottom: 14 })
.backgroundColor(C.card).borderRadius(D.rLg)
.border({ width: 1, color: C.stroke }).margin({ top: -28 })

width('90%') 让 StatRow 比 UserCard 窄 10%,形成"卡片宽度窄于顶部卡片"的层级感。margin({ top: -28 }) 让它向上突出 28vp,与 UserCard 重叠——这是国内 App(如小红书、Keep等)常见的"叠加卡片"设计手法,在支付宝等 App 的"我的"页也能见到。

数据驱动:

private stats: StatCell[] = [
  { label: '拼单数', value: '28', color: C.primary },
  { label: '已省', value: '¥386', color: C.danger },
  { label: '好评', value: '96%', color: C.ok },
  { label: '好友', value: '15', color: C.accent }
];

4 个数字用 4 种颜色:橙(主色)、红(强调)、绿(成功)、金黄(点缀)。每个数字有明确含义,且颜色映射出"用户在 App 里的多维成就"。


五、BadgeWall:成就徽章墙

@Builder BadgeWall() {
  Column({ space: 14 }) {
    Row() {
      Text('成就徽章').fontSize(16).fontWeight(FontWeight.Bold).fontColor(C.text)
      Blank()
      Text('3/6').fontSize(12).fontColor(C.textDim)
    }.width('100%')
    Row() {
      ForEach(this.badges, (b: Badge) => {
        Column({ space: 6 }) {
          Row() { Text(b.emoji).fontSize(28) }
          .width(48).height(48).borderRadius(24)
          .backgroundColor(b.unlocked ? C.primarySoft : C.cardSoft)
          .justifyContent(FlexAlign.Center)
          .opacity(b.unlocked ? 1.0 : 0.35)
          .border({ width: 1, color: b.unlocked ? C.primary : C.stroke })
          Text(b.name).fontSize(10).fontColor(b.unlocked ? C.text : C.textDim)
        }.layoutWeight(1)
      }, (b: Badge) => b.id.toString())
    }.width('100%')
  }
  .width('100%').padding(16).backgroundColor(C.card)
  .borderRadius(D.rLg).border({ width: 1, color: C.stroke })
}

6 个徽章,3 个已解锁 3 个未解锁:

private badges: Badge[] = [
  { id: 1, emoji: '🏆', name: '拼单达人', unlocked: true },
  { id: 2, emoji: '💰', name: '省钱专家', unlocked: true },
  { id: 3, emoji: '🔥', name: '人气王', unlocked: true },
  { id: 4, emoji: '🚀', name: '速拼之星', unlocked: false },
  { id: 5, emoji: '⭐', name: '五星好评', unlocked: false },
  { id: 6, emoji: '👑', name: '团长之王', unlocked: false }
];

每个徽章的处理非常细致——unlocked 字段驱动 4 个视觉维度

维度已解锁未解锁
背景色primarySoft(淡橙)cardSoft(淡灰)
不透明度1.0(饱满)0.35(灰蒙)
边框色primary(主色)stroke(灰描边)
文字色text(主文)textDim(暗淡)

4 重状态差异让"已解锁"和"未解锁"的徽章在视觉上形成强烈对比——已解锁的鲜艳饱满,未解锁的灰暗朦胧。这种"奖励机制"的视觉处理,是游戏化设计(Gamification)的经典手法。

右上角的"3/6"动态显示解锁进度,是给用户的"目标提示"——让用户知道还有 3 个徽章可以努力。


六、WeekChart:本周拼单活跃度

@Builder WeekChart() {
  Column({ space: 12 }) {
    Row() {
      Text('本周拼单活跃度').fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text)
      Blank()
      Text('总计 385').fontSize(12).fontColor(C.primary)
    }.width('100%')
    Row() {
      ForEach(this.barData, (b: BarData, idx: number) => {
        Column({ space: 6 }) {
          Stack({ alignContent: Alignment.Bottom }) {
            Column().width(16).height(b.value * 1.0).borderRadius(5)
              .linearGradient({
                angle: 180,
                colors: this.selectedBar === idx
                  ? [[C.primary, 0.0], [C.accent, 1.0]]
                  : [[C.primarySoft, 0.0], [C.primary, 1.0]]
              }).opacity(this.selectedBar === idx ? 1.0 : 0.5)
          }.width(24).height(90)
          Text(b.day).fontSize(10)
            .fontColor(this.selectedBar === idx ? C.primary : C.textDim)
            .fontWeight(this.selectedBar === idx ? FontWeight.Bold : FontWeight.Normal)
        }.layoutWeight(1).onClick(() => { this.selectedBar = idx; })
      }, (b: BarData) => b.day)
    }.width('100%')
  }
  .width('100%').padding(16).backgroundColor(C.card)
  .borderRadius(D.rLg).border({ width: 1, color: C.stroke })
}

与首页 WeekChart 同构,但有 4 处细节差异:

  1. 柱体最大高度 90(首页是 120),更适合"个人活跃度"的紧凑视觉
  2. 柱体宽度 16(首页是 18),更精致
  3. 不透明度对比 1.0 vs 0.5(首页是 1.0 vs 0.6),更明显的选中态
  4. 数据不同:首页是 60/80/45/90/70/100/55(首页日均拼单数),我的页是 30/50/40/70/60/90/45(个人活跃度)
private barData: BarData[] = [
  { day: '一', value: 30 }, { day: '二', value: 50 }, { day: '三', value: 40 },
  { day: '四', value: 70 }, { day: '五', value: 60 }, { day: '六', value: 90 },
  { day: '日', value: 45 }
];

默认值 5(周六),@State selectedBar: number = 5。点击柱子切换选中态,与首页同模式。

注意右上角的"总计 385"——这是 7 天数据的求和 (30+50+40+70+60+90+45=385)。在 UI 上用静态文字展示,但实际上是"前端实时计算"或"后端返回"的——这种"主操作区域右上角显示汇总"是数据看板的常见设计。


七、MenuList:6 个菜单项 + Badge 角标

@Builder MenuList() {
  Column({ space: 0 }) {
    ForEach(this.menus, (m: MenuItem) => {
      Row() {
        Text(m.icon).fontSize(20).width(32)
        Text(m.label).fontSize(15).fontColor(C.text).layoutWeight(1)
        if (m.showBadge) {
          Text(m.badge).fontSize(10).fontColor('#FFFFFF')
            .padding({ left: 6, right: 6, top: 2, bottom: 2 })
            .backgroundColor(m.badgeColor).borderRadius(8)
            .margin({ right: 8 })
        }
        Text('›').fontSize(18).fontColor(C.textDim)
      }
      .width('100%').height(52)
      .padding({ left: 16, right: 16 })
      .backgroundColor(C.card)
      .onClick(() => { promptAction.showToast({ message: m.label }); })
      if (m.id < this.menus.length) {
        Divider().color(C.stroke).margin({ left: 48 })
      }
    }, (m: MenuItem) => m.id.toString())
  }
  .width('100%').backgroundColor(C.card)
  .borderRadius(D.rLg).border({ width: 1, color: C.stroke })
}

6 个菜单项:

private menus: MenuItem[] = [
  { id: 1, icon: '📦', label: '我的订单', badge: '5', badgeColor: C.danger, showBadge: true },
  { id: 2, icon: '❤️', label: '我的收藏', badge: '', badgeColor: '', showBadge: false },
  { id: 3, icon: '👥', label: '我的好友', badge: 'NEW', badgeColor: C.ok, showBadge: true },
  { id: 4, icon: '🎟️', label: '优惠券', badge: '3', badgeColor: C.danger, showBadge: true },
  { id: 5, icon: '📊', label: '消费统计', badge: '', badgeColor: '', showBadge: false },
  { id: 6, icon: '⚙️', label: '设置', badge: '', badgeColor: '', showBadge: false }
];

每个菜单项的视觉结构是 [emoji icon] [label] [optional badge] [chevron ›]。注意几处细节:

  1. emoji 固定宽度 32:让所有 emoji 头像对齐,整列更整齐
  2. 可选 badgeif (m.showBadge) 控制是否渲染 badge,支持数字("5"“3”)和文字(“NEW”)两种形态
  3. chevron ›:所有菜单都有箭头,提示"可点击跳转"
  4. 分割线Divider().color(C.stroke).margin({ left: 48 }) —— margin left 48 让分割线从 emoji 后开始,避免"穿过 emoji"的视觉混乱
  5. if (m.id < this.menus.length) 边界判断:最后一项不画分割线

MenuItem 接口用 showBadge: boolean 字段控制角标显示,比单纯用 if (badge !== '') 更类型安全——避免空字符串判断的"魔法值"陷阱。

下图是"我的"页底部,可以看到完整菜单列表和版本信息:

可以看到 6 个菜单项(我的订单带红色"5"角标、我的收藏无角标、我的好友带绿色"NEW"、优惠券带红色"3"、消费统计、设置),底部是版本信息"宿舍拼单 v1.0.0" + “HarmonyOS 6.0+ · ArkTS · API 23”。


我的页底部真机截图

八、AboutInfo:版本信息

@Builder AboutInfo() {
  Column({ space: 4 }) {
    Text('宿舍拼单 v1.0.0').fontSize(12).fontColor(C.textDim)
    Text('HarmonyOS 6.0+ · ArkTS · API 23').fontSize(11).fontColor(C.textDim)
  }.width('100%').alignItems(HorizontalAlign.Center).padding({ top: 10 })
}

极简的版本信息——2 行文字居中显示。alignItems(HorizontalAlign.Center) 让文字水平居中,padding({ top: 10 }) 与上方 MenuList 留 10vp 间距。

这种"低调的版本信息"放在页面最底部,符合"用户不关心但不删会显得不完整"的处理原则——既提供了开发者与用户的连接点,又不会抢走主要内容的风头。


我的页底部

九、状态管理:3 个 @State 字段

整个 ProfileTab 用 3 个 @State

@State checkedIn: boolean = false;
@State selectedBar: number = 5;
@State xp: number = 68;
  • checkedIn:签到状态
  • selectedBar:活跃度图选中柱下标
  • xp:经验值(默认 68,签到后 +5)

加上 4 个数据接口(barData / stats / badges / menus),整张页面的"事实来源"清晰简单。注意 xp 的更新逻辑用了 Math.min(100, this.xp + 5) 这种"上限保护"——避免经验值超过 100 导致进度条溢出。


十、ArkTS 工程化细节

ProfileTab 同样严格遵循项目规范:

  1. 接口先行MenuItemBadgeBarDataStatCell 4 个接口显式声明。
  2. ForEach 第三参数必填:所有 ForEach 都传了 keyGenerator。
  3. 无类型断言:全文没有 as any 等逃生舱口。
  4. #XXXXXX44 等 8 位十六进制色值:ArkUI 支持带 alpha 通道的颜色字符串,前六位是 RGB,后两位是 alpha(00-FF),例如 #FFFFFF44 表示白色 RGB=FFFFFF、alpha=44(约 27% 不透明度)。ArkUI 和 CSS 都支持在颜色字符串中带 alpha 通道(CSS Color Level 4 同样支持 #RRGGBBAA 格式)。

特别值得一提的是 opacitybackgroundColor alpha 的区别——opacity(0.35) 控制整个组件(包括文字、边框)的不透明度,而 backgroundColor('#FFFFFF44') 只控制背景的透明度。在徽章墙的设计中,已解锁和未解锁用 opacity(0.35) 一起处理 emoji + 背景 + 边框的"整体灰蒙"效果,是最经济的方式。


十一、可演进方向

ProfileTab 已经是一个相当完整的"个人中心",但还有以下可演进方向:

  1. 签到日历:当前是单次按钮,可以升级为"7 天签到日历",连续签到额外奖励经验。
  2. 徽章点击查看详情:点击未解锁徽章弹层展示"解锁条件"(如"再发起 5 次拼单即可解锁")。
  3. 菜单项跳转:当前菜单点击只弹 toast,可以接入路由跳转到对应子页面。
  4. 头像上传:当前是 emoji 头像,可以接入图片选择器让用户上传真实头像。
  5. 设置页面:从菜单项"设置"跳转到完整的设置页(主题切换、消息推送、隐私设置等)。
  6. 数据可视化升级:当前 WeekChart 是简单的条形图,可以拓展为"日 / 周 / 月"维度切换,或加入更多维度的趋势图。

十二、写在最后

回到开篇的两张真机截图——

从顶部渐变用户卡(橙红 → 金黄,“张同学” Lv5),到 4 栏统计叠卡(28 / ¥386 / 96% / 15),再到 6 个成就徽章(3 解锁 / 3 未解锁)和本周活跃度条形图,最后到 6 个带 badge 的菜单项,整张"我的"页用约 235 行 ArkTS 代码,构建出了一个"既完整又有品牌感"的个人中心。

它最值得借鉴的设计有 4 处:

  1. 没有 Header,用 UserCard 兼任:渐变背景延伸至状态栏,让整个页面更沉浸。
  2. StatRow 向上叠加margin({ top: -28 }) 让统计卡"挂"在用户卡下方,形成国内 App 常见的叠加层次。
  3. 徽章的多重视觉反馈:用 opacity + backgroundColor + border + 文字色 4 重差异区分"已解锁 / 未解锁",让奖励机制非常醒目。
  4. 菜单项的 Badge 模式:用 showBadge: boolean + badgeColor 接口字段,让菜单项能动态显示"5"“NEW”"3"等多种角标,是"通知中心"类设计的基础模式。

如果你也在做 HarmonyOS 个人中心页,不妨从"渐变 + 叠加 + 徽章 + 菜单"这四个组件开始搭骨架。当你把"我的"页打磨到这种程度,整个 App 的品牌感就已经立住了。

Logo

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

更多推荐