2026/8/24 4:18:33

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

HarmonyOS 应用 · 校园宿舍拼单我的页深度解析:渐变用户卡与成就徽章的 ArkUI 实战 HarmonyOS 应用 · 校园宿舍拼单我的页深度解析渐变用户卡与成就徽章的 ArkUI 实战本文以 HarmonyOS 6.0API 23 ArkTS 声明式 UI 为技术底座逐行拆解「校园宿舍拼单」应用我的TabProfileTab的搭建思路——从渐变用户卡到签到系统从成就徽章墙到菜单列表。所有截图均来自本人电脑正在运行的 HarmonyOS 模拟器真机画面。一、项目缘起为什么我的页是品牌感的载体前 3 个 Tab首页、拼单、订单解决的是用户能不能用的问题而我的页解决的是用户愿不愿意用的问题。一款应用的我的页是品牌调性最集中的体现——它不像首页那样追求信息密度也不像拼单页那样追求流程效率而是要展现用户在这个 App 里是什么样的人。「校园宿舍拼单」的ProfileTab用 6 张卡片回答了这个问题用户卡渐变背景 等级徽章 经验进度条 → 用户是 Lv5 拼单达人统计行拼单数 / 已省 / 好评 / 好友 → 用户的拼单履历成就徽章6 个徽章 3 解锁 3 未解锁 → 用户的成就系统活跃度本周拼单趋势 → 用户的活跃节奏菜单列表6 个常用入口 → 用户的下一步行动版本信息App 元数据 → 用户与开发者的连接整个ProfileTab位于entry/src/main/ets/pages/ProfileTab.ets约 235 行 ArkTS 代码由 6 个Builder区块组成。项目源码开源https://gitee.com/codenestFlow/HarmonyOSHub二、整体架构没有 Header 的全屏滚动ProfileTab的结构与前 3 个 Tab 不同——它没有 Headerbuild() { 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完全圆形背景#FFFFFF4426% 不透明白色在橙黄渐变上呈半透明白圆效果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 种不同的白色透明度——#FFFFFF100%、#FFFFFFCC80%、#FFFFFF9960%、#FFFFFFAA67%。这种用透明度而非灰度模拟层级是大厂 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控制按钮状态未签到#FFFFFF3320% 白 文字签到可点击已签到#33FFFFFF20% 白更深 文字已签到enabled(false)禁用点击触发 3 件事this.checkedIn true切换按钮状态this.xp Math.min(100, this.xp 5)经验 5不超过 100 上限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叠上来。四、StatRow4 栏统计卡叠在 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 处细节差异柱体最大高度 90首页是 120更适合个人活跃度的紧凑视觉柱体宽度 16首页是 18更精致不透明度对比 1.0 vs 0.5首页是 1.0 vs 0.6更明显的选中态数据不同首页是 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 天数据的求和 (30504070609045385)。在 UI 上用静态文字展示但实际上是前端实时计算或后端返回的——这种主操作区域右上角显示汇总是数据看板的常见设计。七、MenuList6 个菜单项 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 ›]。注意几处细节emoji 固定宽度 32让所有 emoji 头像对齐整列更整齐可选 badgeif (m.showBadge)控制是否渲染 badge支持数字5“3”和文字“NEW”两种形态chevron ›所有菜单都有箭头提示可点击跳转分割线Divider().color(C.stroke).margin({ left: 48 })—— margin left 48 让分割线从 emoji 后开始避免穿过 emoji的视觉混乱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 个StateState 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同样严格遵循项目规范接口先行MenuItem、Badge、BarData、StatCell4 个接口显式声明。ForEach 第三参数必填所有ForEach都传了 keyGenerator。无类型断言全文没有as any等逃生舱口。#XXXXXX44等 8 位十六进制色值ArkUI 支持带 alpha 通道的颜色字符串前六位是 RGB后两位是 alpha00-FF例如#FFFFFF44表示白色 RGBFFFFFF、alpha44约 27% 不透明度。ArkUI 和 CSS 都支持在颜色字符串中带 alpha 通道CSS Color Level 4 同样支持#RRGGBBAA格式。特别值得一提的是opacity与backgroundColor alpha的区别——opacity(0.35)控制整个组件包括文字、边框的不透明度而backgroundColor(#FFFFFF44)只控制背景的透明度。在徽章墙的设计中已解锁和未解锁用opacity(0.35)一起处理 emoji 背景 边框的整体灰蒙效果是最经济的方式。十一、可演进方向ProfileTab已经是一个相当完整的个人中心但还有以下可演进方向签到日历当前是单次按钮可以升级为7 天签到日历连续签到额外奖励经验。徽章点击查看详情点击未解锁徽章弹层展示解锁条件如再发起 5 次拼单即可解锁。菜单项跳转当前菜单点击只弹 toast可以接入路由跳转到对应子页面。头像上传当前是 emoji 头像可以接入图片选择器让用户上传真实头像。设置页面从菜单项设置跳转到完整的设置页主题切换、消息推送、隐私设置等。数据可视化升级当前 WeekChart 是简单的条形图可以拓展为日 / 周 / 月维度切换或加入更多维度的趋势图。十二、写在最后回到开篇的两张真机截图——从顶部渐变用户卡橙红 → 金黄“张同学” Lv5到 4 栏统计叠卡28 / ¥386 / 96% / 15再到 6 个成就徽章3 解锁 / 3 未解锁和本周活跃度条形图最后到 6 个带 badge 的菜单项整张我的页用约 235 行 ArkTS 代码构建出了一个既完整又有品牌感的个人中心。它最值得借鉴的设计有 4 处没有 Header用 UserCard 兼任渐变背景延伸至状态栏让整个页面更沉浸。StatRow 向上叠加margin({ top: -28 })让统计卡挂在用户卡下方形成国内 App 常见的叠加层次。徽章的多重视觉反馈用opacitybackgroundColorborder 文字色 4 重差异区分已解锁 / 未解锁让奖励机制非常醒目。菜单项的 Badge 模式用showBadge: booleanbadgeColor接口字段让菜单项能动态显示5“NEW”3等多种角标是通知中心类设计的基础模式。如果你也在做 HarmonyOS 个人中心页不妨从渐变 叠加 徽章 菜单这四个组件开始搭骨架。当你把我的页打磨到这种程度整个 App 的品牌感就已经立住了。