ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

HarmonyOS 6.0实战:音乐歌单我的页 —— 经验条与成就徽章墙的组合技

HarmonyOS 6.0实战:音乐歌单我的页 —— 经验条与成就徽章墙的组合技 HarmonyOS 6.0实战音乐歌单我的页 —— 经验条与成就徽章墙的组合技App 13「音乐歌单」我的 TabProfileTab是个人中心页。整页用渐变用户卡片Lv.4 等级 74% 经验条 签到/数据报告 4 栏彩色统计 本周使用条形图周六高亮 6 徽章成就墙4 解锁 2 未解锁 5 项菜单带 NEW 角标 About 信息组成是 ArkUI 个人中心页的完整范本。本篇基于13-music-playlist/entry/src/main/ets/pages/ProfileTab.ets约 225 行逐段拆解附 4 张实机截图。一、整体结构6 个 Builder 拼接ProfileTab整个组件用 6 个 Builder 块拼接成完整的个人中心页build() { 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 个块按用户 → 数据 → 周图 → 成就 → 菜单 → 关于的叙事顺序展开与首页 HomeTab 的头部 → 工具 → 数据 → 筛选 → 列表不同ProfileTab 是人 → 数据 → 行为 → 资产 → 设置 → 关于的纵向叙事。整页用 Scroll 容器与 HomeTab 一致因为内容超过一屏必须可滚。space: 0让 UserCard 和 StatRow 紧贴——这是有意为之UserCard 渐变 StatRow 卡片 margin({ top: -28 })让 StatRow 上浮到 UserCard 底部形成卡片叠在用户卡上的视觉效果Material Design 风格的 elevation。padding({ bottom: D.pad this.safeBottom 20 })给 Scroll 内容底部留出 16vp 安全区 20vp 缓冲避免关于信息贴到 Tab 栏底部。UserCard是整页唯一一个不滚出视口始终可见的卡片虽然它在 Scroll 内但因为最顶部让用户随时看到自己的等级和经验。项目源码开源https://gitee.com/codenestFlow/HarmonyOSHub二、UserCard渐变用户卡 Lv.4 徽章 经验条 签到/数据报告UserCard 是整页的个人门面4 个信息层叠在紫色渐变上头像 Lv.4 徽章、姓名 认证标签、活跃天数 距下一级经验、签到按钮 数据报告按钮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 })让 Lv.4 标签贴在头像右下角底层60×60 头像块半透明白色背景 (rgba(255,255,255,0.35)) 2vp 白色描边上层Lv.4 标签橙底 (C.warn) 白字 1.5vp 白边Stack的alignContent(Alignment.BottomEnd)让上层对齐右下正好把 Lv.4 标签定位到头像的右下角形成角色等级徽章的经典样式游戏 App 常用。2.2 经验值 进度条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)Stack 进度条 背景 前景叠在同一位置背景100% 宽 4vp半透明白色 (rgba(255,255,255,0.25))前景74% 宽 4vp纯白 (#FFFFFF)外层.width(140)限制进度条最大宽度 140vp74% 表示当前经验占 Lv.5 所需总经验的比例120 经验是还差 120 经验升 Lv.5。这两个数字74% / 120一起描述成长进度比单一百分比更清晰。2.3 签到 数据报告双按钮Row()内左右各一个伪按钮背景 文字 emoji每日签到左 文字半透明白底数据报告右 文字半透明白底中间Blank()撑开。两者都点击弹 Toast——签到成功 5 经验和数据报告。签到 5 经验是游戏化机制用户每次签到都获得经验值反馈激励每天打开 App。这是社区留存的核心机制。2.4 紫色渐变.linearGradient({ angle: 135, colors: [[C.primary, 0.0], [C.accent, 1.0]] })与首页 HeroCard 同样的 135° 渐变C.primary → C.accent。UserCard在页面最顶部紫色延伸至状态栏背后padding.top: this.safeTop 30让出安全区形成顶部沉浸式紫色块。三、StatRow4 栏彩色统计数字StatRow 是 4 个数字历史订单 28 / 进行中 3 / 满意度 98% / 连续天数 15横排每个数字有自己的颜色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 }) }4 色映射历史订单 28 →C.primary紫进行中 3 →C.accent浅紫满意度 98% →C.ok绿连续天数 15 →C.warn橙width(92%)比父容器窄 8%让卡片左右两侧各留 4% 空间——这是让 StatRow 缩进 卡片内层视觉上嵌在 UserCard 下。配合margin({ top: -28 })整张 StatRow 上浮 28vp与 UserCard 底部重叠 28vp形成卡片叠在用户卡上的悬浮效果。alignItems(VerticalAlign.Center)让 4 个数字在 76vp 高度内垂直居中比靠顶更平衡。Divider().vertical(true).height(32)是竖向分隔线每两个数字之间用 1vp 浅灰竖线分隔。if (idx this.stats.length - 1)防止最后一条也有分隔线。注意Divider().vertical(true)的语法是 ArkUI 14 才支持的垂直分隔线比手动Column().width(1).height(32).backgroundColor(C.stroke)更简洁。四、WeekChartCard本周使用条形图周六高亮WeekChartCard 是本周使用的可视化条形图与首页 WeekChartCard 类似但默认高亮周六idx5Builder 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 }) }与首页 WeekChartCard 的差异差异点首页 HomeTab我的 ProfileTab标题 本周数据 本周使用总数30 次20 次数据3,5,2,6,4,7,32,3,1,4,3,5,2最高柱默认高亮idx4五value4idx5六value5高度计算b.value / 7 * 80b.value / 5 * 80可点击是onClick切换高亮否静态b.value / 5 * 80vsb.value / 7 * 80我的页最大值是 5周六所以分母用 5让周六柱顶到 80vp 满高。首页最大值是 7分母用 7。两页都按当前数据最大值做归一化让最高柱永远是 80vp给视觉一致感。周六高亮的隐喻周六是周末/休息日用户有更多时间使用 App所以本周使用峰值在周六符合用户心理预期让图表讲一个用户能理解的故事——这是数据可视化的高阶原则。不可点击的差异首页的 WeekChartCard 是可交互点击切高亮我的页的 WeekChartCard 是纯展示。差异源于用户角色——首页是看数据想探索我的页是看自己确认状态。同一组件在不同上下文用不同交互模式是 ArkUI 声明式 UI 的灵活性体现。五、AchievementWall6 徽章成就墙AchievementWall 是 6 个徽章横向排列的成就墙4 个解锁 2 个未解锁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 }) }解锁/未解锁双态实现 3 件套图标块背景unlocked ? C.primarySoft : C.cardSoft— 解锁浅紫未解浅灰整体透明度opacity(unlocked ? 1 : 0.4)— 解锁 100%未解 40%关键让未解锁有灰蒙感文字色unlocked ? C.text : C.textDim— 解锁深色未解灰色opacity: 0.4是关键的未解锁标记——比单纯换背景色更直接传达这个还没拿到。6 个徽章的数据编排 初次使用已解锁第一次下单 活跃用户已解锁使用 7 天⭐ 好评达人已解锁5 星好评 10 次 省钱能手已解锁节省 100 元 资深用户未解锁使用 100 天 完美用户未解锁满意度 100%已解锁的 4 个都是初级成就短期可达未解锁的 2 个都是高级成就长期挑战。这种近易远难的成就编排是游戏化设计的关键——让用户一开始就能拿到几个徽章建立成就感同时有高级徽章作为长期目标。this.achievements.filter(a a.unlocked).length动态计数右上角 4/6 通过.filter()实时计算已解锁数数据更新时自动重渲染因为 .filter 返回新数组每次都重新求值。但这里没State achievementsachievements是private不可变数据所以.filter()只在初始化时算一次。如果未来要解锁新成就后数字自动 1需要把achievements改为 Statefilter 才会响应式重算。Text(item.desc).maxLines(1)描述文字第一次下单限制 1 行超出省略号。maxLines(1)是 ArkUI 的行数限制属性避免长描述撑破卡片布局。六、MenuList5 项菜单收藏带 NEW 角标MenuList 是 5 个菜单项的纵向列表收藏项右上角带NEW绿色角标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) }几个细节第一if (m.badge.length 0)条件渲染。m.badge字段可能是空字符串或NEW长度 0 时才显示角标。这种空字符串作为无角标的占位避免引入额外的 boolean 字段。第二badge NEW ? C.ok : C.danger未来如果增加其他 badge如 HOT可设为红 (C.danger)。当前只有 NEW 一种所以都是绿色。第三Divider().color(C.stroke).margin({ left: 58 })每两个菜单项之间用 1vp 浅灰线分隔。margin.left: 58让分隔线不贯穿整个卡片宽度而是从图标块文字的右侧开始——这是 iOS 设置项的经典样式分隔线只覆盖右侧部分视觉上更精致。第四.clip(true)让整张菜单卡的圆角 (D.rMd) 对内部 Divider 也生效——如果 Divider 比父容器宽会被 clip 裁剪到圆角内。这是 ArkUI 里父容器圆角 子内容裁剪的标准做法。5 个菜单项的编排历史订单 / 消费统计 / 提醒设置 / 我的收藏 (NEW) / 设置——按用户使用频次从高到低排序收藏放第 4 位因为有 NEW 引导用户最容易忽略的次重要功能靠角标引流。七、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.0和HarmonyOS 6.0 · ArkTS · API 23是底部关于信息10-12sp 灰色居中。padding({ top: 24 })让 About 与上方 MenuList 拉开 24vp 间距**段间空隙大于段内空隙**的视觉节奏段间 24vp 卡片间距 14vp。alignItems(HorizontalAlign.Center)让两行文字水平居中。八、State 的克制整页只有 0 个 State——所有数据都是private字段stats/menus/achievements/weekBars且本页没有任何交互WeekChartCard 不可点击、MenuList 点击只弹 Toast、AchievementWall 不可交互。State 数量 0反映了一个事实个人中心页是展示型页面主要功能是展示用户信息而非接收用户操作。这与 Func1Tab5 个 State 表单输入和 Func2Tab2 个 State 筛选/搜索形成对比。如果未来加交互如切换主题色开关、签到按钮更新经验值就新增 State。先看是否需要交互再决定是否加 State是健康的状态管理习惯。九、设计哲学紫色品牌 个人身份整页用紫色 (#8B5CF6) 主导UserCard渐变紫底StatRow紫色数字历史订单WeekChartCard紫色渐变柱子AchievementWall浅紫底已解锁徽章MenuListNEW 角标绿色与紫色互补这种主色 1 个辅色的配色比彩虹配色高级得多——让整页气质统一不花哨。个人中心的 4 个内容层身份层UserCard— 我是谁、我的等级、我的活跃度数据层StatRow / WeekChartCard— 我的使用数据成就层AchievementWall— 我达成过什么资产层MenuList— 我能做什么4 层结构把自我认知身份→ 行为回顾数据→ 行为激励成就→ 行动入口资产的闭环讲清楚。这是个人中心页的通用骨架适用于任何个人主页场景音乐 App、阅读 App、运动 App、社交 App。十、可访问性与扩展可访问性上进度条配 74% 文字色弱用户不靠颜色识别徽章配已解锁/未解锁opacity 文字色色弱用户能靠颜色深浅识别菜单项有标题 副标题屏幕阅读器能读出完整信息扩展方向签到按钮可以加State checked: booleanState exp: number点击翻转 checked 并 exp 5主题切换可以加State isDark: boolean影响 Theme.ets成就解锁可以监听exp变化达到阈值时把achievements[i].unlocked true菜单项点击应跳到对应页面订单页 / 统计页 / 设置页而非弹 Toast
返回列表