【Harmony OS 5】UNIapp在购物类应用中的实践与ArkTS实现

79 阅读6分钟

##UniApp##

UNIapp在购物类应用中的实践与ArkTS实现

引言

随着移动电商的蓬勃发展,购物类应用已成为人们日常生活中不可或缺的一部分。UNIapp作为一款高效的跨平台开发框架,结合ArkTS的强大能力,为电商应用的开发提供了理想的解决方案。本文将探讨UNIapp在购物类应用中的优势,并通过ArkTS代码展示核心功能的实现。

UNIapp在购物应用中的优势

  1. 跨平台一致性:一套代码可同时发布到iOS、Android及各种小程序平台,保持一致的购物体验
  2. 性能与体验:接近原生的性能表现,确保商品列表流畅滚动和交互动画顺滑
  3. 开发效率:基于Vue.js的语法体系,快速实现复杂电商界面和交互逻辑
  4. 生态支持:丰富的插件市场提供支付、分享、统计等电商常用功能

购物应用核心功能实现

1. 商品首页与分类展示

// 电商首页组件
@Component
struct ShopHome {
  @State banners: Banner[] = []          // 轮播广告
  @State categories: Category[] = []    // 商品分类
  @State hotProducts: Product[] = []    // 热销商品
  @State newProducts: Product[] = []    // 新品上市
  @State isLoading: boolean = false     // 加载状态

  aboutToAppear() {
    this.loadHomeData()
  }

  build() {
    Column() {
      // 搜索栏
      Row() {
        TextInput({ placeholder: '搜索商品...' })
          .layoutWeight(1)
          .borderRadius(20)
          .backgroundColor('#F5F5F5')
          .padding(10)
        
        Button({ icon: 'search' })
          .margin({ left: 10 })
      }
      .padding(10)

      Scroll() {
        Column() {
          // 轮播广告
          Swiper() {
            ForEach(this.banners, (banner: Banner) => {
              Image(banner.imageUrl)
                .width('100%')
                .height(150)
                .onClick(() => this.handleBannerClick(banner))
            })
          }
          .indicator(true)
          .autoPlay(true)
          .interval(3000)
          .height(150)
          .margin({ bottom: 10 })

          // 商品分类
          Grid() {
            ForEach(this.categories, (category: Category) => {
              GridItem() {
                Column() {
                  Image(category.icon)
                    .width(40)
                    .height(40)
                  Text(category.name)
                    .fontSize(12)
                    .margin({ top: 5 })
                }
                .onClick(() => this.navigateToCategory(category))
              }
            })
          }
          .columnsTemplate('1fr 1fr 1fr 1fr 1fr')
          .rowsTemplate('1fr')
          .height(80)
          .margin({ bottom: 10 })

          // 热销商品
          SectionTitle('热销商品')
          ProductGrid({ products: this.hotProducts })

          // 新品上市
          SectionTitle('新品上市')
          ProductGrid({ products: this.newProducts })
        }
      }
      .layoutWeight(1)
    }
  }

  private async loadHomeData() {
    this.isLoading = true
    try {
      const [banners, categories, hotProducts, newProducts] = await Promise.all([
        http.get('/api/banners'),
        http.get('/api/categories'),
        http.get('/api/products/hot'),
        http.get('/api/products/new')
      ])
      this.banners = banners
      this.categories = categories
      this.hotProducts = hotProducts
      this.newProducts = newProducts
    } finally {
      this.isLoading = false
    }
  }

  private handleBannerClick(banner: Banner) {
    // 处理广告点击,可能跳转商品详情或活动页面
    router.push({ url: banner.link })
  }

  private navigateToCategory(category: Category) {
    router.push({ 
      url: 'pages/category', 
      params: { categoryId: category.id }
    })
  }
}

// 商品网格组件
@Component
struct ProductGrid {
  @Prop products: Product[]

  build() {
    Grid() {
      ForEach(this.products, (product: Product) => {
        GridItem() {
          ProductCard({ product: product })
        }
      }
    }
    .columnsTemplate('1fr 1fr')
    .rowsTemplate('auto')
    .columnsGap(10)
    .rowsGap(10)
    .margin({ bottom: 20 })
  }
}

// 商品卡片组件
@Component
struct ProductCard {
  @Prop product: Product

  build() {
    Column() {
      Image(this.product.image)
        .width('100%')
        .aspectRatio(1)
        .objectFit(ImageFit.Cover)
        .borderRadius(5)

      Text(this.product.name)
        .fontSize(14)
        .margin({ top: 5 })
        .maxLines(2)
        .textOverflow({ overflow: TextOverflow.Ellipsis })

      Row() {
        Text(`¥${this.product.price.toFixed(2)}`)
          .fontSize(16)
          .fontColor('#FF5000')
          .fontWeight(FontWeight.Bold)

        if (this.product.originalPrice > this.product.price) {
          Text(`¥${this.product.originalPrice.toFixed(2)}`)
            .fontSize(12)
            .fontColor('#999')
            .margin({ left: 5 })
            .decoration({ type: TextDecorationType.LineThrough })
        }
      }
      .margin({ top: 5 })

      Row() {
        Text(`已售${this.product.sales}`)
          .fontSize(12)
          .fontColor('#999')

        if (this.product.stock < 10) {
          Text('仅剩少量')
            .fontSize(12)
            .fontColor('#FF5000')
            .margin({ left: 5 })
        }
      }
      .margin({ top: 5 })
    }
    .padding(5)
    .backgroundColor(Color.White)
    .borderRadius(5)
    .shadow({ radius: 2, color: '#EEE' })
    .onClick(() => {
      router.push({ 
        url: 'pages/product/detail', 
        params: { id: this.product.id }
      })
    })
  }
}

2. 商品详情页

// 商品详情页组件
@Component
struct ProductDetail {
  @State product: ProductDetail | null = null
  @State selectedSku: Sku | null = null
  @State quantity: number = 1
  @State showSkuPicker: boolean = false
  @State isFavorite: boolean = false
  @State relatedProducts: Product[] = []

  aboutToAppear() {
    const params = router.getParams()
    this.loadProductDetail(params.id)
  }

  build() {
    Column() {
      if (this.product) {
        Scroll() {
          Column() {
            // 商品图片轮播
            Swiper() {
              ForEach(this.product.images, (image: string) => {
                Image(image)
                  .width('100%')
                  .height(300)
                  .objectFit(ImageFit.Cover)
              })
            }
            .indicator(true)
            .height(300)

            // 商品基本信息
            Column() {
              Text(this.product.name)
                .fontSize(18)
                .fontWeight(FontWeight.Bold)
                .margin({ bottom: 5 })

              Text(this.product.description)
                .fontSize(14)
                .fontColor('#666')
                .margin({ bottom: 10 })

              Row() {
                Text(`¥${this.product.price.toFixed(2)}`)
                  .fontSize(22)
                  .fontColor('#FF5000')
                  .fontWeight(FontWeight.Bold)

                if (this.product.originalPrice > this.product.price) {
                  Text(`¥${this.product.originalPrice.toFixed(2)}`)
                    .fontSize(14)
                    .fontColor('#999')
                    .margin({ left: 5 })
                    .decoration({ type: TextDecorationType.LineThrough })
                }

                if (this.product.discount) {
                  Text(`${this.product.discount}折`)
                    .fontSize(12)
                    .fontColor(Color.White)
                    .backgroundColor('#FF5000')
                    .borderRadius(3)
                    .padding({ left: 5, right: 5, top: 2, bottom: 2 })
                    .margin({ left: 5 })
                }
              }
              .margin({ bottom: 10 })

              // 商品规格选择
              Row() {
                Text('选择:')
                  .fontSize(14)
                  .fontColor('#666')
                Text(this.selectedSku ? this.selectedSku.name : '请选择规格')
                  .fontSize(14)
                  .layoutWeight(1)
                Image('arrow_right')
                  .width(12)
                  .height(12)
              }
              .padding(10)
              .backgroundColor('#FAFAFA')
              .borderRadius(5)
              .onClick(() => {
                this.showSkuPicker = true
              })
            }
            .padding(15)

            // 商品详情
            Web({ src: this.product.detailHtml })
              .height(800)

            // 相关推荐
            SectionTitle('猜你喜欢')
            ProductGrid({ products: this.relatedProducts })
              .margin({ bottom: 80 })
          }
        }
        .layoutWeight(1)

        // 底部操作栏
        Row() {
          Column() {
            Button({ icon: this.isFavorite ? 'heart' : 'heart-outline' })
              .onClick(() => this.toggleFavorite())
            Text('收藏')
              .fontSize(10)
          }
          .margin({ right: 15 })

          Column() {
            Button({ icon: 'cart' })
              .onClick(() => this.navigateToCart())
            Text('购物车')
              .fontSize(10)
          }
          .margin({ right: 15 })

          Button('加入购物车', { type: ButtonType.Normal })
            .layoutWeight(1)
            .backgroundColor('#FF9500')
            .fontColor(Color.White)
            .onClick(() => this.addToCart())

          Button('立即购买', { type: ButtonType.Normal })
            .layoutWeight(1)
            .backgroundColor('#FF5000')
            .fontColor(Color.White)
            .margin({ left: 10 })
            .onClick(() => this.buyNow())
        }
        .padding(10)
        .backgroundColor(Color.White)
        .border({ width: 1, color: '#EEE' })
      } else {
        LoadingProgress()
      }
    }

    // SKU选择弹窗
    if (this.showSkuPicker && this.product) {
      SkuPicker({
        skus: this.product.skus,
        selectedSku: this.selectedSku,
        quantity: this.quantity,
        onConfirm: (sku: Sku, qty: number) => {
          this.selectedSku = sku
          this.quantity = qty
          this.showSkuPicker = false
        },
        onClose: () => {
          this.showSkuPicker = false
        }
      })
    }
  }

  private async loadProductDetail(id: string) {
    this.product = await http.get(`/api/products/${id}`)
    this.selectedSku = this.product.skus[0]
    this.loadRelatedProducts(id)
    this.checkFavoriteStatus(id)
  }

  private async loadRelatedProducts(productId: string) {
    this.relatedProducts = await http.get(`/api/products/related/${productId}`)
  }

  private async checkFavoriteStatus(productId: string) {
    this.isFavorite = await http.get(`/api/favorite/status/${productId}`)
  }

  private async toggleFavorite() {
    if (this.isFavorite) {
      await http.delete(`/api/favorite/${this.product?.id}`)
    } else {
      await http.post('/api/favorite', { productId: this.product?.id })
    }
    this.isFavorite = !this.isFavorite
  }

  private addToCart() {
    if (!this.selectedSku) {
      prompt.showToast({ message: '请选择商品规格' })
      return
    }
    http.post('/api/cart', {
      productId: this.product?.id,
      skuId: this.selectedSku.id,
      quantity: this.quantity
    })
    prompt.showToast({ message: '已加入购物车' })
  }

  private buyNow() {
    if (!this.selectedSku) {
      prompt.showToast({ message: '请选择商品规格' })
      return
    }
    router.push({
      url: 'pages/order/confirm',
      params: {
        items: JSON.stringify([{
          productId: this.product?.id,
          skuId: this.selectedSku.id,
          quantity: this.quantity
        }])
      }
    })
  }

  private navigateToCart() {
    router.push({ url: 'pages/cart' })
  }
}

// SKU选择器组件
@Component
struct SkuPicker {
  @Prop skus: Sku[]
  @Prop selectedSku: Sku | null
  @Prop quantity: number
  private onConfirm: (sku: Sku, quantity: number) => void
  private onClose: () => void

  @State tempSelectedSku: Sku | null = this.selectedSku
  @State tempQuantity: number = this.quantity

  build() {
    Column() {
      Column() {
        // 标题和关闭按钮
        Row() {
          Text('选择规格')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .layoutWeight(1)
          Button({ icon: 'close' })
            .onClick(() => this.onClose())
        }
        .margin({ bottom: 15 })

        // SKU选择
        Text('规格')
          .fontSize(14)
          .fontColor('#666')
          .margin({ bottom: 10 })
        
        Wrap() {
          ForEach(this.skus, (sku: Sku) => {
            Button(sku.name, { type: ButtonType.Capsule })
              .stateEffect(true)
              .backgroundColor(this.tempSelectedSku?.id === sku.id ? '#FFF0E6' : '#F5F5F5')
              .fontColor(this.tempSelectedSku?.id === sku.id ? '#FF5000' : '#333')
              .margin({ right: 10, bottom: 10 })
              .onClick(() => {
                this.tempSelectedSku = sku
              })
          })
        }
        .margin({ bottom: 20 })

        // 数量选择
        Row() {
          Text('数量')
            .fontSize(14)
            .fontColor('#666')
            .layoutWeight(1)
          
          Stepper({
            value: this.tempQuantity,
            min: 1,
            max: this.tempSelectedSku?.stock || 10,
            step: 1
          })
          .onChange((value: number) => {
            this.tempQuantity = value
          })
        }
        .margin({ bottom: 20 })

        // 确认按钮
        Button('确定')
          .width('100%')
          .height(40)
          .backgroundColor('#FF5000')
          .fontColor(Color.White)
          .onClick(() => {
            if (this.tempSelectedSku) {
              this.onConfirm(this.tempSelectedSku, this.tempQuantity)
            }
          })
      }
      .padding(20)
      .backgroundColor(Color.White)
      .borderRadius(10)
      .width('100%')
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.End)
    .backgroundColor('#80000000')
  }
}

3. 购物车功能实现

// 购物车页面组件
@Component
struct CartPage {
  @State cartItems: CartItem[] = []
  @State selectedItems: string[] = [] // 选中的商品ID数组
  @State isEditing: boolean = false   // 是否编辑状态
  @State isLoading: boolean = false

  aboutToAppear() {
    this.loadCartItems()
  }

  build() {
    Column() {
      // 购物车列表
      if (this.cartItems.length > 0) {
        List() {
          ForEach(this.cartItems, (item: CartItem) => {
            ListItem() {
              CartItemCard({
                item: item,
                isSelected: this.selectedItems.includes(item.id),
                isEditing: this.isEditing,
                onSelect: (selected: boolean) => this.toggleItemSelect(item.id, selected),
                onQuantityChange: (quantity: number) => this.updateItemQuantity(item.id, quantity),
                onRemove: () => this.removeItem(item.id)
              })
            }
          })
        }
        .layoutWeight(1)
      } else {
        Column() {
          Image('empty_cart')
            .width(150)
            .height(150)
          Text('购物车是空的')
            .fontSize(16)
            .margin({ top: 20 })
          Button('去逛逛')
            .margin({ top: 20 })
            .onClick(() => {
              router.replace({ url: 'pages/home' })
            })
        }
        .width('100%')
        .height('100%')
        .justifyContent(FlexAlign.Center)
        .alignItems(HorizontalAlign.Center)
        .layoutWeight(1)
      }

      // 底部结算栏
      if (this.cartItems.length > 0) {
        Row() {
          // 全选
          Row() {
            Checkbox(this.isAllSelected())
              .onChange((checked: boolean) => {
                this.toggleAllSelect(checked)
              })
            Text('全选')
              .fontSize(14)
              .margin({ left: 5 })
          }
          .onClick(() => {
            this.toggleAllSelect(!this.isAllSelected())
          })

          // 编辑/完成按钮
          Button(this.isEditing ? '完成' : '编辑')
            .margin({ left: 15 })
            .onClick(() => {
              this.isEditing = !this.isEditing
            })

          // 合计区域
          Column() {
            Row() {
              Text('合计:')
                .fontSize(14)
              Text(`¥${this.getSelectedAmount().toFixed(2)}`)
                .fontSize(16)
                .fontColor('#FF5000')
                .fontWeight(FontWeight.Bold)
            }
            Text(`已选${this.selectedItems.length}件`)
              .fontSize(12)
              .fontColor('#999')
          }
          .margin({ left: 15 })
          .layoutWeight(1)

          // 结算按钮
          Button('结算')
            .width(100)
            .backgroundColor('#FF5000')
            .fontColor(Color.White)
            .enabled(this.selectedItems.length > 0)
            .onClick(() => {
              this.checkout()
            })
        }
        .padding(10)
        .backgroundColor(Color.White)
        .border({ width: 1, color: '#EEE' })
      }
    }
  }

  private async loadCartItems() {
    this.isLoading = true
    try {
      this.cartItems = await http.get('/api/cart')
    } finally {
      this.isLoading = false
    }
  }

  private toggleItemSelect(itemId: string, selected: boolean) {
    if (selected) {
      this.selectedItems = [...this.selectedItems, itemId]
    } else {
      this.selectedItems = this.selectedItems.filter(id => id !== itemId)
    }
  }

  private toggleAllSelect(selectAll: boolean) {
    if (selectAll) {
      this.selectedItems = this.cartItems.map(item => item.id)
    } else {
      this.selectedItems = []
    }
  }

  private isAllSelected(): boolean {
    return this.selectedItems.length === this.cartItems.length && this.cartItems.length > 0
  }

  private getSelectedAmount(): number {
    return this.cartItems
      .filter(item => this.selectedItems.includes(item.id))
      .reduce((sum, item) => sum + (item.price * item.quantity), 0)
  }

  private async updateItemQuantity(itemId: string, quantity: number) {
    await http.put(`/api/cart/${itemId}`, { quantity })
    this.loadCartItems()
  }

  private async removeItem(itemId: string) {
    await http.delete(`/api/cart/${itemId}`)
    this.loadCartItems()
    this.selectedItems = this.selectedItems.filter(id => id !== itemId)
  }

  private checkout() {
    router.push({
      url: 'pages/order/confirm',
      params: {
        items: JSON.stringify(
          this.cartItems
            .filter(item => this.selectedItems.includes(item.id))
            .map(item => ({
              productId: item.productId,
              skuId: item.skuId,
              quantity: item.quantity
            }))
        )
      }
    })
  }
}

// 购物车商品项组件
@Component
struct CartItemCard {
  @Prop item: CartItem
  @Prop isSelected: boolean
  @Prop isEditing: boolean
  private onSelect: (selected: boolean) => void
  private onQuantityChange: (quantity: number) => void
  private onRemove: () => void

  build() {
    Row() {
      // 选择框
      Checkbox(this.isSelected)
        .onChange((checked: boolean) => {
          this.onSelect(checked)
        })

      // 商品图片
      Image(this.item.image)
        .width(80)
        .height(80)
        .margin({ left: 10 })

      // 商品信息
      Column() {
        Text(this.item.name)
          .fontSize(14)
          .maxLines(2)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
          .margin({ bottom: 5 })

        Text(this.item.skuName)
          .fontSize(12)
          .fontColor('#999')
          .margin({ bottom: 5 })

        Row() {
          Text(`¥${this.item.price.toFixed(2)}`)
            .fontSize(16)
            .fontColor('#FF5000')
            .fontWeight(FontWeight.Bold)
            .layoutWeight(1)

          if (this.isEditing) {
            Button({ icon: 'trash' })
              .onClick(() => {
                this.onRemove()
              })
          } else {
            Stepper({
              value: this.item.quantity,
              min: 1,
              max: 99,
              step: 1
            })
            .onChange((value: number) => {
              this.onQuantityChange(value)
            })
          }
        }
      }
      .margin({ left: 10 })
      .layoutWeight(1)
    }
    .padding(10)
    .backgroundColor(Color.White)
    .margin({ bottom: 5 })
  }
}

购物类应用优化建议

  1. 性能优化

    • 实现商品列表的虚拟滚动和图片懒加载
    • 使用缓存策略减少重复网络请求
    • 优化SKU选择器的渲染性能
  2. 用户体验

    • 添加商品加入购物车的动画效果
    • 实现搜索历史记录和智能提示
    • 优化商品详情页的加载过渡效果
  3. 电商功能增强

    • 集成多种支付方式(微信、支付宝等)
    • 实现优惠券和促销活动系统
    • 添加商品评价和晒单功能
  4. 数据安全

    • 敏感信息加密传输
    • 实现安全的支付流程
    • 用户隐私数据保护

结语

UNIapp结合ArkTS为购物类应用开发提供了强大的技术支撑,通过上述代码示例,我们展示了电商应用的核心功能实现。开发者可以在这些基础功能上进一步扩展和优化,打造出体验优秀、功能丰富的购物应用。

随着移动电商的持续发展和用户需求的不断变化,购物类应用将面临更多创新机会。UNIapp的跨平台特性和ArkTS的高效开发模式,将帮助开发者快速响应市场变化,为用户提供更优质的购物体验。