ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

鸿蒙原生实战:用 ArkUI 搭音乐歌单首页 —— 可点击条形图与推荐列表

2026/9/4 1:04:09 拓冰建站 浏览量
鸿蒙原生实战:用 ArkUI 搭音乐歌单首页 —— 可点击条形图与推荐列表 鸿蒙原生实战用 ArkUI 搭音乐歌单首页 —— 可点击条形图与推荐列表App 13「音乐歌单」首页HomeTab主题色#8B5CF6紫色4 个 Tab 分别为首页、歌曲、歌单、我的。首页采用沉浸式渐变 HeroCard 三栏数字统计 快捷功能入口 本周数据条形图点击切换高亮 横向分类筛选 推荐列表带 HOT/NEW/精选标签和进度条。本篇基于源码13-music-playlist/entry/src/main/ets/pages/HomeTab.ets约 194 行逐段拆解附 4 张实机截图。一、整体结构Scroll 容器 5 个 Builder 块HomeTab 整个组件就是把 5 个 Builder 拼接起来的瀑布流外层用 Scroll 容器装下所有内容配合safeBottom在最底部留出 Tab 栏避让空间。代码骨架非常直白Component export struct HomeTab { StorageProp(safeTop) safeTop: number 0; StorageProp(safeBottom) safeBottom: number 0; State activeCat: number 0; State selectedBar: number 4; // ... build() { Column() { Scroll() { Column({ space: 14 }) { this.HeroCard() this.QuickActionRow() this.WeekChartCard() this.CategoryBar() this.ItemList() Blank().height(this.safeBottom 20) } .width(100%).padding({ left: D.pad, right: D.pad }) } .layoutWeight(1).scrollBar(BarState.Off).align(Alignment.Top) } .width(100%).height(100%).backgroundColor(C.bg) }5 个块按头部 → 工具 → 数据 → 筛选 → 列表五段叙事展开HeroCard情感化欢迎 3 个关键数据→QuickActionRow4 个常用功能入口→WeekChartCard本周数据可视化→CategoryBar横向分类筛选→ItemList推荐列表。每个块独立成 Builder调试时可以单独看一个块渲染单元测试和代码 review 都很友好。Blank().height(this.safeBottom 20)是底部留白让最后一张卡片不会贴到 Tab 栏上。safeBottom来自EntryAbility注入到AppStorage的导航条高度加 20vp 的视觉缓冲——这是沉浸式全屏的补丁标准写法。scrollBar(BarState.Off)关掉右侧的滚动条指示器整页显得更干净align(Alignment.Top)让 Scroll 始终从顶部开始显示不会因为内容不足而居中。项目源码开源https://gitee.com/codenestFlow/HarmonyOSHub二、HeroCard紫色渐变 沉浸式 三栏统计HeroCard是首页的门面把安全区顶部、图标、标题、副标题、3 个关键数字塞进一张紫色渐变卡里Builder HeroCard() { Column({ space: 12 }) { Row({ space: 12 }) { Text().fontSize(24) Column({ space: 4 }) { Text(智能管理).fontSize(20).fontWeight(FontWeight.Bold).fontColor(#FFFFFF) Text(高效便捷 · 智能推荐).fontSize(12).fontColor(rgba(255,255,255,0.85)) }.alignItems(HorizontalAlign.Start).layoutWeight(1) }.width(100%) Row() { ForEach(this.stats, (s: StatItem, idx: number) { Column({ space: 2 }) { Text(s.value).fontSize(20).fontWeight(FontWeight.Bold).fontColor(#FFFFFF) Text(s.label).fontSize(10).fontColor(rgba(255,255,255,0.8)) }.alignItems(HorizontalAlign.Start).layoutWeight(1) if (idx this.stats.length - 1) { Column().width(1).height(28).backgroundColor(rgba(255,255,255,0.25)) } }, (s: StatItem) s.label) }.width(100%) } .width(100%).padding(18).padding({ top: this.safeTop 20 }) .borderRadius({ bottomLeft: D.rLg, bottomRight: D.rLg }) .linearGradient({ angle: 135, colors: [[C.primary, 0.0], [C.accent, 1.0]] }) }几个关键设计点第一padding({ top: this.safeTop 20 })双重 padding 写法。先padding(18)给四面 18vp再padding({ top: this.safeTop 20 })单独覆盖顶部为safeTop 20状态栏高度 20vp 视觉缓冲。这样既保证左右下三边距对称又让顶部 padding 跟随状态栏高度自适应。比起padding({ top: this.safeTop 20, left: 18, right: 18, bottom: 18 })这种四值全写更简洁。第二borderRadius({ bottomLeft: D.rLg, bottomRight: D.rLg })圆角不对称。只圆角底部的两个角让 HeroCard 顶部与状态栏无缝拼接视觉上延伸到状态栏背后底部则形成大圆角与下方白色卡片做软分隔。这是沉浸式 HeroCard 的标准手法上不留白下大圆角。如果四个角都圆角顶部会出现一条空隙破坏沉浸感。第三linearGradient({ angle: 135, colors: [[C.primary, 0.0], [C.accent, 1.0]] })135° 斜向渐变。C.primary(#8B5CF6 主紫) 到C.accent(#A78BFA 浅紫) 从左上往右下渐变营造光从左上洒下的视觉。135° 是大多数 App 渐变的默认角度因为人眼习惯光从左上。第四三栏统计用layoutWeight(1)等分 间隔Column().width(1)竖线。if (idx this.stats.length - 1)防止最后一条也加分隔线——这是分隔符的标准写法硬编码if-else数组长度容易出错。rgba(255,255,255,0.25)是 25% 不透明白色在紫色背景上比纯白柔和得多。stats数据是硬编码的[{ value: 128, label: 总数 }, { value: 32, label: 今日 }, { value: 86%, label: 完成率 }]三个数字组成总量 / 增量 / 质量三角配合智能管理 · 高效便捷 · 智能推荐文案把首页的定位数据驱动 智能推荐讲清楚。三、QuickActionRow4 个圆形入口的工具栏QuickActionRow把 4 个常用功能搜索 / 收藏 / 统计 / 消息压缩成一条横向入口带是首页的快捷导航Builder QuickActionRow() { Row() { ForEach(this.actions, (item: QuickAction) { Column({ space: 6 }) { Row() { Text(item.icon).fontSize(22) } .width(44).height(44).backgroundColor(C.cardSoft).borderRadius(D.rSm).justifyContent(FlexAlign.Center) Text(item.title).fontSize(10).fontColor(C.textSub) } .layoutWeight(1) .onClick(() { promptAction.showToast({ message: item.title }); }) }, (item: QuickAction) item.title) } .width(100%).padding(12).backgroundColor(C.card).borderRadius(D.rMd) .border({ width: 1, color: C.stroke }).justifyContent(FlexAlign.SpaceAround) }每个入口由44×44 圆角图标块 下方 10sp 文字组成4 个等宽分布。Row().width(44).height(44)套一层是为了精确控制图标块的尺寸不然 Text 直接设背景不会撑出 44 高度borderRadius(D.rSm)给图标块小圆角。.justifyContent(FlexAlign.SpaceAround)让 4 个入口之间留出等距空白整条横向带视觉均衡。promptAction.showToast({ message: item.title })是个值得记住的小技巧——给任意点击事件一个轻反馈。如果直接 onClick 留空用户的点击没有反应会以为应用卡了用 Toast 立刻弹出搜索收藏等文字既给反馈又演示这个按钮会跳到对应页面。在原型/demo 阶段这种伪跳转反馈能让应用感觉是完整的。State activeCat: number 0和selectedBar: number 4是整页唯二的可交互状态——activeCat决定分类条哪个高亮selectedBar决定周数据图哪一根柱子高亮默认 4 即五。四、WeekChartCard可点击的条形图与渐变映射WeekChartCard是首页的数据可视化模块把一周 7 天的数据画成 7 根渐变条形图点击柱子能切换高亮Builder WeekChartCard() { Column({ space: 12 }) { Row() { Text( 本周数据).fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text) Blank() Text(30 次).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 / 7 * 80)) .borderRadius(4) .linearGradient({ angle: 180, colors: idx this.selectedBar ? [[C.primary, 0.0], [C.accent, 1.0]] : [[C.primarySoft, 0.0], [C.primary, 1.0]] }) Text(b.day).fontSize(10) .fontColor(idx this.selectedBar ? C.primary : C.textDim) .fontWeight(idx this.selectedBar ? FontWeight.Bold : FontWeight.Normal) }.layoutWeight(1).alignItems(HorizontalAlign.Center) .onClick(() { this.selectedBar idx; }) }, (b: WeekBar) b.day) }.width(100%) } .width(100%).padding(14).backgroundColor(C.card).borderRadius(D.rMd) .border({ width: 1, color: C.stroke }) }7 根柱子每根 18vp 宽柱子高度 b.value / 7 * 80。value是 2-7 的整数数据里最大值 7所以最高柱 7/78080vp最矮柱 2/780≈22.9vp。Math.max(6, ...)兜底最低高度 6vp避免 value0 时柱子消失。条形图最大高度 80vp 给视觉上限整图高度约 80 6 10 96vp。点击交互通过onClick(() { this.selectedBar idx; })改变selectedBar触发整页 ArkUI 重渲染。柱子有两种渐变色选中的柱子用[C.primary, C.accent]主紫→浅紫的激活色未选中的用[C.primarySoft, C.primary]浅紫→主紫的未激活色。180° 渐变垂直方向让亮紫色在顶部深紫色在底部——视觉上像柱子从亮处发出。文字b.day一/二/三/四/五/六/日也跟随高亮变色选中时主紫加粗未选中时灰色常规。颜色 字重双通道标记选中态比单靠颜色更易识别色弱用户友好。右上角30 次是数据汇总3526473 算下来 30用户一眼看到这周总共 30 次——这是数据可视化的summary 数字惯例。weekBars数据故意让五(value4) 成为默认选中selectedBar 4并让六(value7) 是最高柱——周五是工作日尾声周六是峰值对应周五摸鱼、周六大干的使用习惯。这种数据编排有故事性是 demo 设计的细节真实项目里应根据真实数据排布。五、CategoryBar横向 Scroll 分类筛选CategoryBar是 4 个分类标签全部 / 推荐 / 热门 / 最新横向排列的筛选条Builder CategoryBar() { Scroll() { Row({ space: 8 }) { ForEach(this.cats, (cat: string, idx: number) { Text(cat).fontSize(13) .fontColor(this.activeCat idx ? #FFFFFF : C.textSub) .backgroundColor(this.activeCat idx ? C.primary : C.card) .borderRadius(D.rSm).border({ width: 1, color: this.activeCat idx ? C.primary : C.stroke }) .padding({ left: 14, right: 14, top: 8, bottom: 8 }) .onClick(() { this.activeCat idx; }) }, (cat: string) cat) } } .scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off).width(100%) }横向 Scroll 容器装一行 Row 内的分类标签4 个分类的宽度自动撑开没有 layoutWeight靠 padding 控制文字宽度。ScrollDirection.Horizontal开启横向滚动——4 个分类当前是装得下的但 Scroll 包装让未来加到 8-10 个分类时不会破版。每个分类是Text 背景 边框 padding复合体选中态切换三种属性背景C.primaryvsC.card、文字色白 vs 灰、边框紫色 vs 浅灰。三处同步切换让激活感强烈。这种选中态三件套背景 文字 边框是分类标签的标准做法。onClick(() { this.activeCat idx; })是核心交互。当前 demo 没有联动ItemList的实际过滤items 是写死的 3 条真实项目应在点击时调用this.items filterByCategory(this.activeCat)让列表实时筛选。(cat: string) cat是 ForEach 的 keyGenerator用分类名当 key而不是下标。下标作为 key 在分类增删时会错位用稳定字符串作为 key 才能保证 ArkUI 正确复用节点。activeCat 1时文本色变化靠State自动重渲染ForEach 不需要任何额外代码。六、ItemList带 HOT/NEW/精选标签 进度条的推荐列表ItemList是首页的内容主体——3 条推荐项目每条都是图标 标题 描述 标签 进度条的复合卡Builder ItemList() { Column({ space: 10 }) { Row() { Text(推荐列表).fontSize(16).fontWeight(FontWeight.Bold).fontColor(C.text) Blank() Text(查看全部 ›).fontSize(11).fontColor(C.primary) }.width(100%) ForEach(this.items, (item: ListItem) { Column({ space: 10 }) { Row({ space: 12 }) { Row() { Text(item.emoji).fontSize(28) } .width(56).height(56).backgroundColor(C.cardSoft).borderRadius(D.rMd).justifyContent(FlexAlign.Center) Column({ space: 4 }) { Row() { Text(item.title).fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text).layoutWeight(1) Text(item.tag).fontSize(9).fontColor(#FFFFFF) .backgroundColor(item.tag HOT ? C.danger : item.tag NEW ? C.ok : C.accent) .padding({ left: 5, right: 5, top: 2, bottom: 2 }).borderRadius(D.rSm) }.width(100%) Text(item.desc).fontSize(12).fontColor(C.textDim) }.alignItems(HorizontalAlign.Start).layoutWeight(1) }.width(100%) Row() { Text(进度).fontSize(10).fontColor(C.textDim) Stack({ alignContent: Alignment.Start }) { Column().width(100%).height(5).backgroundColor(C.cardSoft).borderRadius(3) Column().width(${item.progress}%).height(5).borderRadius(3).backgroundColor(C.primary) }.layoutWeight(1).margin({ left: 8, right: 8 }) Text(${item.progress}%).fontSize(10).fontColor(C.primary).fontWeight(FontWeight.Bold) }.width(100%) } .width(100%).padding(12).backgroundColor(C.card).borderRadius(D.rMd) .border({ width: 1, color: C.stroke }) .onClick(() { promptAction.showToast({ message: item.title }); }) }, (item: ListItem) item.id.toString()) }.width(100%) }几个关键设计第一标签颜色三态映射item.tag HOT ? C.danger : item.tag NEW ? C.ok : C.accent。Hazard红、新绿、精选紫三色对应不同语义。C.accent浅紫和卡片主题色保持一致让精选标签不会显得突兀。第二Stack 进度条 背景条 前景条叠加。外层Stack({ alignContent: Alignment.Start })把两条 Column 重叠对齐到左边。背景条width(100%).height(5).backgroundColor(C.cardSoft)是浅灰前景条width(${item.progress}%).height(5).backgroundColor(C.primary)是紫色用百分比字符串控制宽度——这是 ArkUI 里实现百分比进度条的标准手法不需要LinearGradient或Progress组件。第三进度条百分比文字双通道同步Text(${item.progress}%)显示数字与.width(${item.progress}%)进度条宽度是同一个数据源item.progress自然同步。文字色C.primary加粗与进度条颜色一致视觉统一。第四Stack进度条 vsRow进度条。如果用Row需要外层 Row 里塞两个 Column第一个宽度 100%第二个宽度 60%然后靠marginLeft把第二条推到右边。Stack 更直接——两条 Column 重叠在 0 位置前景条靠宽度自己长出来。Stack 的优势是天然支持任意宽度叠加缺点是 Stack 本身有性能开销要算 z 序对 7-10 根进度条场景两者差不多复杂列表推荐用 Progress 组件。第五条目 key 用item.id.toString()。ListItem接口有id: number转字符串后传给 ForEach 第三个参数 keyGenerator。id 唯一稳定未来增删/重排列表时 ArkUI 能正确复用节点不闪烁。最后onClick(() promptAction.showToast(...))是和 QuickActionRow 一致的轻反馈策略——点击列表项弹出标题提示用户我收到了你的点击。七、Builder 拆分的工程意义整个 HomeTab 用 5 个 Builder 拆开每个 Builder 独立可读。这种拆法有三重好处1. 单元渲染测试。DevEco Studio 的 Previewer 可以单独预览某个 Builder把this.HeroCard()替换为HeroCard()就能只渲染头部。在大型项目里可以为每个 Builder 写独立的 snapshot 测试。2. 复用与重组。如果未来个人成就页也要 HeroCard可以把 HeroCard 抽成独立组件components/HeroCard.ets被两个页面 import。Builder 本身不导出但可以提到Component struct HeroCard形式支持跨文件复用。3. 协作边界。多人协作时头部 HeroCard 由 A 负责、推荐列表由 B 负责用 Builder 划分后 git diff 干净conflict 少。但 Builder 也有局限不能接受泛型、不能写复杂逻辑、不能挂载生命周期。如果某个 Builder 内有大量状态联动或异步逻辑建议升级为独立 Component 组件。八、State 状态管理的克制整页只有 2 个 StateactiveCat和selectedBar分别控制分类条高亮和周数据图高亮。没有多余的 State——stats/weekBars/items都是private不可变数据不需要响应式。这是 ArkUI 状态管理的最佳实践只把会变的状态放进 State。如果把所有数据都声明 State会让 ArkUI 误以为它们都可能变每次重渲染都做依赖追踪性能下降。**会变的只有交互态**这条原则让代码既简洁又高效。StorageProp(safeTop/safeBottom)是另一种读外部状态的方式——AppStorage是全局响应式存储EntryAbility 在 onWindowStageCreate 里 setOrCreate 这两个 key整个 App 任何组件都能用 StorageProp 订阅。本页用 StorageProp 而不是 State是因为安全区高度由系统决定App 内任何组件都不应该修改它——StorageProp 明确表达这是只读订阅语义。九、沉浸式全屏 状态栏文字色EntryAbility.ets里做了 3 件事实现沉浸式const win windowStage.getMainWindowSync(); win.setWindowLayoutFullScreen(true); win.setWindowSystemBarProperties({ statusBarContentColor: #1C2333, navigationBarContentColor: #1C2333 }); const top win.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM); const bottom win.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR); AppStorage.setOrCreate(safeTop, px2vp(top.topRect.height)); AppStorage.setOrCreate(safeBottom, px2vp(bottom.bottomRect.height));第一行setWindowLayoutFullScreen(true)让内容延伸到状态栏和导航条背后——这是沉浸式的前提。第二行设置状态栏/导航条文字色为#1C2333深色与首页 HeroCard 紫色形成对比。如果 HeroCard 是浅色头状态栏文字就该深如果是深色头状态栏文字就该白。状态栏文字色跟随顶部底色是沉浸式适配的黄金规则。getWindowAvoidArea拿到的是像素值需要px2vp转成 vp 才能在 ArkUI 布局里直接用。这两个值存到 AppStorage 后全局任何组件都能通过StorageProp(safeTop)订阅是鸿蒙生态系统能力 → 全局响应式状态的范式。十、配色与排版细节整页用了 5 个颜色家族紫色C.primary(#8B5CF6) /C.accent(#A78BFA) /C.primarySoft(#F0EBFE) 撑起品牌色文字C.text(#1C2333) /C.textSub(#5A6483) /C.textDim(#9AA3BC) 三级灰度撑起信息层级背景C.bg(#F2F5FB) /C.card(#FFFFFF) /C.cardSoft(#F4F6FB) 撑起空间层级。一种主色 三级灰 三级背景是设计系统最经济的71配色框架。D.rLg20 / D.rMd14 / D.rSm10三档圆角 D.pad16标准 padding 间距5 个块都是D.rMd圆角 D.pad间距整页节奏统一。这种少量 token 大量复用是设计系统建设的第一性原理。space: 14Scroll 内 Column 各块间距 14vpspace: 12卡片内行间距space: 6小元素间形成14 → 12 → 6三级节奏。所有数字都是 2 的倍数或 6 的倍数视觉规整。十一、可访问性与扩展建议可访问性上整页做到了颜色 文字双通道HOT 标签红色 HOT 文字、进度条紫色 75% 数字、选中分类紫色背景 文字加粗——色弱用户不靠颜色也能识别。但还可以更完善进度条加accessibilityHint让屏幕阅读器读出已完成 75%HeroCard 数字加accessibilityLabel避免读出128 32 86%等无语义串ForEach 的 key 全部是稳定字段id、day、title未来增删/重排不会闪烁扩展性上整页是瀑布流骨架未来可以给HeroCard接入真实userStatsAPI替换硬编码 stats给WeekChartCard接入真实weekData替换硬编码 weekBars给ItemList接入分页加载.onReachEnd()触发加载下一页把HeroCard/WeekChartCard/ItemList抽成components/下独立组件供多页复用到此App 13「音乐歌单」首页解析完毕。沉浸式紫色渐变 数据可视化 智能推荐三件套配合 5 个 Builder 拆分的工程化代码让一个 demo 应用同时具备视觉冲击和工程参考价值。