HarmonyOS NEXT - 电商App实例五(首页开发)

        在前面的案例中,电商App的环境搭建、准备工作、网络请求、登录等功能都已讲完,接下来将开始App首页的开发。

        在HarmonyOS NEXT开发中,首页组成模块相对较多,需要使用到组件开发,这样开发者可以将应用中独立的业务模块抽取出来,单独创建一个模块。通过组件化,使得每个独立的业务模块都可以运行、降低开发成本和维护难度,从而大大提高开发效率。

一、基础架构

        下面,我们将首页分为顶部导航栏模块、轮翻图模块、分类导航模块、推荐商品区模块,以及底部导航栏模块等五个组件。如下图:

        首先,以弹性方式布局页面子组件的Flex容器组件,以垂直方向排列,水平居中方式显示子组件。代码如下:

@Entry
@Component
struct Index {
  
  build() {
    RelativeContainer() {
      Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center }){
        // do something...

      }.width('100%')
       .height('100%')
    }
    .height('100%')
    .width('100%')
  }
}

        Felx容器参数说明:

名称 类型 必填 说明
direction FlexDirection

子组件在Flex容器上排列的方向,即主轴的方向。

默认值: FlexDirection.Row

卡片能力: 从API version 9开始,该接口支持在ArkTS卡片中使用。

wrap FlexWrap

Flex容器是单行/列还是多行/列排列。

默认值: FlexWrap.NoWrap

说明:

在多行布局时,通过交叉轴方向,确认新行堆叠方向。

卡片能力: 从API version 9开始,该接口支持在ArkTS卡片中使用。

justifyContent FlexAlign

所有子组件在Flex容器主轴上的对齐格式。

默认值: FlexAlign.Start

卡片能力: 从API version 9开始,该接口支持在ArkTS卡片中使用。

alignItems ItemAlign

所有子组件在Flex容器交叉轴上的对齐格式。

默认值: ItemAlign.Start

卡片能力: 从API version 9开始,该接口支持在ArkTS卡片中使用。

alignContent FlexAlign

交叉轴中有额外的空间时,多行内容的对齐方式。仅在wrap为Wrap或WrapReverse下生效。

默认值: FlexAlign.Start

卡片能力: 从API version 9开始,该接口支持在ArkTS卡片中使用。

space12+ FlexSpaceOptions12+

所有子组件在Flex容器主轴或交叉轴的space。

默认值: {main:LengthMetrics.px(0), cross:LengthMetrics.px(0)}

space为负数、百分比或者justifyContent设置为FlexAlign.SpaceBetween、FlexAlign.SpaceAround、FlexAlign.SpaceEvenly时不生效。

元服务API: 从API version 12开始,该接口支持在元服务中使用。

二、顶部导航栏

        顶部导航栏通常位于App界面的顶部,为用户提供应用核心入口,如搜索框、分类按钮、个人中心等等,以及当前页面标题或标识。这里,我们将在顶部导航栏左侧添加App的logo,右侧添加搜索框功能。

        在components目录中创建Header.ets文件,用于定义顶部导航组件,地址为src/main/ets/components/Header.ets。然后在首页中,引用顶部导航组件,首页示例代码如下:

import Header from '../components/Header';

@Entry
@Component
struct Index {

  build() {
    RelativeContainer() {
      Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center }){
        // 引入顶部导航栏组件
        Header()

      }.width('100%')
       .height('100%')
    }
    .height('100%')
    .width('100%')
  }
}

2.1 Row()容器

        使用Row()容器,将子元素设置为左右对齐显示。示例代码如下:

@Component
export default struct Header {
  build() {
    Row(){
      Text('Header')
      Text('Search')
    }.width('100%')
     .padding({ left: 20, right: 20, top: 10, bottom: 10 })
     .justifyContent(FlexAlign.SpaceBetween)
  }
}

        效果如下图:

2.2 Logo

        使用Image()组件,定义App的Logo,示例代码如下:

@Component
export default struct Header {
  build() {
    Row(){
      Image($rawfile('logo.png'))
        .width(35)
      Text('Search')
    }.width('100%')
     .padding({ left: 20, right: 20, top: 10, bottom: 10 })
     .justifyContent(FlexAlign.SpaceBetween)
  }
}

        页面效果如下图:

2.3 搜索图标

        在顶部导航栏右侧,添加图标+文字方式 的搜索标识,当用户点击时切换输入框进行搜索。示例代码如下:

@Component
export default struct Header {
  build() {
    Row(){
      Image($rawfile('logo.png'))
        .width(35)

      Row(){
        Image($rawfile('search.png'))
          .width(26)
          .margin({ right: 5 })
        Text('搜索')
          .fontSize(18)
      }
    }.width('100%')
     .padding({ left: 20, right: 20, top: 10, bottom: 10 })
     .justifyContent(FlexAlign.SpaceBetween)
  }
}

        页面效果如下图:

2.4 搜索框

        在Header组件中,定义isSearch状态变量用于记录当前是否切换为搜索框状态,以及keyword状态变量用于记录输入框输入的内容文本。

        先将isSearch赋值为true,显示搜索框状态下样式效果。示例代码如下:

@Component
export default struct Header {
  @State isSearch: boolean = true // 标识是否切换 显示搜索框,默认为false
  @State keyword: string = ''     // 搜索关键词

  build() {
    Row(){
      Image($rawfile('logo.png'))
        .width(35)
      // 如果为false,显示为图标 + 文字
      if (!this.isSearch) {
        Row(){
          Image($rawfile('search.png'))
            .width(26)
            .margin({ right: 5 })
          Text('搜索')
            .fontSize(18)
        }
      }
      // 如果为true,显示搜索框
      else {
        Row(){
          // 绘制搜索框
          TextInput({ text: this.keyword, placeholder: '请输入搜索关键词' })
            .height(40)
            .layoutWeight(1)  // 输入框获取剩于宽度作为自己宽度显示
          // 绘制取消按钮
          Button('取消').height(38)
            .backgroundColor('#CCCCCC')
            .fontColor('#333333')
            .fontSize(15)
            .fontWeight(0)
            .padding({ top: 0, bottom: 0, left: 15, right: 15 })
            .margin({ left: 10 })
        }.padding({ left: 20 })
         .layoutWeight(1)  // Row()容器获取剩于宽度作为自己宽度显示
      }

    }.width('100%')
     .padding({ left: 20, right: 20, top: 10, bottom: 10 })
     .justifyContent(FlexAlign.SpaceBetween)
  }
}

        页面效果如下图:

2.5 状态切换

        这里先将搜索框的显示与隐藏功能完成,待商品信息完善后,再来实现搜索功能。header.ets文件的示例代码如下:

@Component
export default struct Header {
  @State isSearch: boolean = false // 标识是否切换 显示搜索框,默认为false
  @State keyword: string = ''     // 搜索关键词

  build() {
    Row(){
      Image($rawfile('logo.png'))
        .width(35)
      // 如果为false,显示为图标 + 文字
      if (!this.isSearch) {
        Row(){
          Image($rawfile('search.png'))
            .width(26)
            .margin({ right: 5 })
          Text('搜索')
            .fontSize(18)
        }.onClick(() => {
          this.isSearch = true  // 置为true,显示搜索框
        })
      }
      // 如果为true,显示搜索框
      else {
        Row(){
          // 绘制搜索框
          TextInput({ text: this.keyword, placeholder: '请输入搜索关键词' })
            .height(40)
            .layoutWeight(1)  // 输入框获取剩于宽度作为自己宽度显示
          // 绘制取消按钮
          Button('取消').height(38)
            .backgroundColor('#CCCCCC')
            .fontColor('#333333')
            .fontSize(15)
            .fontWeight(0)
            .padding({ top: 0, bottom: 0, left: 15, right: 15 })
            .margin({ left: 10 })
            .onClick(() => {
              this.isSearch = false // 置为false,关闭搜索框
            })
        }.padding({ left: 20 })
         .layoutWeight(1)  // Row()容器获取剩于宽度作为自己宽度显示
      }

    }.width('100%')
     .padding({ left: 20, right: 20, top: 10, bottom: 10 })
     .justifyContent(FlexAlign.SpaceBetween)
  }
}

        页面效果如下:

三、底部导航栏

        底部导航栏位于App界面的最底部,为用户提供核心功能的快速入口,通常以图标+文字形式组成,如首页、分类、购买车、个人中心等。

        在绘制底部导航栏前,先准备好以下几个图标。如下图:

3.1 定位底部

        底部导航栏,顾名思义是要固定在页面的底部。传统web页面、App或小程序是通过定位方式,将导航底部在底部或顶部;在HarmonyOS NEXT中,可以使用layoutWeight()权重,将flex容器中剩余高度,分配给指定容器。

        打开Index.ets 首页文件,示例代码如下:

import Header from '../components/Header';
import Navi from '../components/Navigation';

@Entry
@Component
struct Index {

  build() {
    RelativeContainer() {
      Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center }){
        Header()    // 顶部导航组件

        Row(){
          // 将屏幕中 剩余调度,设置为中间内容区域的调度
          // 这样可以使用Header和Navi组件,分别固定在页面的顶部和底部
        }.layoutWeight(1)
        
        Navi()    // 底部菜单栏组件

      }.width('100%')
       .height('100%')
    }
    .height('100%')
    .width('100%')
  }
}

        此时,Navigation底部菜单栏则显示在页面的最底部,如下图:

3.2 定义菜单数据

        在底部菜单组件中,定义好菜单列表数据,将通过foreach循环渲染出来。示例代码如下:

// 定义数据类型
interface navInfo {
  id: number
  name: string
  icon: Resource,
  iconHover: Resource
  path?: string
}

@Component
export default struct CustomNavigation {
  private NavList: Array<navInfo> = [
    {id: 1, name: '首页', icon: $rawfile('icon_home.png'), iconHover: $rawfile('icon_home_hover.png')},
    {id: 2, name: '商品', icon: $rawfile('icon_goods.png'), iconHover: $rawfile('icon_goods_hover.png')},
    {id: 3, name: '购买车', icon: $rawfile('icon_cart.png'), iconHover: $rawfile('icon_cart_hover.png')},
    {id: 4, name: '我的', icon: $rawfile('icon_mine.png'), iconHover: $rawfile('icon_mine_hover.png')}
  ]

  build() {
    Row(){
      Text('Navigation')
    }
  }
}

3.3 Row()容器

        在Row()容器中,通过justifyContent()方法,将横向布局设置为FlexAlign.SpaceAround,将首页、商品、购物车、我的四个菜单四等分显示。并且,通过ForEach()方法,循环输出菜单列表信息。示例代码如下:

// 定义数据类型
interface navInfo {
  id: number
  name: string
  icon: Resource,
  iconHover: Resource
  path?: string
}

@Component
export default struct CustomNavigation {
  private NavList: Array<navInfo> = [
    {id: 1, name: '首页', icon: $rawfile('icon_home.png'), iconHover: $rawfile('icon_home_hover.png')},
    {id: 2, name: '商品', icon: $rawfile('icon_goods.png'), iconHover: $rawfile('icon_goods_hover.png')},
    {id: 3, name: '购买车', icon: $rawfile('icon_cart.png'), iconHover: $rawfile('icon_cart_hover.png')},
    {id: 4, name: '我的', icon: $rawfile('icon_mine.png'), iconHover: $rawfile('icon_mine_hover.png')}
  ]

  build() {
    Row(){
      // 循环输入
      ForEach(this.NavList, (item: navInfo) => {
        Column(){
          Image(item.icon).width(30)  // 图标
          // 标题名称
          Text(item.name)
            .fontSize(14)
            .fontColor('#333333')
        }
      })
    }.width('100%')
     .justifyContent(FlexAlign.SpaceAround)
  }
}

        页面效果如下图:

3.4 菜单高亮

        在底部菜单导航组件中,定义NavIndex变量,用于记录当前菜单项索引。等于当前索引时,显示高亮图标和文字。示例代码如下:

// 定义数据类型
interface navInfo {
  id: number
  name: string
  icon: Resource,
  iconHover: Resource
  path?: string
}

@Component
export default struct CustomNavigation {
  private NavList: Array<navInfo> = [
    {id: 1, name: '首页', icon: $rawfile('icon_home.png'), iconHover: $rawfile('icon_home_hover.png')},
    {id: 2, name: '商品', icon: $rawfile('icon_goods.png'), iconHover: $rawfile('icon_goods_hover.png')},
    {id: 3, name: '购买车', icon: $rawfile('icon_cart.png'), iconHover: $rawfile('icon_cart_hover.png')},
    {id: 4, name: '我的', icon: $rawfile('icon_mine.png'), iconHover: $rawfile('icon_mine_hover.png')}
  ]
  @State NavId: number = 0  // 当前项ID,默认第一项高亮

  build() {
    Row(){
      // 循环输入
      ForEach(this.NavList, (item: navInfo) => {
        Column(){
          Image(this.NavId == item.id ? item.iconHover : item.icon).width(30)  // 图标
          // 标题名称
          Text(item.name)
            .fontSize(14)
            .fontColor(this.NavId == item.id ? '#333333' : '#666666')
        }
      })
    }.width('100%')
     .justifyContent(FlexAlign.SpaceAround)
  }
}

        页面效果如下图:

3.5 页面跳转

        如下图,在项目中先创建首页 和 个人中心两个空页面,用于页面跳转。

        在底部菜单导航组件中,添加首页和个人中心页面路径,并在Column上添加onClick事件,用于跳转业务处理。示例代码如下:

import router from "@ohos.router"

// 定义数据类型
interface navInfo {
  id: number
  name: string
  icon: Resource,
  iconHover: Resource
  path?: string
}

@Component
export default struct CustomNavigation {
  private NavList: Array<navInfo> = [
    {
      id: 1,
      name: '首页',
      icon: $rawfile('icon_home.png'),
      iconHover: $rawfile('icon_home_hover.png'),
      path: 'pages/Index'
    },
    {id: 2, name: '商品', icon: $rawfile('icon_goods.png'), iconHover: $rawfile('icon_goods_hover.png')},
    {id: 3, name: '购买车', icon: $rawfile('icon_cart.png'), iconHover: $rawfile('icon_cart_hover.png')},
    {
      id: 4,
      name: '我的',
      icon: $rawfile('icon_mine.png'),
      iconHover: $rawfile('icon_mine_hover.png'),
      path: 'pages/Mine'
    }
  ]
  @State NavId: number = 1  // 当前项ID,默认第一项高亮

  build() {
    Row(){
      // 循环输入
      ForEach(this.NavList, (item: navInfo) => {
        Column(){
          Image(this.NavId == item.id ? item.iconHover : item.icon).width(30)  // 图标
          // 标题名称
          Text(item.name)
            .fontSize(14)
            .fontColor(this.NavId == item.id ? '#333333' : '#666666')
        }.onClick(() => {
          if (item.path) {
            router.pushUrl({ url: item.path })
          }
        })
      })
    }.width('100%')
     .justifyContent(FlexAlign.SpaceAround)
  }
}

        首页代码如下:

import Header from '../components/Header';
import Navi from '../components/Navigation';

@Entry
@Component
struct Index {

  build() {
    RelativeContainer() {
      Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center }){
        Header()

        Row(){
          // 将屏幕中 剩余调度,设置为中间内容区域的调度
          // 这样可以使用Header和Navi组件,分别固定在页面的顶部和底部
        }.layoutWeight(1)

        Navi({ NavId: 1 })

      }.width('100%')
       .height('100%')
    }
    .height('100%')
    .width('100%')
  }
}

        个人中心页面代码如下:

import Navi from '../components/Navigation';

@Entry
@Component
struct Mine {
  @State message: string = 'Hello World';

  build() {
    RelativeContainer() {
      Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center }){

        Row(){
          // 将屏幕中 剩余调度,设置为中间内容区域的调度
          // 这Navi组件,分别固定在页面的顶部和底部
        }.layoutWeight(1)
        Navi({ NavId: 4 })

      }.width('100%')
      .height('100%')
    }
    .height('100%')
    .width('100%')
  }
}

        首页和个人中心的切换效果,如下图:

四、轮翻图

        轮翻图模块通过滑动切换方式展示多张图片或广告内容,通常展示新品推荐、优惠活动或热门商品等信息。

        由于首页内容较多,需要将各业务模块独立抽取出来,因此将Index调整为目录形式,在内部创建components文件夹,用于单独存放首页中抽取的独立模块,方便各业务模块的维护。如下图:

        注意:首页结构调整后,需要打开src/main/resources/base/profile/main_pages.json文件,查看首页路径地址是否更新,否则无法正常跳转。示例代码如下:

{
  "src": [
    "pages/Index/Index",
    "pages/Mine",
    "pages/Login"
  ]
}

4.1 调整首页结构

        在首页中,引入Banner()组件,并将中间Row()容器添加alignItems和justifyContent,将容器布局调整为居上居左显示。示例代码如下:

import Header from '../../components/Header';
import Navi from '../../components/Navigation';
import Banner from './components/Banner';

@Entry
@Component
struct Index {

  build() {
    RelativeContainer() {
      Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center }){
        Header()  // 顶部导航
        Row(){
          // 将屏幕中 剩余调度,设置为中间内容区域的调度
          // 这样可以使用Header和Navi组件,分别固定在页面的顶部和底部

          // 引入Banner图
          Banner()
        }.layoutWeight(1)
         .width('100%')
         .alignItems(VerticalAlign.Top)
         .justifyContent(FlexAlign.Start)
        // 底部菜单栏
        Navi({ NavId: 1 })

      }.width('100%')
       .height('100%')
    }
    .height('100%')
    .width('100%')
  }
}

4.2 定义Banner数据类型

        在types目录中的index.d.ts文件,定义banner的数据类型。

        示例代码如下:

/**
 * 首页Banner数据结构
 */
interface HomeBanner {
  id: number
  name: string
  thumb: string
}

4.3 API请求

        接下来,我们通过API请求,获取数据库中的Banner数据,并在界面中显示这些图片信息。

        postman中查看API接口数据,如下图:

        在api/index.ts中,添加获取banner数据的API接口函数。目录结构如下图:

        api/index.ets中的getBannerInfo接口函数,示例代码如下:

import { standardInterfaceResult } from '../types/http'
import { httpRequest } from '../utils/request'

/**
 * 获取banner信息
 */
export const getBannerInfo = async () => {
  return await httpRequest.get<standardInterfaceResult>('/banner.php')
}

        在Banner组件模块在,引入getBannerInfo函数并获取数据。当成功获取时,将数据赋值给状态变量bannerList。示例代码如下:

import { getBannerInfo } from "../../../api"

@Component
export default struct Banner {
  // 定义状态变量
  @State bannerList: Array<HomeBanner> = []

  aboutToAppear(): void {
    getBannerInfo().then(res => {
      if (res.code == 200 && Array.isArray(res.data)) {
        this.bannerList = res.data['list']
        console.log('result: ', JSON.stringify(res.data))
      }
    })
  }

  build() {
    Text('banner')
  }
}

4.4 轮翻图组件

        在HarmonyOS NEXT中,Swiper组件提供滑动轮播显示的能力;Swiper本身是一个容器组件,当设置了多个子组件后,可以对这些子组件进行轮播显示。通常,在一些应用首页显示推荐的内容时,需要用到轮播显示的能力。

        官方文档地址:文档中心

        使用Swiper组件,完成轮播图功能,示例代码如下:

import { getBannerInfo } from "../../../api"

@Component
export default struct Banner {
  // 定义状态变量
  @State bannerList: Array<HomeBanner> = []

  aboutToAppear(): void {
    getBannerInfo().then(res => {
      // 获取banner数据
      if (res.code == 200 && res.data) {
        this.bannerList = res.data['list']
        console.log('result: ', JSON.stringify(this.bannerList))
      }
    }).catch(() => {
      console.log('error')
    })
  }

  build() {
    Swiper(){
      // 循环输出图片信息
      ForEach(this.bannerList, (item: HomeBanner) => {
        Image(item.thumb).borderRadius(10)
      })
    }
    .width("100%")
    .height("200vp")
    .padding(10)
    .index(0)         // 默认索引
    .autoPlay(true)   // 自动播放
    .loop(true)       // 循环播放
    .itemSpace(10)    // 图片的间隙
  }
}

        页面效果如下图:

五、分类模块

        分类导航模块可以为用户提供一个快速查询商品的方式,通常为商品的分类标签或图标组成,用户可以通过点击这些标签浏览不同类别的商品信息。

5.1 分类图标

        开发分类导航模块前,先准备好以下分类图标。

5.2 Row()容器

        使用Row()容器对分类信息进行横向显示,配置justifyContent()方法将分类信息进行横向平均(FlexAlign.SpaceAround)分配,配置alignItems()方法使分类信息垂直方向为水平对齐(VerticalAlign.Center)。

        Categroy.ets组件示例代码如下:

/**
 * 定义分类数据结构
 */
interface categoryInfo {
  id: number
  name: string
  thumb: Resource
}

@Component
export default struct CategoryList {
  @State categoryList: Array<categoryInfo> = [
    {id: 1, name: '水果', thumb: $rawfile('category_fruit.png')},
    {id: 2, name: '电动车', thumb: $rawfile('category_car.png')},
    {id: 3, name: '零食', thumb: $rawfile('category_snack.png')},
    {id: 4, name: 'TV', thumb: $rawfile('category_tv.png')},
    {id: 5, name: 'More', thumb: $rawfile('category_more.png')}
  ]

  build() {
    Row({ space: 15 }){
      ForEach(this.categoryList, (item: categoryInfo) => {
        Column(){
          Image(item.thumb).width(30).margin({ bottom: 10 })
          Text(item.name).fontSize(14)
        }.padding({ top: 15, bottom: 15 })
      })
    }.width('100%')
     .justifyContent(FlexAlign.SpaceAround)
     .alignItems(VerticalAlign.Center)
  }
}

5.3 引入分类信息

        将分类Categroy.ets组件引入首页,由于Row() 容器默认是水平方向布局的,如果需要改为垂直显示,可以在内部添加Column() 容器。示例代码如下:

import Header from '../../components/Header';
import Navi from '../../components/Navigation';
import Banner from './components/Banner';
import CategoryList from './components/Category';

@Entry
@Component
struct Index {

  build() {
    RelativeContainer() {
      Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center }){
        Header()  // 顶部导航
        Row(){
          // 将屏幕中 剩余调度,设置为中间内容区域的调度
          // 这样可以使用Header和Navi组件,分别固定在页面的顶部和底部

          Column(){
            // 引入Banner图
            Banner()
            // 引入分类信息
            CategoryList()
          }.width('100%')

        }.layoutWeight(1)
         .width('100%')
         .alignItems(VerticalAlign.Top)
         .justifyContent(FlexAlign.Start)

        // 底部菜单栏
        Navi({ NavId: 1 })

      }.width('100%')
       .height('100%')
    }
    .height('100%')
    .width('100%')
  }
}

        页面效果如下图:

六、推荐商品

        推荐商品一般是根据用户的浏览历史、购买记录或兴趣爱好等信息,向用户推荐个性化的商品,通常位于像首页这种较明显位置。

6.1 定义数据类型

        在types/index.d.ts中定义商品信息的数据类型,代码如下:

/**
 * 商品信息
 */
interface goodsInfo {
  id: number
  name: string      // 商品名称
  thumb: string     // 商品图片
  price: number     // 商品价格
  dis_price: number // 折扣价格
}

6.2 API请求

        打开api/index.ts文件,定义获取首页推荐商品数据的API函数。示例代码如下:

import { standardInterfaceResult } from '../types/http'
import { httpRequest } from '../utils/request'

/**
 * 获取banner信息
 */
export const getBannerInfo = async () => {
  return await httpRequest.get<standardInterfaceResult>('/banner.php')
}

/**
 * 获取首页推荐商品
 */
export const getHomeGoods = async () => {
  return await httpRequest.get<standardInterfaceResult>('homeGoods.php')
}

6.3 获取推荐商品

        在HomeGoods组件模块中,在aboutToAppear()周期函数执行时,读取推荐商品信息。示例代码如下:

import { getHomeGoods } from "../../../api"

@Component
export default struct HomeGoods {
  @State goods: Array<goodsInfo> = []

  aboutToAppear(): void {
    getHomeGoods().then(res => {
      if (res.code == 200 && res.data) {
        this.goods = res.data['list']
        console.log('result', JSON.stringify(this.goods))
      }
    })
  }

  build() {

  }
}

        通过postman查看数据结构,如下图:

6.4 HomeGoods组件

        在src/main/ets/pages/Index/components/Goods.ets文件中,已成功获取推荐商品信息;接下,使用自定义构建函数,完成卡片式商品信息。示例代码如下:

import { getHomeGoods } from "../../../api"

@Component
export default struct HomeGoods {
  @State goods: Array<goodsInfo> = []

  aboutToAppear(): void {
    getHomeGoods().then(res => {
      if (res.code == 200 && res.data) {
        this.goods = res.data['list']
        // console.log('result', JSON.stringify(this.goods))
      }
    }).catch(() => {
      console.log('error')
    })
  }

  @Builder cardItem(item: goodsInfo) {
    Row({ space: 10 }){
      Column(){
        Image(item.thumb).width(100)  // 商品图片
      }
      Column(){
        // 商品标题
        Text(item.name).fontSize(16).fontWeight('bold')
        // 价格信息
        Row(){
          Text('价格:' + item.price).fontSize(18)
            .fontColor('#FF0000')
            .margin({ top: 10 })
          Text('原价:' + item.dis_price).fontSize(14)
            .fontColor('#999999')
            .margin({ left: 10 })
            .decoration({ type: TextDecorationType.LineThrough }) // 添加删除线
        }.alignItems(VerticalAlign.Bottom)
        // 介绍内容
        Text(item.intro).fontSize(14)
          .margin({ top: 15 })
      }.justifyContent(FlexAlign.Start)
       .layoutWeight(1)
      .alignItems(HorizontalAlign.Start)
    }.width('100%')
     .padding({ left: 10, right: 10 })
     .justifyContent(FlexAlign.Start) // 居左对齐
     .alignItems(VerticalAlign.Top)   // 顶部对齐
  }

  build() {
    Column({ space: 15 }){
      // 循环输出推荐商品信息,能使构建方法渲染卡片信息
      ForEach(this.goods, (item: goodsInfo, index) => {
        if (index > 0) {
          Divider() // 添加分割线
        }
        this.cardItem(item) // 渲染卡片
      })
    }.width('100%')
     .padding({ top: 15, bottom: 15 })
     .justifyContent(FlexAlign.Start)
  }
}

6.5 引入推荐商品

        在首页中,引入推荐商品HomeGoods组件模块,示例代码如下:

import Header from '../../components/Header';
import Navi from '../../components/Navigation';
import Banner from './components/Banner';
import CategoryList from './components/Category';
import HomeGoods from './components/Goods';

@Entry
@Component
struct Index {

  build() {
    RelativeContainer() {
      Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center }){
        Header()  // 顶部导航
        Row(){
          // 将屏幕中 剩余调度,设置为中间内容区域的调度
          // 这样可以使用Header和Navi组件,分别固定在页面的顶部和底部

          Column(){
            // 引入Banner图
            Banner()
            // 引入分类信息
            CategoryList()
            // 引入推荐商品
            HomeGoods()
          }.width('100%')

        }.layoutWeight(1)
         .width('100%')
         .alignItems(VerticalAlign.Top)
         .justifyContent(FlexAlign.Start)

        // 底部菜单栏
        Navi({ NavId: 1 })

      }.width('100%')
       .height('100%')
    }
    .height('100%')
    .width('100%')
  }
}

        页面效果如下图:

七、细节调整

        如上图可见,虽然首页已完成,不过还存在一些问题,接下来将一一来解决。

7.1 底部菜单栏

       如底部菜单栏与商品信息超出部分重叠了,这是因为底部菜单栏未添加背景色,添加后即可。

        在底部菜单栏的Row()容器上,添加backgroundColor为白色。示例代码如下图:

        页面效果如下图:

7.2 滚动效果

        另一个问题则是,页面超出部分无法上拉进行滚动显示,这是因为Column()或Row()容器,不会因为内容超出,而自动添加滚动功能;方法有很有多种,大家可根据实际需求选择即可。

7.2 Scroll组件

        在Column()容器外,再包裹一层Scroll组件即可,示例代码如下:

7.2 List容器

        另外,在之前案例中,使用过的List()容器,结合ListItem()一起使用,也可以在内容超出后,实现滚动功能。示例代码如下:

        最终页面效果如下图:

        首页中的五个组件在App应用中各自承担着不同的功能和作用,为用户提供了一些更便捷、高效、个性化的购买体验和通道。为用户更精细化的查找需求,提供多种选择方式。

        这篇就先讲到这,如果有更好建议,欢迎随时沟通交流!

猜你喜欢

转载自blog.csdn.net/jiciqiang/article/details/146296174
今日推荐