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

一、模块概述
美食模块(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,避免了浮点数精度误差导致的价格显示错误。
六、注意事项
- 购物车数据序列化存储,切换Tab时状态保持
- 分类切换后需要保留购物车中所有分类的商品
- 商品列表底部需要
Blank().height(80)避免被购物车栏遮挡 - 门店切换通过
RouterModule.push(PAGE_SHOP_LIST)实现,返回后自动更新