64.[HarmonyOS NEXT 实战案例六] 餐饮菜单网格布局(下)

简介: 在上一篇教程中,我们学习了如何使用GridRow和GridCol组件实现基本的餐饮菜单网格布局。本篇教程将在此基础上,深入探讨如何优化布局、添加交互功能,以及实现更多高级特性,打造一个功能完善的餐饮菜单应用。

[HarmonyOS NEXT 实战案例六] 餐饮菜单网格布局(下)

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

效果演示

img_9264bad3.png

1. 概述

在上一篇教程中,我们学习了如何使用GridRow和GridCol组件实现基本的餐饮菜单网格布局。本篇教程将在此基础上,深入探讨如何优化布局、添加交互功能,以及实现更多高级特性,打造一个功能完善的餐饮菜单应用。

本教程将涵盖以下内容:

  • 菜品详情页的实现
  • 购物车功能的完善
  • 菜品筛选和排序功能
  • 菜品推荐和组合套餐
  • 高级动效和交互优化

2. 菜品详情页实现

2.1 详情页布局设计

当用户点击菜品卡片时,我们需要展示菜品的详细信息。下面是菜品详情页的实现:

@State showFoodDetail: boolean = false; // 是否显示菜品详情
@State currentFood: FoodItem | null = null; // 当前查看的菜品

// 在FoodCard方法中添加点击事件
Column() {
   
  // 菜品卡片内容
}
.width('100%')
.backgroundColor(Color.White)
.borderRadius(8)
.shadow({
   
  radius: 6,
  color: '#1A000000',
  offsetX: 0,
  offsetY: 2
})
.onClick(() => {
   
  this.currentFood = item;
  this.showFoodDetail = true;
})

// 在build方法末尾添加菜品详情页
if (this.showFoodDetail && this.currentFood) {
   
  this.FoodDetailPage()
}

2.2 详情页组件实现

@Builder
private FoodDetailPage() {
   
  Stack() {
   
    Column() {
   
      // 顶部图片区域
      Stack() {
   
        Image(this.currentFood.image)
          .width('100%')
          .height(240)
          .objectFit(ImageFit.Cover)

        // 返回按钮
        Button({
    type: ButtonType.Circle }) {
   
          Image($r('app.media.ic_back'))
            .width(20)
            .height(20)
            .fillColor('#333333')
        }
        .width(36)
        .height(36)
        .backgroundColor('#FFFFFF')
        .position({
    x: 16, y: 16 })
        .onClick(() => {
   
          this.showFoodDetail = false;
        })

        // 收藏按钮
        Button({
    type: ButtonType.Circle }) {
   
          Image($r('app.media.ic_favorite'))
            .width(20)
            .height(20)
            .fillColor('#333333')
        }
        .width(36)
        .height(36)
        .backgroundColor('#FFFFFF')
        .position({
    x: '90%', y: 16 })

        // 标签
        if (this.currentFood.tags && this.currentFood.tags.length > 0) {
   
          Row() {
   
            ForEach(this.currentFood.tags, (tag: string) => {
   
              Text(tag)
                .fontSize(12)
                .fontColor(Color.White)
                .backgroundColor(tag === '辣' || tag === '特辣' ? '#FF5722' : '#FF9800')
                .padding({
    left: 8, right: 8, top: 4, bottom: 4 })
                .borderRadius(4)
                .margin({
    right: 8 })
            })
          }
          .position({
    x: 16, y: 200 })
        }
      }
      .width('100%')
      .height(240)

      // 菜品信息区域
      Column() {
   
        // 菜品名称和评分
        Row() {
   
          Text(this.currentFood.name)
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor('#333333')

          Blank()

          Row() {
   
            Image($r('app.media.ic_star'))
              .width(16)
              .height(16)
              .fillColor('#FFB300')

            Text(this.currentFood.rating.toString())
              .fontSize(16)
              .fontColor('#FFB300')
              .margin({
    left: 4 })
          }
        }
        .width('100%')
        .margin({
    top: 16, bottom: 8 })

        // 价格和销量
        Row() {
   
          Row() {
   
            Text(${
     this.currentFood.price}`)
              .fontSize(20)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FF5722')

            if (this.currentFood.originalPrice) {
   
              Text(${
     this.currentFood.originalPrice}`)
                .fontSize(14)
                .fontColor('#999999')
                .decoration({
    type: TextDecorationType.LineThrough })
                .margin({
    left: 8 })
            }
          }

          Blank()

          Text(`月售${
     this.currentFood.sales}`)
            .fontSize(14)
            .fontColor('#999999')
        }
        .width('100%')
        .margin({
    bottom: 16 })

        // 菜品描述
        Text('菜品介绍')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#333333')
          .margin({
    bottom: 8 })

        Text(this.currentFood.description)
          .fontSize(14)
          .fontColor('#666666')
          .margin({
    bottom: 16 })

        // 菜品特点
        Text('菜品特点')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#333333')
          .margin({
    bottom: 8 })

        Row() {
   
          Column() {
   
            Image($r('app.media.ic_taste'))
              .width(32)
              .height(32)

            Text(this.currentFood.isSpicy ? '麻辣' : '清淡')
              .fontSize(12)
              .fontColor('#666666')
              .margin({
    top: 4 })
          }
          .width('25%')
          .alignItems(HorizontalAlign.Center)

          Column() {
   
            Image($r('app.media.ic_time'))
              .width(32)
              .height(32)

            Text('15-20分钟')
              .fontSize(12)
              .fontColor('#666666')
              .margin({
    top: 4 })
          }
          .width('25%')
          .alignItems(HorizontalAlign.Center)

          Column() {
   
            Image($r('app.media.ic_portion'))
              .width(32)
              .height(32)

            Text('1-2人份')
              .fontSize(12)
              .fontColor('#666666')
              .margin({
    top: 4 })
          }
          .width('25%')
          .alignItems(HorizontalAlign.Center)

          Column() {
   
            Image($r('app.media.ic_calorie'))
              .width(32)
              .height(32)

            Text('350大卡')
              .fontSize(12)
              .fontColor('#666666')
              .margin({
    top: 4 })
          }
          .width('25%')
          .alignItems(HorizontalAlign.Center)
        }
        .width('100%')
        .margin({
    bottom: 16 })

        // 推荐搭配
        Text('推荐搭配')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#333333')
          .margin({
    bottom: 8 })

        Scroll() {
   
          Row() {
   
            ForEach(this.getRecommendedFoods(), (item: FoodItem) => {
   
              Column() {
   
                Image(item.image)
                  .width(80)
                  .height(80)
                  .borderRadius(8)
                  .objectFit(ImageFit.Cover)

                Text(item.name)
                  .fontSize(12)
                  .fontColor('#333333')
                  .maxLines(1)
                  .textOverflow({
    overflow: TextOverflow.Ellipsis })
                  .margin({
    top: 4 })

                Text(${
     item.price}`)
                  .fontSize(12)
                  .fontColor('#FF5722')
                  .margin({
    top: 2 })
              }
              .width(80)
              .alignItems(HorizontalAlign.Center)
              .margin({
    right: 12 })
              .onClick(() => {
   
                this.currentFood = item;
              })
            })
          }
        }
        .scrollBar(BarState.Off)
        .scrollable(ScrollDirection.Horizontal)
        .width('100%')
        .height(120)
        .margin({
    bottom: 16 })

        // 用户评价
        Row() {
   
          Text('用户评价')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#333333')

          Blank()

          Text('查看全部')
            .fontSize(14)
            .fontColor('#FF5722')
            .onClick(() => {
   
              // 查看全部评价
            })
        }
        .width('100%')
        .margin({
    bottom: 8 })

        // 评价列表(简单展示)
        Column() {
   
          Row() {
   
            Image($r('app.media.avatar1'))
              .width(36)
              .height(36)
              .borderRadius(18)

            Column() {
   
              Row() {
   
                Text('用户1234')
                  .fontSize(14)
                  .fontColor('#333333')

                Blank()

                Row() {
   
                  ForEach([1, 2, 3, 4, 5], (item: number) => {
   
                    Image($r('app.media.ic_star'))
                      .width(12)
                      .height(12)
                      .fillColor(item <= this.currentFood.rating ? '#FFB300' : '#E0E0E0')
                      .margin({
    right: 2 })
                  })
                }
              }
              .width('100%')

              Text('菜品很美味,分量足,服务也很好,下次还会再来!')
                .fontSize(14)
                .fontColor('#666666')
                .margin({
    top: 4 })

              Text('2023-06-15')
                .fontSize(12)
                .fontColor('#999999')
                .margin({
    top: 4 })
            }
            .alignItems(HorizontalAlign.Start)
            .margin({
    left: 8 })
            .layoutWeight(1)
          }
          .width('100%')
          .padding(12)
          .backgroundColor('#F9F9F9')
          .borderRadius(8)
        }
        .width('100%')
      }
      .width('100%')
      .padding({
    left: 16, right: 16 })
      .backgroundColor(Color.White)
      .borderRadius({
    topLeft: 16, topRight: 16 })
      .margin({
    top: -20 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor(Color.White)

    // 底部操作栏
    Row() {
   
      // 购物车按钮
      Badge({
   
        count: this.getCartItemsCount(),
        position: BadgePosition.RightTop,
        style: {
    color: '#FFFFFF', fontSize: 12, badgeSize: 16, badgeColor: '#FF5722' }
      }) {
   
        Column() {
   
          Image($r('app.media.ic_cart'))
            .width(24)
            .height(24)

          Text('购物车')
            .fontSize(12)
            .fontColor('#666666')
        }
      }
      .width(60)
      .height(56)
      .onClick(() => {
   
        this.showCart = true;
      })

      Blank()

      // 加入购物车按钮
      Button('加入购物车')
        .width(120)
        .height(40)
        .backgroundColor('#FF9800')
        .borderRadius(20)
        .fontColor(Color.White)
        .onClick(() => {
   
          this.addToCart(this.currentFood.id);
          this.showToast('已加入购物车');
        })

      // 立即购买按钮
      Button('立即购买')
        .width(120)
        .height(40)
        .backgroundColor('#FF5722')
        .borderRadius(20)
        .fontColor(Color.White)
        .margin({
    left: 12 })
        .onClick(() => {
   
          this.addToCart(this.currentFood.id);
          this.showCart = true;
        })
    }
    .width('100%')
    .height(64)
    .padding({
    left: 16, right: 16 })
    .backgroundColor(Color.White)
    .borderWidth({
    top: 0.5 })
    .borderColor('#E0E0E0')
    .position({
    x: 0, y: '92%' })
  }
  .width('100%')
  .height('100%')
  .position({
    x: 0, y: 0 })
  .zIndex(100)

  // 获取推荐菜品
  private getRecommendedFoods(): FoodItem[] {
   
    return this.foodItems.filter(item => 
      item.id !== this.currentFood.id && 
      (item.isRecommended || item.categoryId === this.currentFood.categoryId)
    ).slice(0, 5);
  }

  // 显示提示信息
  private showToast(message: string): void {
   
    // 实现提示信息
    AlertDialog.show({
   
      message: message,
      autoCancel: true,
      alignment: DialogAlignment.Bottom,
      offset: {
    dx: 0, dy: -100 },
      gridCount: 3,
      duration: 2000
    });
  }
}

3. 购物车功能完善

3.1 购物车弹窗实现

@State showCart: boolean = false; // 是否显示购物车

// 在build方法末尾添加购物车弹窗
if (this.showCart) {
   
  this.CartPanel()
}

@Builder
private CartPanel() {
   
  Stack() {
   
    // 半透明背景
    Column()
      .width('100%')
      .height('100%')
      .backgroundColor('#80000000')
      .onClick(() => {
   
        this.showCart = false;
      })

    // 购物车面板
    Column() {
   
      // 顶部标题栏
      Row() {
   
        Text('购物车')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#333333')

        Blank()

        Button('清空')
          .backgroundColor('transparent')
          .fontColor('#999999')
          .fontSize(14)
          .onClick(() => {
   
            this.clearCart();
          })
      }
      .width('100%')
      .height(48)
      .padding({
    left: 16, right: 16 })
      .borderWidth({
    bottom: 0.5 })
      .borderColor('#E0E0E0')

      // 购物车列表
      if (this.getCartItemsCount() > 0) {
   
        List() {
   
          ForEach(this.getCartItems(), (item: CartItem) => {
   
            ListItem() {
   
              Row() {
   
                Image(item.food.image)
                  .width(60)
                  .height(60)
                  .borderRadius(4)
                  .objectFit(ImageFit.Cover)

                Column() {
   
                  Text(item.food.name)
                    .fontSize(14)
                    .fontWeight(FontWeight.Bold)
                    .fontColor('#333333')
                    .maxLines(1)
                    .textOverflow({
    overflow: TextOverflow.Ellipsis })

                  Text(item.food.description)
                    .fontSize(12)
                    .fontColor('#999999')
                    .maxLines(1)
                    .textOverflow({
    overflow: TextOverflow.Ellipsis })
                    .margin({
    top: 4 })

                  Row() {
   
                    Text(${
     item.food.price}`)
                      .fontSize(14)
                      .fontWeight(FontWeight.Bold)
                      .fontColor('#FF5722')

                    Blank()

                    // 数量控制
                    Row() {
   
                      Button({
    type: ButtonType.Circle }) {
   
                        Text('-')
                          .fontSize(16)
                          .fontColor('#666666')
                      }
                      .width(24)
                      .height(24)
                      .backgroundColor('#F5F5F5')
                      .onClick(() => {
   
                        this.decreaseCartItem(item.food.id);
                      })

                      Text(item.quantity.toString())
                        .fontSize(14)
                        .fontColor('#333333')
                        .margin({
    left: 12, right: 12 })

                      Button({
    type: ButtonType.Circle }) {
   
                        Text('+')
                          .fontSize(16)
                          .fontColor('#666666')
                      }
                      .width(24)
                      .height(24)
                      .backgroundColor('#F5F5F5')
                      .onClick(() => {
   
                        this.increaseCartItem(item.food.id);
                      })
                    }
                  }
                  .width('100%')
                  .margin({
    top: 4 })
                }
                .layoutWeight(1)
                .alignItems(HorizontalAlign.Start)
                .margin({
    left: 12 })
              }
              .width('100%')
              .padding({
    top: 12, bottom: 12 })
            }
          })
        }
        .width('100%')
        .layoutWeight(1)
        .padding({
    left: 16, right: 16 })
      } else {
   
        // 空购物车提示
        Column() {
   
          Image($r('app.media.ic_empty_cart'))
            .width(120)
            .height(120)
            .margin({
    bottom: 16 })

          Text('购物车空空如也')
            .fontSize(16)
            .fontColor('#999999')

          Text('去挑选喜欢的美食吧')
            .fontSize(14)
            .fontColor('#999999')
            .margin({
    top: 8 })
        }
        .width('100%')
        .layoutWeight(1)
        .justifyContent(FlexAlign.Center)
      }

      // 底部结算栏
      Row() {
   
        Column() {
   
          Text(`合计:¥${
     this.getCartTotalPrice()}`)
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FF5722')

          Text(`共${
     this.getCartItemsCount()}件商品`)
            .fontSize(12)
            .fontColor('#999999')
            .margin({
    top: 2 })
        }
        .alignItems(HorizontalAlign.Start)

        Blank()

        Button('去结算')
          .width(120)
          .height(40)
          .backgroundColor('#FF5722')
          .borderRadius(20)
          .fontColor(Color.White)
          .enabled(this.getCartItemsCount() > 0)
          .opacity(this.getCartItemsCount() > 0 ? 1 : 0.5)
          .onClick(() => {
   
            this.showCheckout();
          })
      }
      .width('100%')
      .height(64)
      .padding({
    left: 16, right: 16 })
      .borderWidth({
    top: 0.5 })
      .borderColor('#E0E0E0')
      .backgroundColor(Color.White)
    }
    .width('100%')
    .height('60%')
    .backgroundColor(Color.White)
    .borderRadius({
    topLeft: 16, topRight: 16 })
    .position({
    x: 0, y: '40%' })
  }
  .width('100%')
  .height('100%')
  .position({
    x: 0, y: 0 })
  .zIndex(200)
}

3.2 购物车数据结构和方法

// 购物车项接口
interface CartItem {
   
  food: FoodItem;
  quantity: number;
}

// 获取购物车商品列表
private getCartItems(): CartItem[] {
   
  const items: CartItem[] = [];
  this.cartItems.forEach((quantity, foodId) => {
   
    const food = this.foodItems.find(item => item.id === foodId);
    if (food && quantity > 0) {
   
      items.push({
    food, quantity });
    }
  });
  return items;
}

// 获取购物车商品总价
private getCartTotalPrice(): number {
   
  let total = 0;
  this.cartItems.forEach((quantity, foodId) => {
   
    const food = this.foodItems.find(item => item.id === foodId);
    if (food) {
   
      total += food.price * quantity;
    }
  });
  return total;
}

// 增加购物车商品数量
private increaseCartItem(foodId: string): void {
   
  const count = this.cartItems.get(foodId) || 0;
  this.cartItems.set(foodId, count + 1);
}

// 减少购物车商品数量
private decreaseCartItem(foodId: string): void {
   
  const count = this.cartItems.get(foodId) || 0;
  if (count > 1) {
   
    this.cartItems.set(foodId, count - 1);
  } else {
   
    this.cartItems.delete(foodId);
  }
}

// 清空购物车
private clearCart(): void {
   
  this.cartItems.clear();
}

// 显示结算页面
private showCheckout(): void {
   
  // 实现结算页面
  this.showCart = false;
  AlertDialog.show({
   
    title: '订单提交',
    message: `总计:¥${
     this.getCartTotalPrice()}\n即将提交订单`,
    autoCancel: true,
    alignment: DialogAlignment.Center,
    primaryButton: {
   
      value: '确认下单',
      action: () => {
   
        this.clearCart();
        this.showToast('订单已提交');
      }
    },
    secondaryButton: {
   
      value: '取消',
      action: () => {
   
        console.info('取消下单');
      }
    }
  });
}

总结

本案例的布局 可以进行更多的扩展哦

相关文章
|
24天前
|
开发者 UED
HarmonyOS Next快速入门:通用属性
本教程以《HarmonyOS Next快速入门》为基础,涵盖应用开发核心技能。通过代码实例讲解尺寸、位置、布局约束、Flex布局、边框、背景及图像效果等属性设置方法。如`.width()`调整宽度,`.align()`设定对齐方式,`.border()`配置边框样式,以及模糊、阴影等视觉效果的实现。结合实际案例,帮助开发者掌握HarmonyOS组件属性的灵活运用,提升开发效率与用户体验。适合初学者及进阶开发者学习。
64 0
|
24天前
|
开发者
HarmonyOS Next快速入门:通用事件
本教程聚焦HarmonyOS应用开发,涵盖事件处理的核心内容。包括事件分发、触屏事件、键鼠事件、焦点事件及拖拽事件等。通过代码实例讲解点击事件、触控事件(Down/Move/Up)、获焦与失焦事件的处理逻辑,以及气泡弹窗的应用。适合开发者快速掌握HarmonyOS Next中通用事件的使用方法,提升应用交互体验。
63 0
|
29天前
|
开发者
鸿蒙开发:资讯项目实战之项目初始化搭建
目前来说,我们的资讯项目只是往前迈了很小的一步,仅仅实现了项目创建,步虽小,但概念性的知识很多,这也是这个项目的初衷,让大家不仅仅可以掌握日常的技术开发,也能让大家理解实际的项目开发知识。
鸿蒙开发:资讯项目实战之项目初始化搭建
|
23天前
|
缓存 JavaScript IDE
鸿蒙开发:基于最新API,如何实现组件化运行
手动只是让大家了解切换的原理,在实际开发中,可不推荐手动,下篇文章,我们将通过脚本或者插件,快速实现组件化模块之间的切换,实现独立运行,敬请期待!
鸿蒙开发:基于最新API,如何实现组件化运行
|
1月前
|
SQL 弹性计算 数据库
鸿蒙5开发宝藏案例分享---优化应用时延问题
鸿蒙性能优化指南来了!从UI渲染到数据库操作,6大实战案例助你提升应用流畅度。布局层级优化、数据加载并发、数据库查询提速、相机资源延迟释放、手势识别灵敏调整及转场动画精调,全面覆盖性能痛点。附赠性能自检清单,帮助开发者高效定位问题,让应用运行如飞!来自华为官方文档的精华内容,建议收藏并反复研读,共同探讨更多优化技巧。
|
1月前
|
缓存
鸿蒙5开发宝藏案例分享---Swiper组件性能优化实战
本文分享了鸿蒙系统中Swiper组件的性能优化技巧,包括:1) 使用`LazyForEach`替代`ForEach`实现懒加载,显著降低内存占用;2) 通过`cachedCount`精准控制缓存数量,平衡流畅度与内存消耗;3) 利用`onAnimationStart`在抛滑时提前加载资源,提升构建效率;4) 添加`@Reusable`装饰器复用组件实例,减少创建开销。实际应用后,图库页帧率从45fps提升至58fps,效果显著。适合处理复杂列表或轮播场景,欢迎交流经验!
|
1月前
|
缓存 JavaScript 前端开发
鸿蒙5开发宝藏案例分享---Web开发优化案例分享
本文深入解读鸿蒙官方文档中的 `ArkWeb` 性能优化技巧,从预启动进程到预渲染,涵盖预下载、预连接、预取POST等八大优化策略。通过代码示例详解如何提升Web页面加载速度,助你打造流畅的HarmonyOS应用体验。内容实用,按需选用,让H5页面快到飞起!
|
1月前
|
数据库
鸿蒙5开发宝藏案例分享---跨线程性能优化指南
本文深入探讨鸿蒙系统跨线程序列化性能优化,借助DevEco Profiler工具定位序列化瓶颈。通过Sendable接口改造、数据瘦身等方法,将5万本书对象的序列化耗时从260ms+降至&lt;8ms,甚至&lt;1ms。总结避坑经验,建议常态化使用Profiler检测,避免传递大对象,提升多线程开发效率。
|
缓存 数据管理 Shell
鸿蒙5开发宝藏案例分享---性能分析简介
鸿蒙开发资源大揭秘!文中整理了HarmonyOS官方提供的100+场景化案例,涵盖性能优化、UI设计、设备适配等全链路内容。重点解析三大神级案例:折叠屏悬停交互、万人列表流畅滚动和服务卡片实时刷新,附带完整代码与避坑指南。通过精准搜索、代码移植和调试技巧,高效利用这些宝藏资源,助你省时省力避开开发陷阱。更有抖音级短视频流畅度优化方案等彩蛋等待探索!
|
1月前
|
Java
鸿蒙5开发宝藏案例分享---性能检测工具揭秘
鸿蒙性能优化工具全揭秘!本文详解官方隐藏的性能调优利器,包括静态检测(Code Linter)与动态检测(AppAnalyzer)。通过实战案例解析稀疏数组陷阱、循环更新状态变量等问题,并提供优化方案。同时附带高频性能规则速查表及黄金法则,助你高效避坑。开发时建议双开工具,实时检测问题,提升应用性能。