
鸿蒙原生应用 ArkUI 声明式实战志愿服务我的页等级经验条与成就墙组合技App 17「志愿服务」我的 TabProfileTab是个人中心页。整页用渐变用户卡片小明同学 Lv.4 距 Lv.5 还需 120 经验 74% 经验条 4 栏彩色统计历史订单 28/进行中 3/满意度 98%/连续天数 15 本周使用条形图20 次/周六高亮 6 徽章成就墙4 解锁 2 未解锁 5 项菜单我的收藏带 NEW 角标 About。本篇基于17-volunteer/entry/src/main/ets/pages/ProfileTab.ets约 226 行逐段拆解附 4 张实机截图。一、整体结构6 个 Builder 的红色个人中心ProfileTab 的结构与 App 13 我的页完全同源甚至0 个 State——渐变用户卡 4 栏统计 周图 成就墙 菜单列表 Aboutbuild() { 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 个块顺序UserCard身份→ StatRow数据→ WeekChartCard行为图→ AchievementWall成就→ MenuList资产→ About关于。space: 0让 UserCard 和 StatRow 紧贴——UserCard 渐变 StatRow 卡片 margin({ top: -28 })让 StatRow 上浮到 UserCard 底部形成卡片叠在用户卡上的悬浮效果Material Design 的 elevation。与 App 13 我的页完全同源——6 块顺序、布局、负边距悬浮完全一致。唯一差异主题色紫→红 文案音乐→志愿。项目源码开源https://gitee.com/codenestFlow/HarmonyOSHub二、UserCard渐变 Lv.4 经验条 签到UserCard 是整页的个人门面——Stack双层头像/等级徽章 姓名/认证/Slogan 经验条 签到/数据报告双按钮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 }) { 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 }) }Stack({ alignContent: Alignment.BottomEnd })让 Lv.4 标签贴在头像右下角底层60×60 头像块半透明白 2vp 白色描边 emoji上层Lv.4 标签C.warn橙色底 白字 1.5vp 白边注意 Lv.4 徽章是橙色C.warn而不是主色红C.primary——等级徽章用警示色形成显著标记在红色背景的渐变上橙色比红色更跳。60×60 圆角 20——半透明白rgba(255,255,255,0.35) 2vp 白色描边rgba(255,255,255,0.7)——在红色渐变上若隐若现。2.2 姓名 认证 Slogan姓名小明同学 18sp 加粗白字认证认证 小胶囊半透明白底 白字Slogan活跃用户 · 已使用 28 天11sp 白字 85% 透明小明同学是系列虚拟角色名App 13 我的页也是这个名字——同一套 demo 用了统一的虚拟用户小明。真实项目里每个 App 有自己的用户。2.3 经验值 进度条双通道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)距 Lv.5 还需 120 经验74% 进度条——双通道表示成长进度。进度条 背景25% 透明白 前景100% 白、74% 宽——白色进度条在红色渐变上高对比。width(140)限制最大宽度 140vp。74%暗示还差 26% 升级——**具体数字120 经验 抽象百分比74%**双通道让用户既知道还差多少也看到走了多远。2.4 签到/数据报告双按钮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 })两个伪按钮背景 文字 emoji——rgba(255,255,255,0.2)半透明白底在红色渐变上。中间Blank()撑开。签到点击弹 Toast 签到成功 5 经验——注意没有State checked与 App 14/15/16 不同App 13/17 的签到只弹 Toast 不改状态。伪签到——demo 简化版不实现真正的签到状态翻转。2.5 红色 135° 渐变.width(100%).linearGradient({ angle: 135, colors: [[C.primary, 0.0], [C.accent, 1.0]] })#EF4444 → #F87171135° 渐变——左上鲜红、右下浅红营造光从左上洒下的视觉。App 13 是紫色 8B5CF6 → A78BFAApp 17 是红色 EF4444 → F87171——同一套 135° 渐变结构换主题色。三、StatRow4 栏统计 悬浮效果StatRow 是 4 个统计数字历史订单 28/进行中 3/满意度 98%/连续天数 15Builder 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橙色坚持天数**margin({ top: -28 })负边距让 StatRow 上浮到 UserCard 底部——视觉上叠在用户卡上。width(92%)比父容器窄 8%左右各留 4%——缩进卡片**让 StatRow 显得嵌在 UserCard 下。Divider().vertical(true).height(32)竖向分隔线——每两个数字之间用 1vp 浅灰竖线分隔。if (idx this.stats.length - 1)防止最后一条也有分隔线。alignItems(VerticalAlign.Center)让 4 个数字在 76vp 高度内垂直居中。四、WeekChartCard本周使用条形图周六高亮WeekChartCard 与首页同构7 天条形图汇总改为20 次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 }) }数据2/3/1/4/3/5/2——周六idx5value5 最高周三idx2value1 最低。b.value / 5 * 80归一化最大值 5 → 80vp——让最高柱顶到 80vp 上限。周六高亮是写死的idx 5与首页 App 13 同款首页selectedBar 4是动态的本页是写死的——动态 vs 静态高亮的两种实现。汇总20 次2314352 20——汇总严格等于各柱之和App 14 385 严格自洽、App 16 -3,700 不一致App 17 同样严格自洽。五、AchievementWall6 徽章成就墙4 解锁AchievementWall 是 6 个徽章的横向 Row一行 6 个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 }) }6 个公益徽章 初次使用解锁— 第一次下单 活跃用户解锁— 使用 7 天⭐ 好评达人解锁— 5 星好评 10 次 省钱能手解锁— 节省 100 元 资深用户未解锁— 使用 100 天 完美用户未解锁— 满意度 100%注意省钱能手——志愿服务没有省钱概念但徽章是通用的节省 100 元可能是为公益节省成本 100 元。徽章名 业务语义的弱关联——App 13 我的页也用同样的 6 个徽章。这是模板换肤的副作用徽章文案没换但页面主题已换。真实项目应让徽章名跟随业务调整如志愿时长 100 小时取代省钱 100 元。解锁/未解锁三件套背景/透明度/文字.backgroundColor(item.unlocked ? C.primarySoft : C.cardSoft) .opacity(item.unlocked ? 1 : 0.4)解锁浅红背景 100% 不透明 深色文字未解浅灰背景 40% 透明灰蒙感 灰色文字opacity: 0.4关键——比单纯换背景色更直接传达未解锁。4/6动态计算用.filter().length——this.achievements.filter(a a.unlocked).length实时算出已解锁数。动态 vs 硬编码App 13 我的页同款用了动态计算App 14/15 用了硬编码之前文章也指出了这是 App 14/15 的回退。App 17 用动态计算更好。六、MenuList5 项菜单 NEW 角标MenuList 是 5 个菜单项历史订单/消费统计/提醒设置/我的收藏/设置的纵向列表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) }5 项菜单 历史订单 — 查看所有历史记录 消费统计 — 月度数据分析 提醒设置 — 订单状态通知⭐ 我的收藏 — 常用列表NEW 角标⚙️ 设置 — 账号与隐私NEW 角标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) }NEW →C.ok绿色清新新功能数字 →C.danger红色待办提醒if (m.badge.length 0)条件渲染——空字符串视为无角标。Divider().color(C.stroke).margin({ left: 58 })——分隔线不贯穿全宽从图标右侧开始iOS 风格。if (idx this.menus.length - 1)防止最后一条也加分隔线用 idx 判断比 m.id length 可靠。.clip(true)整张菜单卡的圆角对内部 Divider 也生效——父容器圆角 子内容裁剪的标准做法。七、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.0HarmonyOS 6.0 · ArkTS · API 23底部版本信息10-12sp 灰色居中。padding({ top: 24 })让 About 与上方 MenuList 拉开 24vp 间距。注意 About 只有 2 行与 App 14/15/16 的 3 行不同少了版权 © 2024——这是 App 13 我的页的简化版。八、State 数量 0与 App 13 同款ProfileTab0 个 State——所有数据private不可变。与 App 14/15/16 的 1 个checked不同——App 17 完全没有交互签到只弹 Toast 不改状态。4 个 Tab 的 State 分布页面State 数量类型首页2可交互活动5表单记录1筛选我的0纯展示State 0与 App 13 同款个人中心纯展示——App 13 也是 0 个App 14/15/16 是 1 个。0 State 个人中心是 App 13/17 这类模板复刻的副作用——签到交互被简化。State 数量 交互点数规律0 纯展示、1 简单筛选/签到、5 表单。模板复刻的 0 State 版本是合理的简化——读者想看更完整的签到交互可以参考 App 14/15/16。九、个人中心的红色调性App 17 整个个人中心用红色#EF4444主导——UserCard 渐变红→浅红传达公益热情StatRow 数字历史订单红、进行中浅红WeekChartCard 柱子红色渐变与首页同款AchievementWall 已解锁徽章浅红背景MenuList 图标块浅灰C.cardSoft保持简洁红色 调性贯穿UserCard最显眼→ StatRow数字→ 周图行为→ 成就已解锁→ 菜单资产。5 个层次都用红色用户从任何一个模块都感受到志愿红的品牌。与 App 14/15/16 的个人中心对比App主题用户等级签到状态13 音乐紫 #8B5CF6小明Lv.4伪0 State14 运动绿 #16A34A张运动Lv.5101 State15 环保绿 #22C55E李环保Lv.651 State16 商城玫红 #F43F5E王积分Lv.7201 State17 志愿红 #EF4444小明Lv.4伪0 StateApp 17 与 App 13 完全一致小明/Lv.4/伪签到/0 State——再次印证模板复刻。对比 App 14/15/16 的渐进Lv.5→6→7、签到 10→5→20、1 StateApp 17 退回了最简化版本。十、总结本系列个人中心模板的 5 次复用回顾系列 5 个个人中心App 13/14/15/16/176 个 Builder 块完全同源UserCard/StatRow/WeekChartCard/AchievementWall/MenuList/About维度App 13App 14App 15App 16App 17主题色紫绿绿玫红红用户小明张运动李环保王积分小明等级Lv.4Lv.5Lv.6Lv.7Lv.4经验74%64%62%57%74%签到伪10520伪统计通用运动环保商城通用成就6 通用6 运动6 环保6 商城6 通用菜单56665State011105 次复用展示了 ArkUI 个人中心模板的极致稳定性——**6 个 Builder 1 个 Scroll 容器**承载了任意主题/业务/数据。真实项目套用建议复制任意一个 App 的 ProfileTab 源码改 Theme.ets 的 3 个颜色primary/accent/primarySoft改 4 个数据数组stats/weekBars/achievements/menus 3 个标量值用户名/经验/等级改 Slogan 文案改 README 的 App 名/bundleName5 步换肤 5 分钟生成一个全新个人中心——这是 ArkUI 模板设计的最高境界。十一、徽章的业务语义重写与 App 13 不同的建议App 17 复用了 App 13 的 6 个徽章省钱能手完美用户等但这些徽章与志愿服务的业务语义不匹配——志愿者不省钱、不消费。真实项目应重写徽章App 13 徽章App 17 应改为 初次使用 初次参加 活跃用户 活跃志愿者⭐ 好评达人⭐ 服务明星 省钱能手 服务100h时长 资深用户 资深志愿者 完美用户 公益大使徽章名 业务成就——徽章应该让志愿者一眼读懂我达成了一个什么成就而不是省钱 100 元这种电商逻辑。模板复刻不是完全照抄——数据/文案要跟随业务调整否则会出现省钱能手出现在志愿 App 这种违和感。真实项目套用模板时必改业务字段用户名/徽章名/统计项/菜单/周图数据。改进建议复制 App 13 我的页源码改 Theme.ets 3 个颜色primary/accent/primarySoft改 4 个数据数组 3 个标量值注意徽章名要符合新业务改 Slogan 文案小红帽/小志愿者/小公益人改 About 版本号Version 1.0.0 → 新版本5 步换肤 业务适配 5 分钟生成新应用。