HarmonyOS鸿蒙Next《仿盒马》app开发技术分享09--商品规格弹窗(端云一体)

HarmonyOS鸿蒙Next《仿盒马》app开发技术分享09–商品规格弹窗(端云一体)

开发准备

上一节我们实现了商品详情页面,并且成功在页面上展示了商品的图片、商品规格、活动详情等信息,要知道同一种商品大多数都是有多种型号跟规格的,所以这一节我们来实现商品的规格弹窗。这节的要点是自定义弹窗的运用。

功能分析

规格弹窗,我们的数据源需要根据当前商品的specid当条件去规格表里查询对应的数据,需要我们针对id做一个查询。

弹窗的唤起逻辑是我们点击规格列表时,以及点击加入购物车时,这时候我们再去选择对应的规格

代码实现

先创建对应的表结构

{
  "objectTypeName": "product_details_spec",
  "fields": [
    {"fieldName": "id", "fieldType": "Integer", "notNull": true, "belongPrimaryKey": true},
    {"fieldName": "spec_id", "fieldType": "Integer", "notNull": true, "defaultValue": 0},
    {"fieldName": "name", "fieldType": "String"},
    {"fieldName": "url", "fieldType": "String"},
    {"fieldName": "price", "fieldType": "Double"},
    {"fieldName": "original_price", "fieldType": "Double"},
    {"fieldName": "maxLoopAmount", "fieldType": "Integer"},
    {"fieldName": "loopAmount", "fieldType": "Integer"},
    {"fieldName": "coupon", "fieldType": "Double"}
  ],
  "indexes": [
    {"indexName": "field1IndexId", "indexList": [{"fieldName":"id","sortType":"ASC"}]}
  ],
  "permissions": [
    {"role": "World", "rights": ["Read"]},
    {"role": "Authenticated", "rights": ["Read", "Upsert"]},
    {"role": "Creator", "rights": ["Read", "Upsert", "Delete"]},
    {"role": "Administrator", "rights": ["Read", "Upsert", "Delete"]}
  ]
}

然后我们填充几条数据进去,可以暂时不用管一致性

{
  "cloudDBZoneName": "default",
  "objectTypeName": "product_details_spec",
  "objects": [
    {
      "id": 10,
      "spec_id": 10,
      "name": "红颜草莓",
      "url": "https://img2.baidu.com/it/u=839516299,2795069982&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=667",
      "price": 23,
      "original_price": 27,
      "maxLoopAmount": 8,
      "loopAmount": 10,
      "coupon": 10
    },
    {
      "id": 20,
      "spec_id": 10,
      "name": "蓝颜草莓",
      "url": "https://img1.baidu.com/it/u=1747689961,4259776077&fm=253&fmt=auto&app=120&f=JPEG?w=800&h=1067",
      "price": 70,
      "original_price": 99,
      "maxLoopAmount": 20,
      "loopAmount": 20,
      "coupon": 20
    },
    {
      "id": 30,
      "spec_id": 10,
      "name": "紫颜草莓",
      "url": "https://img0.baidu.com/it/u=3689406962,149894862&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",
      "price": 19,
      "original_price": 33,
      "maxLoopAmount": 10,
      "loopAmount": 10,
      "coupon": 10.5
    },
    {
      "id": 60,
      "spec_id": 11,
      "name": "麒麟",
      "url": "https://img0.baidu.com/it/u=1577385102,2202152889&fm=253&fmt=auto&app=120&f=JPEG?w=500&h=666",
      "price": 20.5,
      "original_price": 20.5,
      "maxLoopAmount": 20,
      "loopAmount": 20,
      "coupon": 20.5
    },
    {
      "id": 70,
      "spec_id": 11,
      "name": "甜王",
      "url": "https://img0.baidu.com/it/u=2504604673,2021231300&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=653",
      "price": 10.5,
      "original_price": 10.5,
      "maxLoopAmount": 10,
      "loopAmount": 10,
      "coupon": 10.5
    },
    {
      "id": 80,
      "spec_id": 11,
      "name": "早春红玉",
      "url": "https://t15.baidu.com/it/u=253523176,3189044305&fm=224&app=112&f=JPEG?w=500&h=500",
      "price": 20.5,
      "original_price": 20.5,
      "maxLoopAmount": 20,
      "loopAmount": 20,
      "coupon": 20.5
    },
    {
      "id": 90,
      "spec_id": 11,
      "name": "黑美人",
      "url": "https://img0.baidu.com/it/u=2760413692,4211366512&fm=253&fmt=auto&app=120&f=JPEG?w=500&h=500",
      "price": 10.5,
      "original_price": 10.5,
      "maxLoopAmount": 10,
      "loopAmount": 10,
      "coupon": 10.5
    }
  ]
}

紧接着我们根据详情页的id 查询出对应的规格集合

let databaseZone = cloudDatabase.zone('default');
let condition = new cloudDatabase.DatabaseQuery(product_details_spec);
condition.equalTo("spec_id",this.productParams.spec_id)
let listData = await databaseZone.query(condition);
let json = JSON.stringify(listData)
let data:ProductDetailsSpec[]= JSON.parse(json)
this.specList=data
hilog.error(0x0000, 'testTag', `Failed to query data, code: ${this.specList}`);

然后创建自定义弹窗,把查询的数据传入进去进行页面的绘制

@CustomDialog
export default struct SpecDialog{
  @State specList:ProductDetailsSpec[]=[];
  @State product:HomeProductList|null=null;
  controller: CustomDialogController
  @State productSpec?:ProductDetailsSpec|null=null;
  @State pushCart:CartProductList|null=null;
  @State @Watch("onChange") checkIndex:number=0
  @State selectedItem:number = -1;
  @State addNumber:number=1;

  async aboutToAppear(): Promise<void> {
    const value = await StorageUtils.getAll('user');
    if (value!='') {
      this.user=JSON.parse(value)
    }
    this.productSpec=this.specList[this.checkIndex]
  }

  onChange(){
    this.productSpec=this.specList[this.checkIndex]
  }

  build(){
    Column({space:10}){
      Row(){
        Image(this.productSpec?.url)
          .height(100)
          .width(100)

        Column(){
          Row(){
            Text(){
              Span("¥")
                .fontSize(16)
                .fontColor(Color.Red)
              Span(this.productSpec?.price+"")
                .fontSize(22)
                .fontWeight(FontWeight.Bold)
                .padding({top:10})
                .fontColor(Color.Red)
            }
            .margin({left:15})

            Text("¥"+String(this.productSpec?.original_price))
              .fontSize(16)
              .fontColor('#999')
              .decoration({
                type: TextDecorationType.LineThrough,
                color: Color.Gray
              })
              .margin({left:10})
          }
        }
        Blank()
        Image($r('app.media.spec_dialog_close'))
          .height(20)
          .width(20)
          .onClick(()=>{
            this.controller.close()
          })
      }
      .alignItems(VerticalAlign.Top)
      .width('100%')
      .justifyContent(FlexAlign.SpaceBetween)

      Divider().width('90%').height(0.5)

      List({space:10}){
        ForEach(this.specList,(item:ProductDetailsSpec,index:number)=>{
          ListItem(){
            Text(item.name)
              .backgroundColor(Color.Green)
              .padding(3)
              .borderRadius(5)
              .backgroundColor(this.checkIndex==index?"#FCDB29":Color.Grey)
              .fontColor(this.checkIndex==index?"#000000":Color.White)
              .onClick(()=>{
                this.checkIndex=index
              })
          }
        })
      }
      .height(50)
      .listDirection(Axis.Horizontal)

      Row(){
        Text("购买数量")
          .fontSize(16)
          .fontColor(Color.Black)

        Blank()

        Text(" - ")
          .textAlign(TextAlign.Center)
          .border({width:0.5,color:Color.Gray})
          .fontSize(14)
          .height(20)
          .padding({left:7,right:7})
          .fontColor(Color.Black)
          .onClick(()=>{
            if (this.addNumber==1) {
              showToast("已经是最小数量了~")
            } else {
              this.addNumber--
            }
          })
          .borderRadius({topLeft:5,bottomLeft:5})

        Text(this.addNumber+"")
          .textAlign(TextAlign.Center)
          .fontColor(Color.Black)
          .fontSize(14)
          .height(20)
          .padding({left:20,right:20})
          .border({width:0.5,color:Color.Gray})

        Text(" + ")
          .textAlign(TextAlign.Center)
          .fontColor(Color.Black)
          .fontSize(14)
          .height(20)
          .padding({left:7,right:7})
          .onClick(()=>{
            this.addNumber++
          })
          .border({width:0.5,color:Color.Gray})
          .borderRadius({topRight:5,bottomRight:5})
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceBetween)

      Row(){
        Text("加入购物车")
          .width('70%')
          .borderRadius(30)
          .textAlign(TextAlign.Center)
          .fontColor(Color.Black)
          .margin({top:70})
          .height(40)
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .backgroundColor("#FCDB29")
          .onClick(async ()=>{
          })
      }
      .width('100%')
      .justifyContent(FlexAlign.Center)
    }
    .alignItems(HorizontalAlign.Start)
    .backgroundColor(Color.White)
    .justifyContent(FlexAlign.Start)
    .padding(15)
    .height(400)
    .width('100%')
  }
}

创建完成之后我们在详情页面初始化弹窗,把查询的数据传进去

specDialogController:CustomDialogController=new CustomDialogController({
  builder:SpecDialog({
    specList:this.specList
  }),
  alignment: DialogAlignment.Bottom,
  customStyle:true
})

调用弹窗

this.specDialogController.open()

执行代码看看效果


更多关于HarmonyOS鸿蒙Next《仿盒马》app开发技术分享09--商品规格弹窗(端云一体)的实战教程也可以访问 https://www.itying.com/category-93-b0.html

2 回复

HarmonyOS Next实现商品规格弹窗(端云一体)的核心技术:

  1. 使用ArkUI的CustomDialogController控制弹窗显示/隐藏
  2. 弹窗布局采用Column+Flex+ForEach组合实现规格选项
  3. 云端数据通过@ohos.net.http模块获取,使用Promise异步处理
  4. 本地与云端数据同步采用ohos.data.preferences持久化存储
  5. 规格选择状态管理使用@Observed@ObjectLink装饰器
  6. 交互动画使用animateTo实现平滑过渡效果

关键代码结构:

  • 规格数据模型类(SpecModel)
  • 云端API请求方法(getSpecsFromCloud)
  • 弹窗自定义组件(SpecDialog)

更多关于HarmonyOS鸿蒙Next《仿盒马》app开发技术分享09--商品规格弹窗(端云一体)的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


在HarmonyOS Next中实现商品规格弹窗的开发思路很清晰,主要涉及以下几个关键技术点:

  1. 云数据库查询:
  • 使用CloudDB的equalTo方法根据spec_id查询对应规格数据
  • 查询结果通过JSON转换后绑定到specList状态变量
  1. 自定义弹窗实现:
  • 通过@CustomDialog装饰器创建规格选择弹窗组件
  • 使用@State管理选中规格、数量等状态
  • 采用@Watch监听规格选择变化
  1. 核心交互逻辑:
  • 水平列表展示所有规格选项
  • 数量增减控件实现购买数量调整
  • 加入购物车按钮预留事件处理
  1. 数据传递:
  • 通过CustomDialogController将规格数据传入弹窗
  • 使用alignment: DialogAlignment.Bottom实现底部弹出效果

建议可以进一步优化:

  1. 加入规格库存校验逻辑
  2. 实现规格选择与主页面数据联动
  3. 添加动画效果提升用户体验

整体实现符合HarmonyOS的声明式开发范式,代码结构清晰,是典型的电商类应用规格选择实现方案。

回到顶部