蛋糕美食元服务_美食实现指南

美食模块实现流程操作指南

美食页面

一、模块概述

美食模块(Food)是整个蛋糕美食元服务中最核心、最复杂的业务模块。它实现了完整的分类菜单浏览 + 购物车管理功能,采用双栏布局设计,左侧为分类导航,右侧为商品列表,底部为悬浮购物车栏。

二、模块职责

职责 说明
分类导航 左侧分类栏,点击切换商品列表
商品列表 右侧展示当前分类下的蛋糕列表
购物车管理 底部购物车栏,实时显示总价和数量
数量控制 每项商品支持加减数量操作
门店展示 顶部显示当前选中门店

三、数据模型

3.1 蛋糕数据模型(CakeModel.ets)

typescript 复制代码
export class CakeDetail {
  id: string = ''           // 唯一标识
  title: string = ''        // 蛋糕名称
  desc: string = ''         // 描述
  price: number = 0         // 单价
  pic: string = ''          // 图片(emoji替代)
  label: string = ''        // 标签(人气TOP/新品/店长推荐)
  category: string = ''     // 所属分类
  buyNum: number = 0        // 购买数量
  isHot: boolean = false    // 是否热销
}

export class CakeCategory {
  category: string = ''     // 分类名称
  list: CakeDetail[] = []   // 该分类下的商品列表
}

3.2 购物车业务模型(CartViewModel.ets)

typescript 复制代码
export class CartViewModel {
  private buyFoods: Map<string, CakeDetail> = new Map()
  private totalPrice: number = 0
  private totalCount: number = 0

  addItem(cake: CakeDetail): void {
    // 已存在则buyNum+1,否则新增
  }

  removeItem(cakeId: string): void {
    // buyNum-1,为0则移除
  }

  private recalculate(): void {
    // 遍历Map重算总价和总数
    // 使用 MathUtil.addition 和 multiply 保证精度
  }
}

四、实现流程

步骤1:创建双栏布局骨架

typescript 复制代码
@Component
export struct Food {
  @State currentCategory: string = ''
  @State categories: string[] = []
  @State currentCakes: CakeDetail[] = []
  @State cartVM: CartViewModel = new CartViewModel()

  build() {
    Column() {
      // 顶部门店信息
      Row() { /* 门店名称 */ }

      // 双栏区域
      Row() {
        // 左侧分类栏 (80px宽)
        Scroll() { /* 分类按钮 */ }.width(80)

        // 右侧商品列表
        Scroll() { /* 商品卡片 */ }.layoutWeight(1)
      }
      .layoutWeight(1)

      // 底部购物车栏
      CartBar({ /* 总价、总数、结算回调 */ })
    }
  }
}

步骤2:实现左侧分类导航

分类栏使用 Scroll 包裹 Column,支持分类过多时滚动:

typescript 复制代码
Scroll() {
  Column() {
    ForEach(this.categories, (category: string) => {
      Column() {
        Text(category)
          .fontSize(13)
          .fontWeight(this.currentCategory === category ? FontWeight.Medium : FontWeight.Normal)
          .fontColor(this.currentCategory === category ? '#FF6B35' : '#666666')
      }
      .width(80).height(60)
      .backgroundColor(this.currentCategory === category ? '#FFF5E6' : '#F8F8F8')
      .onClick(() => {
        this.currentCategory = category
        this.currentCakes = FoodViewModel.getCakeList(category)
      })
    })
  }
}
.scrollBar(BarState.Off)

设计要点

  • 选中分类使用高亮背景色 #FFF5E6 + 橙色文字
  • 点击切换时通过 FoodViewModel.getCakeList() 获取对应分类数据

步骤3:实现右侧商品列表

每个商品项包含图片、名称、描述、价格和数量控制:

typescript 复制代码
ForEach(this.currentCakes, (cake: CakeDetail) => {
  Row() {
    Text(cake.pic)
      .fontSize(36).width(64).height(64)
      .backgroundColor('#FFF5E6').borderRadius(10)

    Column() {
      // 名称 + 标签
      Row() {
        Text(cake.title).layoutWeight(1)
        if (cake.label.length > 0) {
          Text(cake.label) // 人气TOP/新品等标签
        }
      }
      // 描述
      Text(cake.desc)
      // 价格 + 数量控制
      Row() {
        Text(`¥${cake.price.toFixed(1)}`)
        Blank()
        // 减号按钮(仅在buyNum>0时显示)
        if (this.cartVM.getBuyNum(cake.id) > 0) {
          Button('-').onClick(() => {
            this.cartVM.removeItem(cake.id)
            this.saveCart()
          })
          Text(`${this.cartVM.getBuyNum(cake.id)}`)
        }
        // 加号按钮
        Button('+').onClick(() => {
          this.cartVM.addItem(cake)
          this.saveCart()
        })
      }
    }
  }
})

步骤4:实现购物车数据同步

购物车数据需要在Food页面和PageOrderPreview之间共享,采用 @LocalStorageLink 以JSON字符串形式存储:

typescript 复制代码
@LocalStorageLink('buyFoods') buyFoodsJson: string = '[]'
@LocalStorageLink('choosePrice') choosePrice: number = 0

private saveCart(): void {
  this.buyFoodsJson = this.cartVM.toJSON()      // 序列化
  this.choosePrice = this.cartVM.getTotalPrice() // 更新总价
}

数据流

复制代码
用户点击[+] → cartVM.addItem() → saveCart()
  → cartVM.toJSON() → buyFoodsJson (LocalStorage)
  → cartVM.getTotalPrice() → choosePrice (LocalStorage)
  → UI自动刷新(@LocalStorageLink响应式更新)

步骤5:实现购物车弹出面板

点击底部购物车栏可展开/收起购物车详情:

typescript 复制代码
@State showCart: boolean = false

// 点击购物车栏切换显示
CartBar({
  onCartClick: () => { this.showCart = !this.showCart }
})

// 购物车弹出面板
if (this.showCart && this.cartVM.getTotalCount() > 0) {
  Column() {
    // 标题栏 + 清空按钮
    Row() { Text('购物车'); Blank(); Text('清空') }
    // 商品列表
    ForEach(this.cartVM.getCartItems(), (item: CakeDetail) => {
      Row() {
        Text(item.title); Text(`¥${item.price}`); Text(`x${item.buyNum}`)
      }
    })
  }
  .maxHeight('40%')
}

步骤6:结算跳转

购物车总价大于0时,点击"去结算"跳转到订单确认页:

typescript 复制代码
CartBar({
  onCheckout: () => {
    if (!this.cartVM.isEmpty()) {
      RouterModule.push({
        stackName: NavStackMap.MAIN_STACK,
        url: NavRouterMap.PAGE_ORDER_PREVIEW
      } as NavRouterInfo)
    }
  }
})

五、关键代码:CartViewModel 精度计算

typescript 复制代码
private recalculate(): void {
  this.totalPrice = 0
  this.totalCount = 0
  this.buyFoods.forEach((item: CakeDetail) => {
    // multiply保证单价×数量的精度
    this.totalPrice = addition(this.totalPrice, multiply(item.price, item.buyNum))
    this.totalCount += item.buyNum
  })
}

为什么不用普通的 price * num

JavaScript中 168.5 * 3 = 505.50000000000006,而 multiply(168.5, 3) = 505.5,避免了浮点数精度误差导致的价格显示错误。

六、注意事项

  1. 购物车数据序列化存储,切换Tab时状态保持
  2. 分类切换后需要保留购物车中所有分类的商品
  3. 商品列表底部需要 Blank().height(80) 避免被购物车栏遮挡
  4. 门店切换通过 RouterModule.push(PAGE_SHOP_LIST) 实现,返回后自动更新
相关推荐
RReality1 小时前
【Unity UGUI】血条 / 进度条(HP Bar)
ui·unity·游戏引擎·图形渲染
王二蛋与他的张大花3 小时前
高德地图 Flutter 插件:跨 Android / iOS / HarmonyOS 的完整实现
harmonyos
狼哥16863 小时前
蛋糕美食元服务_地图实现指南
ui·harmonyos
JohnnyDeng945 小时前
【鸿蒙】HarmonyOS 数据持久化:Preferences/KV Store/RelationalStore 选型指南
harmonyos·arkts·鸿蒙·数据持久化·arkui
小雨青年5 小时前
【鸿蒙原生开发会议随记 Pro】用 NavPathStack 收拢会议页面跳转和返回刷新
华为·harmonyos
UXbot5 小时前
AI网页开发工具能替代工具吗?5大平台对比
前端·人工智能·低代码·ui·原型模式·web app
轻口味6 小时前
轻规划鸿蒙开发实战3:AR Engine Kit 深度实践,基于面部追踪与骨骼捕捉的体感微笑打
华为·ar·harmonyos·鸿蒙
Swift社区6 小时前
鸿蒙 App 为什么需要统一状态源?
华为·harmonyos
星释6 小时前
HDC 2026 跨平台框架专题:HarmonyOS 生态下的跨端技术全景
华为·harmonyos