181.[HarmonyOS NEXT 实战案例九:Grid] 电商网格布局基础篇:打造精美商品展示页面

2025-06-30 23:01:10
104次阅读
0个评论

[HarmonyOS NEXT 实战案例九:Grid] 电商网格布局基础篇:打造精美商品展示页面

项目已开源,开源地址: https://gitcode.com/nutpi/HarmonyosNextCaseStudyTutorial , 欢迎fork & star

效果演示

image.png

1. 电商网格布局概述

在移动电商应用中,网格布局是展示商品的最佳选择之一,它能够以规整、美观的方式呈现多个商品项,并支持用户快速浏览和筛选。HarmonyOS NEXT 的 ArkUI 框架提供了强大的 Grid 和 GridItem 组件,使得开发者能够轻松实现各种复杂的电商网格布局。

1.1 电商网格布局的特点

特点 描述
规整的视觉呈现 以网格形式展示商品,视觉整齐,便于用户浏览
高效的空间利用 充分利用屏幕空间,展示更多商品信息
灵活的筛选排序 支持按类别、价格、销量等多维度筛选和排序
丰富的交互体验 支持点击查看详情、加入购物车等交互操作
响应式布局 能够适应不同屏幕尺寸和方向的变化

1.2 本案例实现的功能

本案例将实现一个完整的电商商品展示页面,包含以下功能:

  1. 商品网格展示:使用 Grid 和 GridItem 组件展示商品信息
  2. 分类筛选:支持按商品分类筛选商品
  3. 多维度排序:支持按价格、销量、评分等维度排序
  4. 搜索功能:支持按关键词搜索商品
  5. 商品详情:点击商品可查看详细信息
  6. 购物车功能:支持将商品加入购物车

2. 数据模型设计

2.1 商品数据模型

虽然我们无法直接查看 CommonTypes.ts 文件中的 EcommerceProduct 接口定义,但从代码中可以推断出其结构:

// 推断的 EcommerceProduct 接口结构
interface EcommerceProduct {
  id: number;                 // 商品ID
  name: string;               // 商品名称
  description: string;        // 商品描述
  price: number;              // 商品价格
  originalPrice?: number;     // 原价(可选)
  discount?: number;          // 折扣(可选)
  image: Resource;            // 商品主图
  images: Resource[];         // 商品图片集
  category: string;           // 商品分类
  brand: string;              // 品牌
  rating: number;             // 评分
  reviewCount: number;        // 评价数量
  salesCount: number;         // 销售数量
  stock: number;              // 库存
  tags: string[];             // 标签
  isHot: boolean;             // 是否热销
  isNew: boolean;             // 是否新品
  isFreeShipping: boolean;    // 是否免运费
  specifications: {           // 规格
    color?: string[];         // 颜色选项
    size?: string[];          // 尺寸选项
    material?: string;        // 材质
  };
  seller: {                   // 卖家信息
    name: string;             // 卖家名称
    rating: number;           // 卖家评分
    location: string;         // 卖家位置
  };
}

2.2 购物车数据模型

interface CateItem {
  productId: number;  // 商品ID
  quantity: number;   // 数量
}

2.3 价格范围数据模型

interface PriceRange {
  min: number;  // 最低价格
  max: number;  // 最高价格
}

3. 状态管理设计

在电商应用中,有效的状态管理对于实现复杂的交互逻辑至关重要。本案例使用 ArkUI 的 @State 装饰器来管理组件的状态:

@State products: EcommerceProduct[] = [...]       // 商品数据
@State cartItems: CateItem[] = []                 // 购物车项目
@State selectedCategory: string = '全部'           // 选中的分类
@State sortBy: string = '综合'                     // 排序方式
@State priceRange: PriceRange = { min: 0, max: 20000 } // 价格范围
@State searchKeyword: string = ''                 // 搜索关键词
@State showProductDetail: boolean = false         // 是否显示商品详情
@State selectedProduct: EcommerceProduct = {...}  // 选中的商品
@State showCart: boolean = false                  // 是否显示购物车

这些状态变量使得我们能够:

  1. 跟踪用户的筛选和排序选择
  2. 管理购物车中的商品
  3. 控制商品详情和购物车的显示与隐藏
  4. 记录当前选中的商品

4. 辅助功能实现

4.1 商品过滤与排序

getFilteredProducts(): EcommerceProduct[] {
  let filtered = this.products

  // 分类过滤
  if (this.selectedCategory !== '全部') {
    filtered = filtered.filter(product => product.category === this.selectedCategory)
  }

  // 价格过滤
  filtered = filtered.filter(product =>
    product.price >= this.priceRange.min && product.price <= this.priceRange.max
  )

  // 搜索过滤
  if (this.searchKeyword.trim() !== '') {
    filtered = filtered.filter(product =>
      product.name.includes(this.searchKeyword) ||
      product.description.includes(this.searchKeyword) ||
      product.brand.includes(this.searchKeyword) ||
      product.tags.some(tag => tag.includes(this.searchKeyword))
    )
  }

  // 排序
  switch (this.sortBy) {
    case '价格升序':
      filtered.sort((a, b) => a.price - b.price)
      break
    case '价格降序':
      filtered.sort((a, b) => b.price - a.price)
      break
    case '销量':
      filtered.sort((a, b) => b.salesCount - a.salesCount)
      break
    case '评分':
      filtered.sort((a, b) => b.rating - a.rating)
      break
    default: // 综合
      filtered.sort((a, b) => {
        const scoreA = a.salesCount * 0.3 + a.rating * 1000 + (a.isHot ? 500 : 0)
        const scoreB = b.salesCount * 0.3 + b.rating * 1000 + (b.isHot ? 500 : 0)
        return scoreB - scoreA
      })
  }

  return filtered
}

这个方法实现了多维度的商品过滤和排序:

  1. 分类过滤:根据用户选择的商品分类进行过滤
  2. 价格过滤:根据设定的价格范围过滤商品
  3. 关键词搜索:在商品名称、描述、品牌和标签中搜索关键词
  4. 多维度排序:支持按价格升序/降序、销量、评分排序,以及综合排序(考虑销量、评分和热销标志)

4.2 购物车功能

// 添加到购物车
addToCart(productId: number, quantity: number = 1) {
  const existingItem = this.cartItems.find(item => item.productId === productId)
  if (existingItem) {
    existingItem.quantity += quantity
  } else {
    this.cartItems.push({ productId, quantity })
  }
}

// 获取购物车商品数量
getCartItemCount(): number {
  return this.cartItems.reduce((total, item) => total + item.quantity, 0)
}

// 获取购物车总价
getCartTotal(): number {
  return this.cartItems.reduce((total, item) => {
    const product = this.products.find(p => p.id === item.productId)
    return total + (product ? product.price * item.quantity : 0)
  }, 0)
}

这些方法实现了购物车的核心功能:

  1. 添加商品:将商品添加到购物车,如果已存在则增加数量
  2. 计算总数量:计算购物车中所有商品的总数量
  3. 计算总价:计算购物车中所有商品的总价

4.3 数字格式化

// 格式化数字
formatNumber(num: number): string {
  if (num >= 10000) {
    return `${(num / 10000).toFixed(1)}万`
  } else if (num >= 1000) {
    return `${(num / 1000).toFixed(1)}k`
  }
  return num.toString()
}

这个方法将大数字格式化为更易读的形式,例如将 15600 格式化为 "1.6万",提升用户体验。

5. 页面结构设计

5.1 整体布局

电商商品展示页面的整体布局如下:

build() {
  Stack() {
    Column() {
      // 顶部搜索和购物车
      Row() { ... }
      
      // 分类和排序
      Column() { ... }
      
      // 商品网格
      Grid() { ... }
    }
    .width('100%')
    .height('100%')

    // 商品详情对话框
    if (this.showProductDetail) { ... }

    // 购物车浮层
    if (this.showCart) { ... }
  }
  .width('100%')
  .height('100%')
}

整体布局采用 Stack 组件作为根容器,包含三个主要部分:

  1. 主内容区域:包含顶部搜索栏、分类和排序选项、商品网格
  2. 商品详情对话框:点击商品时显示的详情浮层
  3. 购物车浮层:点击购物车图标时显示的浮层

5.2 顶部搜索和购物车

// 顶部搜索和购物车
Row() {
  Row() {
    Image($r('app.media.search_icon'))
      .width(20)
      .height(20)
      .fillColor('#999999')
      .margin({ left: 12 })

    TextInput({ placeholder: '搜索商品' })
      .fontSize(16)
      .backgroundColor('transparent')
      .border({ width: 0 })
      .layoutWeight(1)
      .margin({ left: 8, right: 12 })
      .onChange((value: string) => {
        this.searchKeyword = value
      })
  }
  .width('100%')
  .height(44)
  .backgroundColor('#F5F5F5')
  .borderRadius(22)
  .layoutWeight(1)

  Button() {
    Stack({ alignContent: Alignment.TopEnd }) {
      Image($r('app.media.cart_icon'))
        .width(24)
        .height(24)
        .fillColor('#333333')

      if (this.getCartItemCount() > 0) {
        Text(this.getCartItemCount().toString())
          .fontSize(10)
          .fontColor('#FFFFFF')
          .backgroundColor('#FF3B30')
          .width(16)
          .height(16)
          .borderRadius(8)
          .textAlign(TextAlign.Center)
          .margin({ top: -4, right: -4 })
      }
    }
  }
  .width(44)
  .height(44)
  .borderRadius(22)
  .backgroundColor('#F0F0F0')
  .margin({ left: 12 })
  .onClick(() => {
    this.showCart = true
  })
}
.width('100%')
.padding({ left: 16, right: 16, top: 16, bottom: 16 })
.backgroundColor('#FFFFFF')

顶部区域包含:

  1. 搜索框:用户可以输入关键词搜索商品
  2. 购物车按钮:显示购物车图标和商品数量,点击后显示购物车浮层

5.3 分类和排序区域

// 分类和排序
Column() {
  // 分类标签
  Scroll() {
    Row() {
      ForEach(this.categories, (category:string, index) => {
        Button(category)
          .fontSize(14)
          .fontColor(this.selectedCategory === category ? '#FFFFFF' : '#333333')
          .backgroundColor(this.selectedCategory === category ? '#007AFF' : '#F0F0F0')
          .borderRadius(16)
          .padding({ left: 16, right: 16, top: 8, bottom: 8 })
          .margin({ right: index < this.categories.length - 1 ? 8 : 0 })
          .onClick(() => {
            this.selectedCategory = category
          })
      })
    }
  }
  .scrollable(ScrollDirection.Horizontal)
  .scrollBar(BarState.Off)
  .width('100%')
  .margin({ bottom: 12 })

  // 排序选项
  Row() {
    Text('排序:')
      .fontSize(14)
      .fontColor('#666666')

    ForEach(this.sortOptions, (option:string, index) => {
      Button(option)
        .fontSize(12)
        .fontColor(this.sortBy === option ? '#007AFF' : '#666666')
        .backgroundColor('transparent')
        .border({
          width: 1,
          color: this.sortBy === option ? '#007AFF' : '#E0E0E0'
        })
        .borderRadius(12)
        .padding({ left: 12, right: 12, top: 4, bottom: 4 })
        .margin({ left: 8 })
        .onClick(() => {
          this.sortBy = option
        })
    })

    Blank()
  }
  .width('100%')
}
.width('100%')
.padding({ left: 16, right: 16, bottom: 16 })
.backgroundColor('#FFFFFF')

分类和排序区域包含:

  1. 分类标签:水平滚动的分类按钮,用户可以选择不同的商品分类
  2. 排序选项:多个排序按钮,用户可以选择不同的排序方式

6. 商品网格实现

6.1 Grid 组件配置

// 商品网格
Grid() {
  ForEach(this.getFilteredProducts(), (product: EcommerceProduct) => {
    GridItem() {
      // GridItem 内容...
    }
  })
}
.columnsTemplate('1fr 1fr')
.rowsGap(12)
.columnsGap(12)
.width('100%')
.layoutWeight(1)
.padding({ left: 16, right: 16, bottom: 16 })
.backgroundColor('#F8F8F8')

Grid 组件的关键配置:

  1. columnsTemplate:设置为 '1fr 1fr',表示两列等宽布局
  2. rowsGap:行间距为 12 像素
  3. columnsGap:列间距为 12 像素
  4. layoutWeight:设置为 1,使网格区域占据剩余空间

6.2 GridItem 商品卡片实现

GridItem() {
  Column() {
    // 商品图片
    Stack({ alignContent: Alignment.TopStart }) {
      Image(product.image)
        .width('100%')
        .height(160)
        .objectFit(ImageFit.Cover)
        .borderRadius({ topLeft: 12, topRight: 12 })

      // 标签
      Column() {
        if (product.isHot) {
          Text('热销')
            .fontSize(10)
            .fontColor('#FFFFFF')
            .backgroundColor('#FF9500')
            .padding({ left: 6, right: 6, top: 2, bottom: 2 })
            .borderRadius(4)
            .margin({ bottom: 4 })
        }

        if (product.isNew) {
          Text('新品')
            .fontSize(10)
            .fontColor('#FFFFFF')
            .backgroundColor('#34C759')
            .padding({ left: 6, right: 6, top: 2, bottom: 2 })
            .borderRadius(4)
        }
      }
      .margin({ top: 8, left: 8 })
    }

    // 商品信息
    Column() {
      Text(product.name)
        .fontSize(14)
        .fontWeight(FontWeight.Bold)
        .fontColor('#333333')
        .maxLines(2)
        .textOverflow({ overflow: TextOverflow.Ellipsis })
        .width('100%')
        .textAlign(TextAlign.Start)
        .margin({ bottom: 6 })

      // 价格
      Row() {
        Text(`¥${product.price}`)
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FF3B30')

        if (product.originalPrice) {
          Text(`¥${product.originalPrice}`)
            .fontSize(12)
            .fontColor('#999999')
            .decoration({ type: TextDecorationType.LineThrough })
            .margin({ left: 6 })
        }

        Blank()
      }
      .width('100%')
      .margin({ bottom: 6 })

      // 评分和销量
      Row() {
        Row() {
          ForEach([1,2,3,4,5], (star:number) => {
            Image(star <= product.rating ? $r('app.media.heart_filled') : $r('app.media.heart_outline'))
              .width(10)
              .height(10)
              .fillColor(star <= product.rating ? '#FFD700' : '#E0E0E0')
          })

          Text(`${product.rating}`)
            .fontSize(10)
            .fontColor('#666666')
            .margin({ left: 2 })
        }

        Blank()

        Text(`${this.formatNumber(product.salesCount)}已售`)
          .fontSize(10)
          .fontColor('#999999')
      }
      .width('100%')
      .margin({ bottom: 8 })

      // 添加到购物车按钮
      Button('加入购物车')
        .fontSize(12)
        .fontColor('#FFFFFF')
        .backgroundColor('#007AFF')
        .borderRadius(16)
        .width('100%')
        .height(32)
        .onClick(() => {
          this.addToCart(product.id)
        })
    }
    .padding(12)
    .alignItems(HorizontalAlign.Start)
  }
  .width('100%')
  .backgroundColor('#FFFFFF')
  .borderRadius(12)
  .shadow({
    radius: 6,
    color: 'rgba(0, 0, 0, 0.1)',
    offsetX: 0,
    offsetY: 2
  })
  .onClick(() => {
    this.selectedProduct = product
    this.showProductDetail = true
  })
}

每个 GridItem 包含一个商品卡片,结构如下:

  1. 商品图片:顶部显示商品主图,左上角显示热销/新品标签
  2. 商品名称:最多显示两行,超出部分使用省略号
  3. 价格信息:显示当前价格和原价(如果有折扣)
  4. 评分和销量:显示星级评分和销售数量
  5. 加入购物车按钮:点击后将商品添加到购物车

整个卡片添加了圆角和阴影效果,提升了视觉层次感,点击卡片会显示商品详情对话框。

7. 商品详情对话框

@Builder
ProductDetailDialog() {
  if (this.selectedProduct) {
    Column() {
      // 商品图片
      Image(this.selectedProduct.image)
        .width('100%')
        .height(300)
        .objectFit(ImageFit.Cover)
        .borderRadius({ topLeft: 16, topRight: 16 })

      // 商品信息
      Column() {
        // 价格和标签
        Row() { ... }
        
        // 商品名称
        Text(this.selectedProduct.name) ... 
        
        // 商品描述
        Text(this.selectedProduct.description) ... 
        
        // 评分和销量
        Row() { ... }
        
        // 商品标签
        Row() { ... }
        
        // 操作按钮
        Row() {
          Button('加入购物车') ... 
          Button('立即购买') ... 
        }
      }
      .padding(20)
      .alignItems(HorizontalAlign.Start)
    }
    .width('95%')
    .height('85%')
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
  }
}

商品详情对话框以浮层形式展示,包含:

  1. 商品大图:顶部显示商品主图
  2. 价格信息:显示当前价格、原价和折扣信息
  3. 商品名称和描述:详细展示商品信息
  4. 评分和销量:以星级形式展示评分,并显示评价数量和销售数量
  5. 商品标签:展示商品的所有标签
  6. 操作按钮:提供"加入购物车"和"立即购买"两个操作

8. Grid 组件在电商场景中的优势

8.1 Grid 组件的核心优势

优势 描述
灵活的布局控制 通过 columnsTemplate 属性可以轻松设置列数和宽度比例
间距控制 rowsGap 和 columnsGap 属性可以精确控制商品卡片之间的间距
自动换行 自动将超出当前行的商品卡片放置到下一行
响应式布局 可以根据屏幕尺寸动态调整列数和卡片大小
高性能渲染 对于大量商品数据,Grid 组件能够保持良好的性能

8.2 电商商品卡片设计技巧

  1. 视觉层次:使用阴影和圆角创造立体感,突出商品卡片
  2. 信息优先级:将最重要的信息(图片、名称、价格)放在最显眼的位置
  3. 标签设计:使用醒目的颜色标识热销、新品等特殊属性
  4. 文本处理:对长文本使用 maxLines 和 textOverflow 属性处理溢出
  5. 价格展示:使用不同的字体大小和颜色区分当前价格和原价
  6. 评分可视化:使用星级图标直观展示商品评分
  7. 操作按钮:提供明确的加入购物车按钮,增强交互性

9. 总结

在本教程中,我们学习了如何使用 HarmonyOS NEXT 的 Grid 和 GridItem 组件创建一个功能完善的电商商品展示页面。我们详细讲解了数据模型设计、状态管理、过滤排序功能、商品网格布局实现以及商品详情对话框的创建。

收藏00

登录 后评论。没有帐号? 注册 一个。