183.[HarmonyOS NEXT 实战案例九:Grid] 电商网格布局高级篇:复杂场景与性能优化

2025-06-30 23:02:11
106次阅读
0个评论

[HarmonyOS NEXT 实战案例九:Grid] 电商网格布局高级篇:复杂场景与性能优化

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

效果演示

image.png

1. 电商网格布局高级应用概述

在前两篇教程中,我们学习了电商网格布局的基础实现和进阶特性。本篇教程将深入探讨电商网格布局的高级应用,包括复杂业务场景实现、高级交互技术、性能优化策略等,帮助开发者打造出专业级的电商应用。

1.1 高级应用概览

高级应用 描述
复杂业务场景 多样化商品卡片、混合布局、推荐系统
高级交互技术 手势操作、拖拽排序、下拉刷新
性能优化策略 虚拟列表、懒加载、图片优化
数据管理 分页加载、缓存策略、状态管理
特殊布局 瀑布流商品展示、分组展示、促销专区

2. 复杂业务场景实现

2.1 多样化商品卡片

在实际电商应用中,不同类型的商品可能需要不同样式的卡片展示,例如普通商品、促销商品、限时抢购商品等。

// 商品卡片类型枚举
enum ProductCardType {
  NORMAL,    // 普通商品
  PROMOTION, // 促销商品
  FLASH_SALE, // 限时抢购
  NEW_ARRIVAL, // 新品
  RECOMMEND   // 推荐商品
}

// 根据商品属性确定卡片类型
getCardType(product: EcommerceProduct): ProductCardType {
  if (product.isFlashSale) {
    return ProductCardType.FLASH_SALE
  } else if (product.discount && product.discount < 0.8) {
    return ProductCardType.PROMOTION
  } else if (product.isNew) {
    return ProductCardType.NEW_ARRIVAL
  } else if (product.isRecommended) {
    return ProductCardType.RECOMMEND
  } else {
    return ProductCardType.NORMAL
  }
}

// 多样化商品卡片构建器
@Builder
ProductCard(product: EcommerceProduct) {
  const cardType = this.getCardType(product)
  
  Column() {
    // 商品图片区域
    Stack({ alignContent: Alignment.TopStart }) {
      Image(product.image)
        .width('100%')
        .height(cardType === ProductCardType.PROMOTION ? 180 : 160)
        .objectFit(ImageFit.Cover)
        .borderRadius({ topLeft: 12, topRight: 12 })

      // 根据卡片类型显示不同的标签
      if (cardType === ProductCardType.FLASH_SALE) {
        Row() {
          Image($r('app.media.flash_sale'))
            .width(16)
            .height(16)
            .margin({ right: 4 })
          
          Text('限时抢购')
            .fontSize(12)
            .fontColor('#FFFFFF')
          
          // 倒计时
          Text(this.getFlashSaleTimeRemaining(product))
            .fontSize(12)
            .fontColor('#FFFFFF')
            .margin({ left: 4 })
        }
        .padding({ left: 8, right: 8, top: 4, bottom: 4 })
        .backgroundColor('#FF3B30')
        .borderRadius(12)
        .margin(8)
      } else if (cardType === ProductCardType.PROMOTION) {
        Text(`${product.discount * 10}折`)
          .fontSize(12)
          .fontColor('#FFFFFF')
          .backgroundColor('#FF9500')
          .borderRadius(12)
          .padding({ left: 8, right: 8, top: 4, bottom: 4 })
          .margin(8)
      } else if (cardType === ProductCardType.NEW_ARRIVAL) {
        Text('新品')
          .fontSize(12)
          .fontColor('#FFFFFF')
          .backgroundColor('#34C759')
          .borderRadius(12)
          .padding({ left: 8, right: 8, top: 4, bottom: 4 })
          .margin(8)
      } else if (cardType === ProductCardType.RECOMMEND) {
        Text('推荐')
          .fontSize(12)
          .fontColor('#FFFFFF')
          .backgroundColor('#007AFF')
          .borderRadius(12)
          .padding({ left: 8, right: 8, top: 4, bottom: 4 })
          .margin(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 })

      // 价格区域 - 根据卡片类型显示不同样式
      if (cardType === ProductCardType.FLASH_SALE) {
        Row() {
          Text(`¥${product.price}`)
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FF3B30')

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

          Blank()

          Text(`已抢${Math.floor(product.salesCount / product.stock * 100)}%`)
            .fontSize(12)
            .fontColor('#FF3B30')
        }
        .width('100%')
        .margin({ bottom: 6 })

        // 进度条
        Row() {
          Row()
            .width(`${Math.floor(product.salesCount / product.stock * 100)}%`)
            .height(4)
            .backgroundColor('#FF3B30')
            .borderRadius(2)
        }
        .width('100%')
        .height(4)
        .backgroundColor('#F0F0F0')
        .borderRadius(2)
        .margin({ bottom: 8 })
      } else {
        // 普通价格显示
        Row() {
          Text(`¥${product.price}`)
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FF3B30')

          if (product.originalPrice && product.originalPrice > product.price) {
            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.star_filled') : $r('app.media.star_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 })
      }

      // 操作按钮 - 根据卡片类型显示不同按钮
      if (cardType === ProductCardType.FLASH_SALE) {
        Button('立即抢购')
          .fontSize(12)
          .fontColor('#FFFFFF')
          .backgroundColor('#FF3B30')
          .borderRadius(16)
          .width('100%')
          .height(32)
          .onClick(() => {
            this.addToCart(product.id)
          })
      } else if (cardType === ProductCardType.PROMOTION) {
        Button('立即购买')
          .fontSize(12)
          .fontColor('#FFFFFF')
          .backgroundColor('#FF9500')
          .borderRadius(16)
          .width('100%')
          .height(32)
          .onClick(() => {
            this.addToCart(product.id)
          })
      } else {
        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
  })
  // 根据卡片类型应用不同的样式
  .border(cardType === ProductCardType.FLASH_SALE ? {
    width: 2,
    color: '#FF3B30',
    style: BorderStyle.Solid
  } : cardType === ProductCardType.PROMOTION ? {
    width: 2,
    color: '#FF9500',
    style: BorderStyle.Solid
  } : null)
  .onClick(() => {
    this.selectedProduct = product
    this.showProductDetail = true
  })
}

// 获取限时抢购剩余时间
getFlashSaleTimeRemaining(product: EcommerceProduct): string {
  // 这里假设 product 有 flashSaleEndTime 属性
  const endTime = product.flashSaleEndTime
  const now = new Date().getTime()
  const remaining = Math.max(0, endTime - now)
  
  const hours = Math.floor(remaining / (60 * 60 * 1000))
  const minutes = Math.floor((remaining % (60 * 60 * 1000)) / (60 * 1000))
  const seconds = Math.floor((remaining % (60 * 1000)) / 1000)
  
  return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
}

这个多样化商品卡片构建器根据商品的不同属性(限时抢购、促销、新品、推荐)显示不同样式的卡片,包括不同的标签、价格展示方式、进度条和操作按钮,使商品展示更加丰富多样。

2.2 混合布局

在实际电商应用中,首页通常会包含多种布局,如轮播图、分类导航、促销专区、商品网格等。我们可以实现一个混合布局的首页。

build() {
  Scroll() {
    Column() {
      // 顶部搜索栏
      this.SearchBar()
      
      // 轮播图
      this.BannerCarousel()
      
      // 分类导航
      this.CategoryNav()
      
      // 促销专区
      this.PromotionSection()
      
      // 限时抢购
      this.FlashSaleSection()
      
      // 新品专区
      this.NewArrivalsSection()
      
      // 推荐商品网格
      this.RecommendProductsGrid()
    }
    .width('100%')
  }
  .scrollBar(BarState.Off)
  .scrollable(ScrollDirection.Vertical)
  .width('100%')
  .height('100%')
}

// 轮播图
@Builder
BannerCarousel() {
  Swiper() {
    ForEach(this.banners, (banner) => {
      Image(banner.image)
        .width('100%')
        .height('100%')
        .objectFit(ImageFit.Cover)
        .borderRadius(12)
        .onClick(() => {
          // 处理轮播图点击
        })
    })
  }
  .width('100%')
  .height(180)
  .autoPlay(true)
  .interval(3000)
  .indicator(true)
  .indicatorStyle({
    selectedColor: '#007AFF',
    color: '#CCCCCC',
    size: 8
  })
  .margin({ top: 12, bottom: 16 })
  .padding({ left: 16, right: 16 })
}

// 分类导航
@Builder
CategoryNav() {
  Column() {
    Text('商品分类')
      .fontSize(16)
      .fontWeight(FontWeight.Bold)
      .fontColor('#333333')
      .width('100%')
      .textAlign(TextAlign.Start)
      .padding({ left: 16, right: 16 })
      .margin({ bottom: 12 })
    
    Grid() {
      ForEach(this.categories, (category:string, index) => {
        GridItem() {
          Column() {
            Image($r(`app.media.category_${index % 10}`))
              .width(48)
              .height(48)
              .margin({ bottom: 8 })
            
            Text(category)
              .fontSize(12)
              .fontColor('#333333')
          }
          .width('100%')
          .height('100%')
          .justifyContent(FlexAlign.Center)
          .onClick(() => {
            this.selectedCategory = category
          })
        }
      })
    }
    .columnsTemplate('1fr 1fr 1fr 1fr 1fr')
    .rowsTemplate('1fr')
    .width('100%')
    .height(90)
    .padding({ left: 16, right: 16 })
    .margin({ bottom: 16 })
  }
  .width('100%')
  .backgroundColor('#FFFFFF')
  .padding({ top: 16, bottom: 16 })
  .margin({ bottom: 8 })
}

// 促销专区
@Builder
PromotionSection() {
  Column() {
    Row() {
      Text('促销专区')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor('#333333')
      
      Blank()
      
      Button() {
        Row() {
          Text('查看更多')
            .fontSize(12)
            .fontColor('#666666')
          
          Image($r('app.media.arrow_right'))
            .width(12)
            .height(12)
            .fillColor('#666666')
            .margin({ left: 4 })
        }
      }
      .backgroundColor('transparent')
      .padding(0)
      .onClick(() => {
        // 查看更多促销商品
      })
    }
    .width('100%')
    .padding({ left: 16, right: 16 })
    .margin({ bottom: 12 })
    
    List() {
      ForEach(this.getPromotionProducts(), (product: EcommerceProduct) => {
        ListItem() {
          this.PromotionProductCard(product)
        }
        .margin({ right: 12 })
      })
    }
    .listDirection(Axis.Horizontal)
    .scrollBar(BarState.Off)
    .width('100%')
    .height(220)
    .padding({ left: 16, right: 4 })
  }
  .width('100%')
  .backgroundColor('#FFFFFF')
  .padding({ top: 16, bottom: 16 })
  .margin({ bottom: 8 })
}

// 促销商品卡片
@Builder
PromotionProductCard(product: EcommerceProduct) {
  Column() {
    Image(product.image)
      .width(140)
      .height(140)
      .objectFit(ImageFit.Cover)
      .borderRadius(12)
    
    Text(product.name)
      .fontSize(14)
      .fontWeight(FontWeight.Bold)
      .fontColor('#333333')
      .maxLines(1)
      .textOverflow({ overflow: TextOverflow.Ellipsis })
      .width('100%')
      .textAlign(TextAlign.Start)
      .margin({ top: 8, bottom: 4 })
    
    Row() {
      Text(`¥${product.price}`)
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor('#FF3B30')
      
      Text(`¥${product.originalPrice}`)
        .fontSize(12)
        .fontColor('#999999')
        .decoration({ type: TextDecorationType.LineThrough })
        .margin({ left: 6 })
    }
    .width('100%')
  }
  .width(140)
  .padding(8)
  .backgroundColor('#FFFFFF')
  .borderRadius(12)
  .shadow({
    radius: 4,
    color: 'rgba(0, 0, 0, 0.1)',
    offsetX: 0,
    offsetY: 2
  })
  .onClick(() => {
    this.selectedProduct = product
    this.showProductDetail = true
  })
}

// 限时抢购专区
@Builder
FlashSaleSection() {
  // 类似促销专区的实现
}

// 新品专区
@Builder
NewArrivalsSection() {
  // 类似促销专区的实现
}

// 推荐商品网格
@Builder
RecommendProductsGrid() {
  Column() {
    Row() {
      Text('为你推荐')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor('#333333')
    }
    .width('100%')
    .padding({ left: 16, right: 16 })
    .margin({ bottom: 12 })
    
    Grid() {
      ForEach(this.getRecommendProducts(), (product: EcommerceProduct) => {
        GridItem() {
          this.ProductCard(product)
        }
      })
    }
    .columnsTemplate('1fr 1fr')
    .rowsGap(12)
    .columnsGap(12)
    .width('100%')
    .padding({ left: 16, right: 16, bottom: 16 })
  }
  .width('100%')
  .backgroundColor('#FFFFFF')
  .padding({ top: 16, bottom: 16 })
}

// 获取促销商品
getPromotionProducts(): EcommerceProduct[] {
  return this.products.filter(product => product.discount && product.discount < 0.8)
    .sort((a, b) => a.discount - b.discount)
    .slice(0, 10)
}

// 获取推荐商品
getRecommendProducts(): EcommerceProduct[] {
  return this.products.filter(product => product.isRecommended)
    .sort((a, b) => {
      const scoreA = a.salesCount * 0.3 + a.rating * 1000 + a.reviewCount * 0.5
      const scoreB = b.salesCount * 0.3 + b.rating * 1000 + b.reviewCount * 0.5
      return scoreB - scoreA
    })
}

这个混合布局首页包含多个不同的区域:

  1. 顶部搜索栏:用于搜索商品
  2. 轮播图:展示促销活动、新品上市等信息
  3. 分类导航:快速进入不同商品分类
  4. 促销专区:水平滚动列表展示促销商品
  5. 限时抢购:展示限时抢购商品
  6. 新品专区:展示新上市商品
  7. 推荐商品网格:使用 Grid 组件展示推荐商品

这种混合布局能够在有限的屏幕空间内展示更多样化的商品,提升用户体验和转化率。

3. 高级交互技术

3.1 手势操作

在电商应用中,手势操作可以提供更自然、流畅的交互体验。例如,我们可以实现商品卡片的左右滑动操作。

@State swipeOffset: number = 0
@State isSwipeActionVisible: boolean = false
@State swipeThreshold: number = 80

@Builder
SwipeableProductCard(product: EcommerceProduct) {
  Row() {
    // 商品卡片主体
    Column() {
      // 商品卡片内容...
    }
    .width('100%')
    .backgroundColor('#FFFFFF')
    .borderRadius(12)
    .translate({ x: this.swipeOffset })
    .gesture(
      PanGesture({ direction: PanDirection.Horizontal })
        .onActionStart(() => {
          // 开始滑动
        })
        .onActionUpdate((event: GestureEvent) => {
          // 限制只能向左滑动(显示操作按钮)
          if (event.offsetX < 0) {
            this.swipeOffset = Math.max(-this.swipeThreshold, event.offsetX)
          } else {
            this.swipeOffset = 0
          }
        })
        .onActionEnd(() => {
          // 根据滑动距离决定是否显示操作按钮
          if (this.swipeOffset < -this.swipeThreshold / 2) {
            this.swipeOffset = -this.swipeThreshold
            this.isSwipeActionVisible = true
          } else {
            this.swipeOffset = 0
            this.isSwipeActionVisible = false
          }
        })
    )
    
    // 滑动后显示的操作按钮
    Row() {
      Button() {
        Image($r('app.media.favorite'))
          .width(24)
          .height(24)
          .fillColor('#FFFFFF')
      }
      .width(60)
      .height('100%')
      .backgroundColor('#FF9500')
      .onClick(() => {
        // 添加到收藏
        this.addToFavorite(product.id)
        this.swipeOffset = 0
        this.isSwipeActionVisible = false
      })
      
      Button() {
        Image($r('app.media.cart_add'))
          .width(24)
          .height(24)
          .fillColor('#FFFFFF')
      }
      .width(60)
      .height('100%')
      .backgroundColor('#007AFF')
      .onClick(() => {
        // 添加到购物车
        this.addToCart(product.id)
        this.swipeOffset = 0
        this.isSwipeActionVisible = false
      })
    }
    .width(this.swipeThreshold)
    .height('100%')
    .margin({ left: -this.swipeThreshold })
  }
  .width('100%')
  .height(240)
  .clip(true)
}

这个可滑动的商品卡片实现了左滑显示操作按钮(收藏和加入购物车)的功能,提供了更丰富的交互方式。

3.2 下拉刷新与上拉加载

在电商应用中,下拉刷新和上拉加载是常见的交互模式,用于更新和加载更多商品数据。

@State isRefreshing: boolean = false
@State isLoadingMore: boolean = false
@State hasMoreData: boolean = true
@State currentPage: number = 1
@State pageSize: number = 20

build() {
  Refresh({ refreshing: $$this.isRefreshing, offset: 120, friction: 66 }) {
    List() {
      // 商品网格头部
      ListItem() {
        Column() {
          // 顶部搜索和筛选区域
          // ...
        }
      }
      
      // 商品网格
      ListItem() {
        Grid() {
          ForEach(this.getFilteredProducts(), (product: EcommerceProduct) => {
            GridItem() {
              this.ProductCard(product)
            }
          })
        }
        .columnsTemplate('1fr 1fr')
        .rowsGap(12)
        .columnsGap(12)
        .width('100%')
        .padding({ left: 16, right: 16, bottom: 16 })
      }
      
      // 加载更多指示器
      if (this.hasMoreData) {
        ListItem() {
          Row() {
            if (this.isLoadingMore) {
              LoadingProgress()
                .width(24)
                .height(24)
                .color('#999999')
                .margin({ right: 8 })
            }
            
            Text(this.isLoadingMore ? '正在加载更多...' : '上拉加载更多')
              .fontSize(14)
              .fontColor('#999999')
          }
          .width('100%')
          .justifyContent(FlexAlign.Center)
          .padding({ top: 12, bottom: 12 })
        }
      } else {
        ListItem() {
          Text('没有更多商品了')
            .fontSize(14)
            .fontColor('#999999')
            .width('100%')
            .textAlign(TextAlign.Center)
            .padding({ top: 12, bottom: 12 })
        }
      }
    }
    .width('100%')
    .height('100%')
    .onReachEnd(() => {
      if (this.hasMoreData && !this.isLoadingMore) {
        this.loadMoreData()
      }
    })
  }
  .onRefresh(() => {
    this.refreshData()
  })
}

// 刷新数据
async refreshData() {
  this.isRefreshing = true
  
  try {
    // 模拟网络请求
    await this.fetchProducts(1)
    this.currentPage = 1
    this.hasMoreData = true
  } catch (error) {
    console.error('刷新数据失败', error)
  } finally {
    this.isRefreshing = false
  }
}

// 加载更多数据
async loadMoreData() {
  if (this.isLoadingMore) return
  
  this.isLoadingMore = true
  
  try {
    // 模拟网络请求
    const hasMore = await this.fetchProducts(this.currentPage + 1)
    this.currentPage++
    this.hasMoreData = hasMore
  } catch (error) {
    console.error('加载更多数据失败', error)
  } finally {
    this.isLoadingMore = false
  }
}

// 模拟获取商品数据
async fetchProducts(page: number): Promise<boolean> {
  return new Promise((resolve) => {
    setTimeout(() => {
      if (page === 1) {
        // 刷新数据,替换现有数据
        this.products = this.generateMockProducts(this.pageSize)
      } else {
        // 加载更多,追加数据
        const moreProducts = this.generateMockProducts(this.pageSize)
        this.products = [...this.products, ...moreProducts]
      }
      
      // 模拟是否还有更多数据
      resolve(page < 5) // 假设只有5页数据
    }, 1500) // 模拟网络延迟
  })
}

// 生成模拟商品数据
generateMockProducts(count: number): EcommerceProduct[] {
  const products: EcommerceProduct[] = []
  const startId = (this.currentPage - 1) * this.pageSize + 1
  
  for (let i = 0; i < count; i++) {
    const id = startId + i
    products.push({
      id,
      name: `商品${id}`,
      description: `这是商品${id}的详细描述,包含了商品的各种信息。`,
      price: Math.floor(Math.random() * 1000) + 100,
      originalPrice: Math.floor(Math.random() * 1000) + 500,
      discount: Math.random() * 0.5 + 0.5,
      image: $r('app.media.product_placeholder'),
      images: [$r('app.media.product_placeholder')],
      category: this.categories[Math.floor(Math.random() * this.categories.length)],
      brand: `品牌${Math.floor(Math.random() * 10) + 1}`,
      rating: Math.random() * 3 + 2,
      reviewCount: Math.floor(Math.random() * 1000),
      salesCount: Math.floor(Math.random() * 10000),
      stock: Math.floor(Math.random() * 100) + 10,
      tags: ['标签1', '标签2', '标签3'].slice(0, Math.floor(Math.random() * 3) + 1),
      isHot: Math.random() > 0.7,
      isNew: Math.random() > 0.8,
      isFreeShipping: Math.random() > 0.5,
      specifications: {
        color: ['红色', '蓝色', '黑色'],
        size: ['S', 'M', 'L', 'XL'],
        material: '棉质'
      },
      seller: {
        name: `卖家${Math.floor(Math.random() * 10) + 1}`,
        rating: Math.random() * 2 + 3,
        location: '北京'
      }
    })
  }
  
  return products
}

这个实现包含了下拉刷新和上拉加载更多的功能:

  1. 下拉刷新:使用 Refresh 组件包裹内容,当用户下拉时触发 onRefresh 回调,刷新第一页数据。
  2. 上拉加载:监听 List 的 onReachEnd 事件,当用户滚动到底部时加载下一页数据。
  3. 加载状态指示:显示加载中的进度指示器和文本提示。
  4. 边界处理:当没有更多数据时,显示"没有更多商品了"的提示。

这种交互模式使用户能够方便地刷新和加载更多商品,提升浏览体验。

4. 性能优化策略

4.1 虚拟列表

当商品数量非常多时,一次性渲染所有商品会导致性能问题。虚拟列表技术只渲染可见区域的商品,可以大幅提升性能。

@Component
struct VirtualGridItem {
  product: EcommerceProduct
  @Consume addToCart: (productId: number) => void
  @Consume showProductDetail: (product: EcommerceProduct) => void
  
  build() {
    Column() {
      // 商品卡片内容...
    }
    .width('100%')
    .backgroundColor('#FFFFFF')
    .borderRadius(12)
    .shadow({
      radius: 6,
      color: 'rgba(0, 0, 0, 0.1)',
      offsetX: 0,
      offsetY: 2
    })
    .onClick(() => {
      this.showProductDetail(this.product)
    })
  }
}

@Component
struct VirtualProductGrid {
  @Consume products: EcommerceProduct[]
  @Consume addToCart: (productId: number) => void
  @Consume showProductDetail: (product: EcommerceProduct) => void
  
  build() {
    List() {
      LazyForEach(new VirtualProductDataSource(this.products), (product: EcommerceProduct) => {
        ListItem() {
          Row() {
            VirtualGridItem({ product })
              .layoutWeight(1)
              .margin({ right: 6 })
            
            VirtualGridItem({ product })
              .layoutWeight(1)
              .margin({ left: 6 })
          }
          .width('100%')
          .padding({ left: 16, right: 16 })
          .margin({ bottom: 12 })
        }
      }, (product: EcommerceProduct) => product.id.toString())
    }
    .width('100%')
    .height('100%')
    .divider({ strokeWidth: 0 })
  }
}

class VirtualProductDataSource implements IDataSource {
  private products: EcommerceProduct[]
  private listener: DataChangeListener
  
  constructor(products: EcommerceProduct[]) {
    this.products = products
  }
  
  totalCount(): number {
    return Math.ceil(this.products.length / 2) // 每行两个商品
  }
  
  getData(index: number): Object {
    const productIndex = index * 2
    return this.products[productIndex]
  }
  
  registerDataChangeListener(listener: DataChangeListener): void {
    this.listener = listener
  }
  
  unregisterDataChangeListener(): void {
    this.listener = null
  }
  
  notifyDataReload(): void {
    this.listener.onDataReloaded()
  }
  
  notifyDataAdd(index: number): void {
    this.listener.onDataAdd(index)
  }
  
  notifyDataChange(index: number): void {
    this.listener.onDataChange(index)
  }
  
  notifyDataDelete(index: number): void {
    this.listener.onDataDelete(index)
  }
}

这个虚拟列表实现使用 LazyForEach 和自定义的 IDataSource 接口,只渲染可见区域的商品卡片,大幅减少内存占用和提升渲染性能。

4.2 图片懒加载

在电商应用中,图片通常是最占用资源的部分。实现图片懒加载可以减少初始加载时间和内存占用。

@Component
struct LazyImage {
  src: Resource
  @State isLoaded: boolean = false
  @State isLoading: boolean = false
  @State isError: boolean = false
  
  build() {
    Stack({ alignContent: Alignment.Center }) {
      if (!this.isLoaded) {
        Row()
          .width('100%')
          .height('100%')
          .backgroundColor('#F0F0F0')
        
        if (this.isLoading) {
          LoadingProgress()
            .width(24)
            .height(24)
            .color('#999999')
        } else if (this.isError) {
          Image($r('app.media.image_error'))
            .width(32)
            .height(32)
            .fillColor('#999999')
        }
      }
      
      Image(this.src)
        .width('100%')
        .height('100%')
        .objectFit(ImageFit.Cover)
        .opacity(this.isLoaded ? 1 : 0)
        .onComplete(() => {
          this.isLoaded = true
          this.isLoading = false
        })
        .onError(() => {
          this.isError = true
          this.isLoading = false
        })
    }
    .width('100%')
    .height('100%')
    .onAppear(() => {
      this.isLoading = true
    })
  }
}

然后在商品卡片中使用这个懒加载图片组件:

@Builder
ProductCard(product: EcommerceProduct) {
  Column() {
    // 使用懒加载图片
    LazyImage({ src: product.image })
      .width('100%')
      .height(160)
      .borderRadius({ topLeft: 12, topRight: 12 })
    
    // 其他商品信息...
  }
}

这个懒加载图片组件只有在图片出现在可视区域时才开始加载,并显示加载状态和错误状态,提升用户体验和应用性能。

5. 高级数据处理

5.1 分页加载

在实际应用中,商品数据通常需要从服务器分页加载,以减少网络传输和提升性能。

@State products: EcommerceProduct[] = []
@State isLoading: boolean = false
@State hasError: boolean = false
@State errorMessage: string = ''
@State currentPage: number = 1
@State pageSize: number = 20
@State hasMoreData: boolean = true

// 初始加载数据
aboutToAppear() {
  this.loadProducts()
}

// 加载商品数据
async loadProducts() {
  if (this.isLoading) return
  
  this.isLoading = true
  this.hasError = false
  
  try {
    // 模拟API请求
    const response = await this.fetchProducts({
      page: this.currentPage,
      pageSize: this.pageSize,
      category: this.selectedCategory !== '全部' ? this.selectedCategory : undefined,
      minPrice: this.priceRange.min,
      maxPrice: this.priceRange.max,
      keyword: this.searchKeyword.trim() !== '' ? this.searchKeyword : undefined,
      sortBy: this.getSortParam()
    })
    
    if (this.currentPage === 1) {
      // 第一页,替换现有数据
      this.products = response.data
    } else {
      // 追加数据
      this.products = [...this.products, ...response.data]
    }
    
    this.hasMoreData = response.hasMore
    this.currentPage++
  } catch (error) {
    this.hasError = true
    this.errorMessage = '加载商品失败,请重试'
    console.error('加载商品失败', error)
  } finally {
    this.isLoading = false
  }
}

// 刷新数据
refreshData() {
  this.currentPage = 1
  this.hasMoreData = true
  this.loadProducts()
}

// 获取排序参数
getSortParam(): string {
  switch (this.sortBy) {
    case '价格升序':
      return 'price_asc'
    case '价格降序':
      return 'price_desc'
    case '销量':
      return 'sales_desc'
    case '评分':
      return 'rating_desc'
    default: // 综合
      return 'comprehensive'
  }
}

// 模拟API请求
async fetchProducts(params: {
  page: number,
  pageSize: number,
  category?: string,
  minPrice?: number,
  maxPrice?: number,
  keyword?: string,
  sortBy?: string
}): Promise<{ data: EcommerceProduct[], hasMore: boolean }> {
  return new Promise((resolve) => {
    setTimeout(() => {
      // 模拟服务器返回的数据
      const data = this.generateMockProducts(params.pageSize)
      const hasMore = params.page < 5 // 假设只有5页数据
      
      resolve({ data, hasMore })
    }, 1000) // 模拟网络延迟
  })
}

这个分页加载实现包含以下特点:

  1. 初始加载:在组件初始化时加载第一页数据
  2. 分页参数:支持页码、每页数量、分类、价格范围、关键词和排序等参数
  3. 数据追加:第一页替换现有数据,后续页面追加数据
  4. 加载状态:跟踪加载状态、错误状态和是否有更多数据
  5. 错误处理:捕获并显示加载错误

这种分页加载策略能够有效减少网络传输和内存占用,提升应用性能和用户体验。

6. 总结

在本教程中,我们深入探讨了 HarmonyOS NEXT 电商网格布局的高级应用,包括复杂业务场景实现、高级交互技术、性能优化策略和高级数据处理等方面。

收藏00

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