开篇:系列承接与本篇定位

        在上一篇实战中,我们完成了整个应用的底部 Tab 导航架构,搭建好了「推荐/发现/动态/我的」四大页面的切换框架。从本篇开始,我们将逐个填充页面内容,首当其冲的就是音乐 App 最核心的首页——推荐页。

        推荐页是用户打开 App 第一眼看到的页面,承载了搜索、广告轮播、个性化推荐、歌单入口等核心功能。本篇我们将从零到一完成推荐页的完整 UI 开发,全程遵循企业级开发规范,不仅讲「怎么写」,更讲「为什么这么写」,同时把开发过程中所有真实踩坑点全部复盘,帮你避开 90% 新手会遇到的问题。

你将从本篇学到

  • 移动端首页模块化拆分思路,掌握从设计稿到代码的布局拆解能力

  • 熟练运用 Row/Column/Swiper/List/Stack/Scroll 六大核心布局组件

  • 理解 TypeScript 接口强类型约束的价值,规范数据结构定义

  • 掌握 @Builder 组件复用技巧,告别冗余代码,提升可维护性

  • 吃透图片填充、圆角裁剪、文本溢出、横向列表等高频 UI 问题的解决方案

  • 完整掌握鸿蒙网络权限配置与 HTTP 明文请求放行流程

  • 学会排查组件导入、资源失效、解析报错等常见编译与运行问题

前置准备

  • 已完成前两篇的项目搭建与底部 Tab 导航开发

  • DevEco Studio 开发环境正常,可运行模拟器

  • 掌握 ArkTS 基础语法与声明式 UI 开发范式

一、页面整体设计与技术选型

1.1 页面模块拆分

        我们参考主流音乐 App 的首页结构,将推荐页从上到下拆分为四个独立模块,模块之间低耦合、可单独调试、方便后续迭代替换:

  1. 顶部搜索栏:全局搜索入口,搭配扫码功能,固定在页面顶部

  2. 轮播 Banner 区:运营广告位,自动轮播,支持手势滑动切换

  3. 每日推荐卡片区:个性化推荐入口,横向滚动卡片,差异化配色

  4. 推荐歌单列表区:歌单广场入口,封面叠加播放量,横向滚动展示

        整个页面外层包裹 Scroll 容器,实现整页垂直滑动,符合移动端信息流的交互习惯。

1.2 核心技术选型思考

        每个功能点都有多种实现方式,我们选择方案的核心原则是:性能优先、符合官方规范、便于后续扩展。

  • 横向滚动:List 优先于 Scroll+Row少量固定卡片时两者差异不大,但 List 内置组件复用机制,数据量增大时性能优势明显,且 API 更丰富,支持后续扩展分割线、分组等能力。本次直接采用 List 方案,为后续接口对接预留性能空间。

  • 轮播图:原生 Swiper 组件不用自定义 Scroll 封装轮播,原生 Swiper 自带手势适配、切换动画、循环播放、指示器等能力,稳定性更高,是官方推荐的 Banner 实现方案。

  • 数据层:Interface 强类型约束即使是模拟数据,也提前定义数据接口,一方面编译期校验字段,避免拼写错误;另一方面后续对接真实接口时,只需替换数据源,UI 代码无需改动。

  • 组件复用:@Builder 装饰器标题栏这类重复出现的 UI 片段,通过 @Builder 封装复用,统一维护样式,符合 DRY(不要重复自己)开发原则。

二、分步实现:从 0 到 1 搭建推荐页

        我们从上到下逐个模块实现,每一步都可单独预览调试,降低出错概率。所有代码最终都整合在 pages/home/Recommend.ets 文件中。

2.1 数据层封装:强类型接口定义

        开发UI前先规范数据结构,是工程化开发的基础。通过TS接口约束卡片、歌单数据格式,让UI与数据精准对应。

// 每日推荐卡片数据类型约束
// 作用:统一所有推荐卡片的数据结构,编译期校验字段,代码自动提示,规避数据错乱问题
interface recommendDailyType {
  img: string,    // 网络图片地址:卡片封面背景图
  title: string,  // 卡片底部简介文字:推荐内容描述
  type: string,   // 卡片顶部标题文字:分类标签(每日推荐/私人漫游等)
  top: string,    // 顶部标题背景色:十六进制配色
  bottom: string  // 底部简介背景色:十六进制配色
}

// 推荐歌单数据类型约束
// 作用:规范歌单列表数据格式,适配横向歌单卡片UI
interface RecommendListType {
  img: string;     // 歌单封面图网络地址
  title: string;   // 歌单名称文案
  count: string;   // 歌单播放量展示文案
}

2.2 全局数据源配置(官方原生阿里云资源)

        全程使用课程配套原生OSS图片资源,无需替换、无需修改,适配项目原生配置,保证与课程案例完全一致。

@Component
export struct Recommend {
  // 轮播图数据源:官方原生Banner图片资源
  swiperList: string[] = [
    "http://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/HeimaCloudMusic/banner1.png",
    "http://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/HeimaCloudMusic/banner3.png"
  ]

  // 每日推荐卡片数据源:原生推荐图+配套文案+差异化配色
  dailyRecommend: recommendDailyType[] = [
    {
      img: 'http://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/HeimaCloudMusic/recommend1.png',
      title: '每日推荐 | 今天从《不得不爱》听起 | 私人雷达',
      type: '每日推荐',
      top: '#660000',
      bottom: '#382e2f'
    },
    {
      img: 'http://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/HeimaCloudMusic/recommend2.png',
      title: '从 [Nothing on Me] 开启无限漫游',
      type: '私人漫游',
      top: '#382e2f',
      bottom: '#a37862'
    },
    {
      img: 'http://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/HeimaCloudMusic/recommend3.png',
      title: '每日推荐 | 今天从《不得不爱》听起 | 私人雷达',
      type: '华语流行',
      top: '#a37862',
      bottom: '#174847'
    },
    {
      img: 'http://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/HeimaCloudMusic/recommend4.png',
      title: '每日推荐 | 今天从《不得不爱》听起 | 私人雷达',
      type: '私人雷达',
      top: '#174847',
      bottom: '#174847'
    }
  ]

  // 推荐歌单数据源:原生歌单封面+真实业务文案
  recommendList: RecommendListType[] = [
    {
      img: 'http://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/HeimaCloudMusic/list1.jpg',
      title: '每日推荐 | 今天从《不得不爱》听起 | 私人雷达',
      count: '270.9万'
    },
    {
      img: 'http://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/HeimaCloudMusic/list2.jpg',
      title: 'Yasuo和更多好听的 | 华语私人雷达 | 回忆8090',
      count: '476.1万'
    },
    {
      img: 'http://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/HeimaCloudMusic/list3.jpg',
      title: 'Trap Remix丨当欧美热单遇上毒性低音',
      count: '186.3万'
    },
    {
      img: 'http://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/HeimaCloudMusic/list4.jpg',
      title: '满级人类进化之路必备BGM | 根本停不下来',
      count: '186.3万'
    },
    {
      img: 'http://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/HeimaCloudMusic/list5.jpg',
      title: '认真去聆听这个世界的每一分每一秒 (强烈推荐)',
      count: '362.8万'
    }
  ]

2.3 通用UI组件封装(@Builder精讲)

@Builder是ArkUI轻量级UI复用核心语法,适合页面内通用小段UI封装,无需新建文件,简化代码结构、统一样式。

/**
 * 自定义标题Builder:每日推荐模块标题栏
 * @param title 动态传入标题文字,实现组件复用
 * 布局结构:左侧标题文字 + 右侧更多图标,两端自适应对齐
 */
@Builder
daytitleBuilder(title:string){
  Row(){
    // 左侧主标题:加粗自适应占位
    Text(title)
      .fontColor('#fff')
      .fontWeight(700)
      .layoutWeight(1) // 占满剩余空间,自动挤压右侧图标

    // 右侧更多功能图标
    Image($rawfile('ic_more.svg'))
      .width(22)
      .fillColor('#fff')
  }.width('100%')
  .height(35)
  .justifyContent(FlexAlign.SpaceBetween) // 两端对齐布局
}

/**
 * 自定义标题Builder:推荐歌单模块大标题
 * 样式差异化设计:无右侧图标,字号更大、权重更高,区分二级模块标题
 * @param title 模块标题文字
 */
@Builder
songtitleBuilder(title: string) {
  Text(title)
    .fontSize(18)
    .fontColor('#fff')
    .fontWeight(FontWeight.Bold)
    .width('92%')
    .margin({ top: 20, bottom: 12 }) // 上下外边距,分隔模块间距
}

2.4 顶部自定义搜索栏

效果目标

        实现深色主题下的胶囊形搜索栏,左侧搜索图标、中间输入框、右侧功能图标,宽度自适应屏幕,视觉风格统一。

实现思路

        外层用 Row 水平布局,左右图标固定宽度,中间输入框通过 layoutWeight 自适应填充剩余空间;外层容器统一设置背景与圆角,输入框设为透明背景,避免原生样式干扰。

完整代码
// 1. 顶部自定义搜索区域
        Row(){
          Image($rawfile('ic_search.svg')).width(22).fillColor('#817D83')
          TextInput({placeholder:'只因你太美🔥'}).placeholderColor('#817D83')
            .padding({left:5}).fontColor('#999').layoutWeight(1)
            .backgroundColor('transparent')
          Image($rawfile('ic_code.svg')).width(20).fillColor('#817D83')
        }.width('92%')
        .height(36)
        .backgroundColor('#2D2B29')
        .borderRadius(20)
        .padding({left:8,right:8,top:3})
核心知识点精讲
  • layoutWeight(1) 自适应原理这是弹性布局的核心属性。设置该属性的组件会在父容器主轴方向上,自动填充所有剩余空间。本例中左右图标宽度固定,输入框占满中间区域,无论屏幕宽度多少,布局都不会变形。

  • 透明背景的设计意义TextInput 原生自带边框与背景,和我们自定义的胶囊样式冲突。设置透明背景后,视觉上完全由外层容器控制样式,UI 一致性更高。

  • 92% 宽度的留白设计不使用 100% 宽度贴边,左右各留 4% 边距,符合移动端视觉规范,避免内容贴边产生的压迫感。

新手避坑
  • 不要给输入框设置固定宽度,否则小屏幕会挤压图标,大屏幕会留白,必须用 layoutWeight 自适应

  • 圆角数值建议为高度的一半,实现完美胶囊效果,本例 36vp 高度对应 20vp 圆角

2.5 轮播图 Banner 区

效果目标

实现自动轮播的广告 Banner,3 秒切换一次,无限循环,底部带指示器,图片采用对角圆角的差异化设计。

实现思路

使用原生 Swiper 组件作为容器,ForEach 遍历图片数组渲染轮播项;图片设置等比裁剪填充,避免变形;通过 border 属性单独设置左上角与右下角圆角,实现异形视觉效果。

完整代码
// 2. 轮播图区域
        Swiper() {
          ForEach(this.swiperList,(item:string)=>{
            Image(item).width("100%")
              .height(160)
              .objectFit(ImageFit.Cover)
              .border({radius:{topLeft:40,bottomRight:40}})
          })
        }
        .width('92%')
        .autoPlay(true)
        .loop(true)
        .interval(3000)
        .indicator(true)
核心知识点精讲
  • objectFit 图片填充模式详解这是图片开发最容易踩坑的属性,五种模式差异明显:本例使用 Cover 模式,保证不同比例的网络图片都能填满 Banner 区域,视觉整齐。

    • Cover(推荐):保持宽高比放大,填满容器,超出部分裁剪,无留白,适合 Banner、封面

    • Contain:保持宽高比缩小,完整显示图片,容器可能留白,适合 LOGO、产品图

    • Fill:拉伸图片填满容器,会变形,非特殊场景不建议使用

    • None:不缩放,按原始尺寸显示

    • ScaleDown:仅缩小不放大,小图保持原尺寸,大图缩小显示

  • 单角圆角实现通过 border.radius 对象可以分别控制四个角的圆角大小,实现差异化设计,比统一圆角更有设计感。

  • 轮播核心参数autoPlay 开启自动播放、loop 开启循环、interval 设置间隔毫秒数、indicator 显示底部指示器,是商业 Banner 的标准四件套配置。

新手避坑
  • 必须给 Swiper 或内部图片设置固定高度,否则容器高度为 0,轮播不显示

  • 网络图片必须配置网络权限,否则真机上图片空白(后文有完整配置教程)

2.6 每日推荐横向卡片区

效果目标

实现横向滚动的推荐卡片,每张卡片采用「顶部标题栏 + 中间封面图 + 底部简介」的三段式结构,差异化渐变配色,整体圆角裁剪,长文本自动省略。

实现思路

先通过 Interface 定义卡片数据结构,准备模拟数据源;用 @Builder 封装通用标题栏;外层使用横向 List 承载卡片列表;单张卡片用 Column 垂直布局,外层设置圆角并开启 clip 裁剪,实现整体圆角效果。

完整代码
// 3. 每日推荐横向卡片区域
        this.daytitleBuilder('每日推荐')
        List(){
          ForEach(this.dailyRecommend,(item:recommendDailyType)=>{
            ListItem() {
              Column() {
                // 卡片顶部标题
                Text(item.type)
                  .width('100%')
                  .padding(5)
                  .height(35)
                  .backgroundColor(item.top)
                  .fontColor('#fff')
                // 卡片主体图片
                Image(item.img).width('100%')
                  .layoutWeight(1)
                  .objectFit(ImageFit.Cover)
                // 卡片底部简介文字(两行省略)
                Text(item.title)
                  .width('100%')
                  .padding(5)
                  .fontSize(14)
                  .fontColor('#fff')
                  .backgroundColor(item.bottom)
                  .maxLines(2)
                  .textOverflow({overflow:TextOverflow.Ellipsis})
              }
              .width(160)
              .height(220)
              .border({ radius: 10 })
              .margin({ right: 10 })
              .clip(true) // 裁剪圆角,解决文字溢出直角问题
            }
          })
        }.listDirection(Axis.Horizontal)
        .scrollBar(BarState.Off)
        .width('92%')
        .height(220)
核心知识点精讲
  • Interface 强类型的价值很多新手觉得模拟数据没必要写接口,这是错误的认知。接口的核心作用有三个:一是编译期校验,写错字段名、漏写字段直接报错,提前发现问题;二是代码提示,书写时自动补全字段,提升开发效率;三是规范统一,团队协作时所有人遵循同一份数据结构,避免混乱。

  • clip(true) 圆角裁剪原理父容器设置 borderRadius 后,默认只会改变自身边框形状,内部的子组件(图片、色块)仍然是直角,会溢出父容器的圆角边界。开启 clip(true) 后,系统会按照父容器的边缘轮廓对子组件进行裁剪,超出圆角范围的内容直接不渲染,从而实现整体圆角效果。这是鸿蒙开发中解决「圆角不生效」问题的最常用方案,90% 的圆角失效问题都是因为忘记开启裁剪。

  • 文本溢出处理maxLines 限制最大行数,textOverflow 设置超出部分用省略号显示,两者搭配使用彻底解决长文案撑破布局的问题,是列表开发的必备技巧。

  • layoutWeight 垂直适配卡片高度固定为 220vp,上下文字区域高度固定,中间图片设置 layoutWeight(1) 自动填充剩余高度,保证所有卡片高度完全一致,不会因为图片尺寸不同而错落。

新手避坑
  • 横向 List 必须设置固定高度,否则容器会默认占满全屏,导致页面布局塌陷

  • List 内部必须包裹 ListItem,这是官方规范,直接写子组件会触发编译警告与潜在性能问题

  • 隐藏滚动条可以让页面更干净,符合移动端 App 的视觉规范

2.7 推荐歌单列表区

效果目标

实现横向滚动的歌单卡片,封面图左上角悬浮播放量,下方展示歌单名称,长名称两行省略,整体样式对齐主流音乐 App。

实现思路

使用 Stack 堆叠布局实现播放量悬浮在封面之上的效果;外层同样采用横向 List 承载列表;歌单名称做文本溢出处理;播放量文字添加半透明背景,保证在亮色图片上的可读性。

完整代码
// 4. 推荐歌单区域
        this.songtitleBuilder('推荐歌单')
        List() {
          ForEach(this.recommendList, (item: RecommendListType) => {
            ListItem() {
              Column({ space: 6 }) {
                // 封面+播放量悬浮层
                Stack({ alignContent: Alignment.TopStart }) {
                  Image(item.img)
                    .width(120)
                    .height(120)
                    .objectFit(ImageFit.Cover)
                    .borderRadius(8)

                  Text(item.count)
                    .fontColor('#fff')
                    .fontSize(12)
                    .fontWeight(FontWeight.Bold)
                    .margin(5)
                    .backgroundColor('rgba(0,0,0,0.4)')
                    .padding({left:4,right:4})
                    .borderRadius(4)
                }
                .width(120)

                // 歌单名称文字
                Text(item.title)
                  .fontColor('#fff')
                  .fontSize(12)
                  .maxLines(2)
                  .textOverflow({ overflow: TextOverflow.Ellipsis })
                  .width(120)
              }
            }
          })
        }
        .width('92%')
        .height(180)
        .listDirection(Axis.Horizontal)
        .scrollBar(BarState.Off)
核心知识点精讲
  • Stack 堆叠布局原理Stack 内部的子组件按书写顺序层级叠加,先写的在底层,后写的在顶层。通过 alignContent 可以控制子组件的对齐位置,本例设置为左上角对齐,实现播放量悬浮在封面左上角的效果,是实现标签、水印、悬浮按钮的核心布局方式。

  • 半透明背景提升可读性直接把白色文字放在图片上,遇到亮色图片会看不清。添加一层 40% 透明度的黑色背景,既不遮挡封面主体,又能保证文字在任何图片上都清晰可读,是行业通用做法。

  • 固定尺寸保证整齐封面、文字宽度统一设置为 120vp,所有歌单卡片尺寸完全一致,横向排列整齐美观,不会因为图片和文字长度不同而变形。

三、核心原理深度解析

3.1 横向滚动方案对比:List vs Scroll+Row

        很多新手会疑惑,为什么不用更简单的 Scroll+Row 实现横向滚动,这里做一个完整对比:

对比维度

Scroll + Row

List + ListItem

渲染机制

一次性渲染所有子组件

按需渲染可见区域,滚动时回收复用组件

性能表现

数据量少时流畅,20项以上内存占用显著上升

大数据量下性能优势明显,100项也能保持流畅

功能丰富度

基础滚动能力

支持分割线、分组、粘性标题、懒加载等高级特性

代码复杂度

写法简单

需要嵌套 ListItem,略繁琐

适用场景

少量固定选项、标签栏

数据列表、卡片流、后续可能扩展数据的场景

        本次歌单、推荐卡片后续都会对接真实接口,数据量可能增加,因此直接选用 List 方案,为后续扩展留足空间。

3.2 @Builder 组件复用的意义

@Builder 是 ArkUI 提供的轻量级 UI 复用机制,它的核心价值在于:

  • 减少冗余代码:相同样式的标题、卡片只写一次,多处调用

  • 统一维护成本:修改样式只改一处,所有引用位置同步生效,避免多处修改遗漏

  • 提升代码可读性:业务代码更简洁,结构更清晰,聚焦核心逻辑

        注意区分:@Builder 用于页面内小范围 UI 复用;如果是跨页面复用的通用组件,应该封装成独立的自定义组件(单独 struct)。

3.3 为什么必须配置网络权限

        鸿蒙系统对应用权限做了严格管控,默认状态下应用没有任何网络访问能力,这是出于安全与隐私保护的设计。应用要访问网络,必须在配置文件中主动声明互联网权限,系统才会开放对应的网络能力。

        除此之外,鸿蒙默认只允许 HTTPS 加密请求,HTTP 明文请求会被系统拦截。这是因为 HTTP 传输的数据不加密,存在被窃听、篡改的风险。开发阶段如果使用 HTTP 测试资源,需要额外配置网络安全策略放行明文流量,正式上线建议全部切换为 HTTPS。

四、网络权限完整配置指南

        我们的页面使用了大量网络图片,必须完成两步配置才能正常加载,缺一不可。

4.1 第一步:声明互联网权限

  1. 打开配置文件:entry/src/main/module.json5

  2. module 节点下添加 requestPermissions 数组

  3. 写入 ohos.permission.INTERNET 权限配置

{
  "module": {
    // routerMap、name、pages等其余原有配置保持不变
    
    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET"
      },
      {
        "name": "ohos.permission.KEEP_BACKGROUND_RUNNING"
      }
    ],

    "abilities": [
      {
        "name": "EntryAbility",
        // 其余原有配置保持不变
        "backgroundModes": [
          "audioPlayback"
        ]
      }
    ]
  }
}

说明:互联网权限属于普通权限,静态声明即可生效,不需要用户手动授权。

4.2 第二步:放行 HTTP 明文请求

  1. 找到目录:entry/src/main/resources/base/profile,没有 profile 文件夹就手动新建

  2. 在 profile 目录下新建文件:network_config.json

  3. 写入全局明文放行配置

{
  "network-security-config": {
    "base-config": {
      "cleartextTrafficPermitted": true
    }
  }
}

注意:该配置仅用于开发测试;正式上线请全部使用 HTTPS 地址,并按需给指定域名放开权限,提升安全性。

4.3 配置生效步骤

  1. 点击 DevEco Studio 顶部的 Sync Project 同步项目配置

  2. 卸载模拟器/真机上的旧版本应用

  3. 重新编译运行,网络图片即可正常加载

五、实战踩坑全记录(附完整解决方案)

以下是本次开发过程中真实遇到的所有问题,每一个都是新手高频踩坑点,建议收藏对照排查。

坑1:编译报错 - 组件导出导入名称不匹配

报错信息

10505001 ArkTS Compiler Error
'"./home/Recommend"' has no exported member named 'Recommend'. Did you mean 'Recommend1'?
10905204 ArkTS Compiler Error
'Recommend()' does not meet UI component syntax.

原因分析

组件文件内导出的结构体名称,和导入时使用的名称不一致。鸿蒙对组件名称严格匹配,大小写、字符差异都会导致识别失败,进而连锁触发 UI 组件语法报错。

解决方案

保持「文件名 = 组件名」的命名规范,Recommend.ets 文件内导出 struct Recommend,导入时也使用 Recommend,三者完全统一。

避坑建议

养成一个文件一个组件、文件名和组件名完全一致的开发习惯,从源头避免此类问题。

坑2:运行异常 - 网络图片全部空白

现象:代码编译正常,但页面上所有网络图片位置都是空白,没有报错。

排查思路

  1. 先检查是否配置了 INTERNET 网络权限

  2. 如果是 HTTP 地址,检查是否配置了明文请求放行

  3. 检查图片链接是否有效,是否过期、是否禁止外部访问

  4. 卸载旧应用重新安装,排除配置未生效问题

本次原因:课程配套的阿里云 OSS 资源已过期,禁止外部访问,属于资源失效问题,和代码无关。

解决方案:替换为有效的公共图片资源,或使用本地图片兜底。

坑3:编辑器警告 - 网页解析失败

现象:编辑器提示「网页解析失败,可能是不支持的网页类型」,但模拟器/真机上图片可以正常显示。

原因分析

这是编辑器的资源解析器导致的提示。纯图片链接只返回二进制图片数据,没有网页文本结构,编辑器的网页解析器无法识别,就会抛出该警告。该警告仅存在于编辑器,完全不影响实际运行效果

解决方案

  • 方案一:忽略即可,不影响编译和运行

  • 方案二:将图片资源下载到本地 rawfile 目录,使用本地资源引用,彻底消除警告

  • 方案三:更换支持网页解析的图床资源(一般带跳转页面的图片链接)

六、Recommend.ets 最终完整源码

以下是整合所有模块、修复所有问题、可直接编译运行的完整代码,直接替换文件内容即可使用。

/**
 * 实战03 推荐页
 * 分层结构:搜索区+轮播区+每日推荐区+歌单区
 */

// 每日推荐卡片数据类型约束
interface recommendDailyType {
  img: string,    // 网络图片地址
  title: string,  // 卡片底部简介文字
  type: string,   // 卡片顶部标题文字
  top: string,    // 顶部标题背景色
  bottom: string  // 底部简介背景色
}

// 推荐歌单数据类型约束
interface RecommendListType {
  img: string;     // 歌单封面图
  title: string;   // 歌单名称
  count: string;   // 歌单播放量
}

@Component
export struct Recommend {
  // 轮播图网络图片数据源
  swiperList: string[] = [
    "http://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/HeimaCloudMusic/banner1.png",
    "http://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/HeimaCloudMusic/banner3.png"
  ]

  // 每日推荐卡片数据源
  dailyRecommend: recommendDailyType[] = [
    {
      img: 'http://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/HeimaCloudMusic/recommend1.png',
      title: '每日推荐 | 今天从《不得不爱》听起 | 私人雷达',
      type: '每日推荐',
      top: '#660000',
      bottom: '#382e2f'
    },
    {
      img: 'http://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/HeimaCloudMusic/recommend2.png',
      title: '从 [Nothing on Me] 开启无限漫游',
      type: '私人漫游',
      top: '#382e2f',
      bottom: '#a37862'
    },
    {
      img: 'http://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/HeimaCloudMusic/recommend3.png',
      title: '每日推荐 | 今天从《不得不爱》听起 | 私人雷达',
      type: '华语流行',
      top: '#a37862',
      bottom: '#174847'
    },
    {
      img: 'http://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/HeimaCloudMusic/recommend4.png',
      title: '每日推荐 | 今天从《不得不爱》听起 | 私人雷达',
      type: '私人雷达',
      top: '#174847',
      bottom: '#174847'
    }
  ]

  // 推荐歌单数据源
  recommendList: RecommendListType[] = [
    {
      img: 'http://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/HeimaCloudMusic/list1.jpg',
      title: '每日推荐 | 今天从《不得不爱》听起 | 私人雷达',
      count: '270.9万'
    },
    {
      img: 'http://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/HeimaCloudMusic/list2.jpg',
      title: 'Yasuo和更多好听的 | 华语私人雷达 | 回忆8090',
      count: '476.1万'
    },
    {
      img: 'http://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/HeimaCloudMusic/list3.jpg',
      title: 'Trap Remix丨当欧美热单遇上毒性低音',
      count: '186.3万'
    },
    {
      img: 'http://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/HeimaCloudMusic/list4.jpg',
      title: '满级人类进化之路必备BGM | 根本停不下来',
      count: '186.3万'
    },
    {
      img: 'http://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/HeimaCloudMusic/list5.jpg',
      title: '认真去聆听这个世界的每一分每一秒 (强烈推荐)',
      count: '362.8万'
    }
  ]

  // 自定义标题Builder:每日推荐标题+更多按钮
  @Builder
  daytitleBuilder(title:string){
    Row(){
      Text(title)
        .fontColor('#fff')
        .fontWeight(700)
        .layoutWeight(1)

      Image($rawfile('ic_more.svg'))
        .width(22)
        .fillColor('#fff')
    }.width('100%')
    .height(35)
    .justifyContent(FlexAlign.SpaceBetween)
  }

  // 自定义标题Builder:歌单模块标题
  @Builder
  songtitleBuilder(title: string) {
    Text(title)
      .fontSize(18)
      .fontColor('#fff')
      .fontWeight(FontWeight.Bold)
      .width('92%')
      .margin({ top: 20, bottom: 12 })
  }

  build() {
    // 外层滚动容器,实现整页上下滑动
    Scroll() {
      Column({space:15}){
        // 1. 顶部自定义搜索区域
        Row(){
          Image($rawfile('ic_search.svg')).width(22).fillColor('#817D83')
          TextInput({placeholder:'只因你太美🔥'}).placeholderColor('#817D83')
            .padding({left:5}).fontColor('#999').layoutWeight(1)
            .backgroundColor('transparent')
          Image($rawfile('ic_code.svg')).width(20).fillColor('#817D83')
        }.width('92%')
        .height(36)
        .backgroundColor('#2D2B29')
        .borderRadius(20)
        .padding({left:8,right:8,top:3})

        // 2. 轮播图区域
        Swiper() {
          ForEach(this.swiperList,(item:string)=>{
            Image(item).width("100%")
              .height(160)
              .objectFit(ImageFit.Cover)
              .border({radius:{topLeft:40,bottomRight:40}})
          })
        }
        .width('92%')
        .autoPlay(true)
        .loop(true)
        .interval(3000)
        .indicator(true)

        // 3. 每日推荐横向卡片区域
        this.daytitleBuilder('每日推荐')
        List(){
          ForEach(this.dailyRecommend,(item:recommendDailyType)=>{
            ListItem() {
              Column() {
                // 卡片顶部标题
                Text(item.type)
                  .width('100%')
                  .padding(5)
                  .height(35)
                  .backgroundColor(item.top)
                  .fontColor('#fff')
                // 卡片主体图片
                Image(item.img).width('100%')
                  .layoutWeight(1)
                  .objectFit(ImageFit.Cover)
                // 卡片底部简介文字(两行省略)
                Text(item.title)
                  .width('100%')
                  .padding(5)
                  .fontSize(14)
                  .fontColor('#fff')
                  .backgroundColor(item.bottom)
                  .maxLines(2)
                  .textOverflow({overflow:TextOverflow.Ellipsis})
              }
              .width(160)
              .height(220)
              .border({ radius: 10 })
              .margin({ right: 10 })
              .clip(true) // 裁剪圆角,解决文字溢出直角问题
            }
          })
        }.listDirection(Axis.Horizontal)
        .scrollBar(BarState.Off)
        .width('92%')
        .height(220)

        // 4. 推荐歌单区域
        this.songtitleBuilder('推荐歌单')
        List() {
          ForEach(this.recommendList, (item: RecommendListType) => {
            ListItem() {
              Column({ space: 6 }) {
                // 封面+播放量悬浮层
                Stack({ alignContent: Alignment.TopStart }) {
                  Image(item.img)
                    .width(120)
                    .height(120)
                    .objectFit(ImageFit.Cover)
                    .borderRadius(8)

                  Text(item.count)
                    .fontColor('#fff')
                    .fontSize(12)
                    .fontWeight(FontWeight.Bold)
                    .margin(5)
                    .backgroundColor('rgba(0,0,0,0.4)')
                    .padding({left:4,right:4})
                    .borderRadius(4)
                }
                .width(120)

                // 歌单名称文字
                Text(item.title)
                  .fontColor('#fff')
                  .fontSize(12)
                  .maxLines(2)
                  .textOverflow({ overflow: TextOverflow.Ellipsis })
                  .width(120)
              }
            }
          })
        }
        .width('92%')
        .height(180)
        .listDirection(Axis.Horizontal)
        .scrollBar(BarState.Off)
      }
      .width('100%')
      .padding({top:10})
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#131215')
    .scrollBar(BarState.Off)
  }
}

效果图:

七、本篇总结与下篇预告

本篇核心收获

  • 掌握了移动端首页模块化拆分的设计思路,能独立完成从设计到代码的布局拆解

  • 熟练运用六大核心布局组件,理解各自的适用场景与性能差异

  • 吃透了图片填充、圆角裁剪、文本溢出三大高频 UI 问题的底层原理与解决方案

  • 建立了强类型开发意识,学会了用 Interface 规范数据、用 @Builder 复用 UI

  • 完整掌握了鸿蒙网络配置流程,具备独立排查图片加载问题的能力

  • 积累了三个真实开发踩坑经验,避开新手常见误区

下篇预告

        目前我们完成了推荐页的完整 UI 开发后,下一篇我们将正式进入发现组件页面的搭建实战,继续完善音乐应用的 Tab 导航体系。通用卡片封装、UI 数据解耦与后端接口对接等进阶内容,会放在后续章节逐步展开。。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐