案例+1HarmonyOS官方模板优秀案例

(第14期:美食行业 · 餐饮点餐

💡 鸿蒙生态为开发者提供海量的HarmonyOS模板/组件,助力开发效率原地起飞 💡

★ 一键直达生态市场组件&模板市场 , 快速应用DevEco Studio插件市场集成组件&模板 ★

贴心的用餐体验能够为美味加分,本期为大家介绍点餐元服务模板

★ 一键直达 HarmonyOS 行业解决方案 美食行业解决方案 

👉 覆盖20+行业,点击查看往期案例汇总贴,持续更新点击收藏!一键三连!常看常新!

【第14期】美食行业 · 餐饮点餐

一、概述

1.行业洞察

1)行业诉求:

  • 传统餐饮私域流量缺失,依赖第三方平台导流,佣金成本高且较难沉淀用户;
  • 第三方平台都在推各家的点餐码,导致餐饮门店一张桌上多张码,不仅增加了布码和维护成本,还给顾客带来不好的点餐体验。
  • 低频使用的独立App极易被用户遗忘删除;公众号/小程序需主动打开,入口深且触达率低。

2)行业常用三方SDK

分类

三方库名称

功能

支持情况

SDK链接

媒体

阿里云视频播放器SDK

音视频

已支持

支付宝SDK

微信支付SDK

银联SDK

腾讯QQ SDK

新浪微博SDK

极光PUSH SDK

友盟移动统计SDK

腾讯微信SDK

高德地图SDK

个推

Bugly

ShareSDK

听云SDK

登录认证

中国移动一键登录SDK/易盾一键登录SDK/创蓝闪验/极光安全认证/阿里云号码认证SDK/中国电信一键登录SDK

登录

已支持

分享

友盟/ShareSDK/微信分享/QQ分享/新浪微博SDK/MobTech ShareSDK

统计/推送/分享

已支持

支付

支付宝支付/微信支付/银联支付

支付

已支持

数据分析

友盟移动统计SD/神策数据SDK

数据收集、处理、分析、运用

已支持

性能监控

腾讯Bugly SDK/听云SDK/岳鹰全景监控SDK

异常上报和运营统计

已支持

地图

高德地图SDK

地图

已支持

推送

个推/华为推送/极光PUSH/阿里推送SDK

消息推送

已支持

媒体

阿里云视频播放器SDK

音视频

已支持

说明:“以上三方库及链接仅为示例,三方库由三方开发者独立提供,以其官方内容为准”

2.案例概览(下载模板

基于以上行业分析,本期将介绍鸿蒙生态市场生活服务类行业模板——点餐元服务模板,为行业提供常用功能的开发案例,模板主要分点餐、订单和我的三大模块。

  • Stage开发模型 + 声明式UI开发范式。
  • 分层架构设计 + 组件化拆分,支持开发者在开发时既可以选择完整使用模板,也可以根据需求单独选用其中的业务组件。
  • 本模板为餐饮点餐类元服务提供了常用功能的开发样例,已集成预加载、华为账号、地图、华为支付、通话等服务,只需做少量配置和定制即可快速实现页面的快速加载、华为账号的登录、商家位置定位导航、购买餐饮和联系商家等功能。

本模板主要页面及核心功能如下所示

餐饮点餐模板

|-- 点餐

|    |-- 店铺信息

|    |    |-- 店铺选择

|    |    |-- 店铺详情

|    |    |-- 店铺位置和导航

|    |    └-- 店铺电话

|    |-- 优惠券

|    |    |-- 店铺优惠

|    |    └-- 优惠券列表

|    |-- 商品列表

|    |    |-- 搜索商品

|    |    |-- 商品详情

|    |    |-- 商品规格

|    |    └-- 加入购物车

|    |-- 购物车

|    |    |-- 清空购物车

|    |    |-- 修改购物车商品

|    |    └-- 下单

|    └-- 提交订单

|         └-- 超值加购

|         └-- 钱包支付

|         └-- 订单提交

|-- 订单列表

|    └-- 订单详情

|    └-- 订单支付

└-- 我的

     |-- 用户信息

     |    |-- 修改头像

     |    └-- 关联解绑账号

     |-- 我的中心

     |    |-- 我的钱包

     |    |    |-- 钱包充值

     |    |    └-- 充值记录

     |    |-- 我的优惠券

     |    └-- 我的积分

     └-- 帮助中心

          |-- 常见问题

          └-- 客服电话

 

二、应用架构设计

1.分层模块化设计

  • 产品定制层:专注于满足不同设备或使用场景的个性化需求,作为应用的入口,是用户直接互动的界面。
  • 本实践暂时只支持直板机,为单HAP包形式,包含路由根节点、底部导航栏等。

  • 基础特性层:用于存放相对独立的功能UI和业务逻辑实现。
  • 本实践的基础特性层将应用底部导航栏的每个选项拆分成一个独立的业务功能模块。
  • 每个功能模块都具备高内聚、低耦合、可定制的特点,支持产品的灵活部署。

  • 公共能力层:存放公共能力,包括公共UI组件、数据管理、外部交互和工具库等共享功能。
  • 本实践的公共能力层分为公共基础能力和可分可合组件,均打包为HAR包被上层业务组件引用。
  • 公共基础能力包含日志、文件处理等工具类,公共类型定义,网络库,以及弹窗、加载等公共组件。
  • 可分可合组件将包含行业特点、可完全自闭环的能力抽出独立的组件模块,支持开发者在开发中单独集成使用,详见业务组件设计章节。

2.业务组件设计

为支持开发者单独获取特定场景的页面和功能,本模板将功能完全自闭环的部分能力抽离出独立的行业组件模块,不依赖公共基础能力包,开发者可以单独集成,开箱即用,降低使用难度。

三、行业场景技术方案

1.商品列表

1)场景说明

  • 商品列表左侧为分类信息,右侧为商品信息。左侧和右侧列表支持联动滚动,支持搜索和分类商品快速定位。

2)技术方案

  • 使用左右各用一个List实现商品列表的展示。分别设置其onScrollIndex()事件,使左右List实现连续滚动和快速定位
  • 通过过滤列表数据实现搜索结果展示

2.店铺选择

1)场景说明

  • 用户可以通过选择店铺实现店铺切换功能。页面地图通过华为地图可以展示店铺位置和距离;页面列表展示店铺信息,并且支持店铺导航和拨打电话。

2)技术方案

四、模板代码

1.工程结构下载模板

详细代码结构如下所示:

CateringOrders

  ├─commons/common/src/main

  │  ├─ets

  │  │  ├─cardManager

  │  │  │      CardManager.ets                // 卡片管理

  │  │  │      EntryContext.ets               // 应用上下文

  │  │  │      SubscriberClass.ets            // 卡片公共事件

  │  │  ├─components

  │  │  │      NavHeaderBar.ets               // navigation页面抬头

  │  │  │      CommonConfirmDialog.ets        // 确认弹窗

  │  │  │      LoadingDialog.ets              // 加载中弹窗

  │  │  ├─constants

  │  │  │      Common.ets                     // 公共常量

  │  │  ├─mapper

  │  │  │      Index.ets                      // 数据映射

  │  │  ├─models

  │  │  │      RouterModel.ets                // 路由参数对象

  │  │  │      StorageModel.ets               // AppStorage参数对象

  │  │  │      TabBarModel.ets                // 底部导航栏对象

  │  │  └─utils

  │  │         AsWebRichText.ets              // asweb富文本展示

  │  │         Logger.ets                     // 日志方法

  │  │         PermissionUtil.ets             // 权限申请方法

  │  │         RouterModule.ets               // 路由工具方法

  │  │         Utils.ets                      // 公共方法

  │  └─resources

  ├─commons/network/src/main

  │  ├─ets

  │  │  ├─apis

  │  │  │      APIList.ets                    // 网络请求API

  │  │  │      AxiosHttp.ets                  // 网络请求封装

  │  │  │      AxiosModel.ets                 // 网络请求对象

  │  │  │      HttpRequest.ets                // 网络请求

  │  │  ├─constants

  │  │  │      Index.ets                      // 网络请求常量

  │  │  ├─mocks

  │  │  │  └─MockData

  │  │  │         Order.ets                   // 点餐mock数据

  │  │  │         Store.ets                   // 店铺mock数据

  │  │  │         User.ets                    // 用户mock数据

  │  │  │      AxiosMock.ets                  // mock请求

  │  │  │      RequestMock.ets                // mock API

  │  │  └─types

  │  │         Order.ets                      // 点餐抽象类

  │  │         Request.ets                    // 请求参数抽象类

  │  │         Response.ets                   // 响应参数抽象类

  │  │         Store.ets                      // 店铺抽象类

  │  │         User.ets                       // 用户抽象类

  │  └─resources

  │─components/base_ui/src/main  

  │  ├─ets

  │  │  ├─components

  │  │  │      BusinessTimeDialog.ets         // 店铺休息组件

  │  │  │      CallTelSheetBuilder.ets        // 拨号组件

  │  │  │      CouponCardComp.ets             // 优惠券组件

  │  │  │      OrderGoodsCard.ets             // 订单商品组件

  │  │  │      PayTypeDialog.ets              // 支付弹窗组件

  │  │  │      SheetHeaderComp.ets            // 半模态标题组件

  │  │  ├─constants

  │  │  │      Index.ets                      // 常量数据

  │  │  ├─models

  │  │  │      Index.ets                      // 数据类型

  │  │  └─utils

  │  │         Index.ets                      // 工具方法

  │─components/goods_detail/src/main  

  │  ├─ets

  │  │  ├─components

  │  │  │      GoodsDetail                    // 商品详情组件

  │  │  ├─constants

  │  │  │      Index.ets                      // 常量数据

  │  │  └─models

  │  │         Index.ets                     // 数据类型

  │─components/my_wallet/src/main  

  │  ├─ets

  │  │  ├─components

  │  │  │      MyWallet                       // 我的钱包组件

  │  │  │      RechargeRecordComp             // 充值记录组件

  │  │  ├─models

  │  │  │      Index.ets                      // 数据类型

  │  │  └─utils

  │  │         Logger.ets                     // 日志方法

  │─components/select_store/src/main  

  │  ├─ets

  │  │  ├─components

  │  │  │      HwMapComp                      // 华为地图组件

  │  │  │      SelectStore                    // 选择店铺组件

  │  │  │      StoreCard                      // 店铺卡片组件

  │  │  └─models

  │  │         Index.ets                      // 数据类型

  │─components/snack_sized_deal/src/main  

  │  ├─ets

  │  │  ├─components

  │  │  │      SnackSizedDeal                 // 超值加购组件

  │  │  ├─constants

  │  │  │      Index.ets                      // 常量数据

  │  │  └─models

  │  │         Index.ets                      // 数据类型

  │─features/order/src/main  

  │  ├─ets

  │  │  ├─api

  │  │  │      Index.ets                      // 接口请求封装

  │  │  ├─components

  │  │  │      CustomSelectDialog.ets         // 数据选择半模态弹窗

  │  │  │      GoodInfoComp.ets               // 商品信息组件

  │  │  │      MyCarComp.ets                  // 购物车组件

  │  │  │      MyCarListComp.ets              // 购物车列表组件

  │  │  │      OrderListComp.ets              // 订单内商品列表组件

  │  │  │      TitleComp.ets                  // 点餐标题栏组件

  │  │  ├─constants

  │  │  │      OrderConstant.ets              // 常量数据

  │  │  ├─mapper

  │  │  │      Index.ets                      // 数据映射

  │  │  ├─models

  │  │  │      Index.ets                      // 数据类型

  │  │  │      MustGoodsController.ets        // 必选品控制对象

  │  │  └─pages

  │  │         ConfirmOrderPage.ets           // 确认订单页面

  │  │         GoodDetailPage.ets             // 商品详情页面

  │  │         MerchantDetailPage.ets         // 店铺详情页面

  │  │         OrderPage.ets                  // 点餐页面

  │  │         PreviewImagePage.ets           // 图片预览页面

  │  │         RemarksPage.ets                // 添加备注页面

  │  │         SelectCouponPage.ets           // 选择优惠券页面

  │  │         SelectStorePage.ets            // 选择店铺页面

  │  │         SnackSizedDealPage.ets         // 超值加购页面

  │  └─resources

  │─features/order_list/src/main  

  │  ├─ets

  │  │  ├─api

  │  │  │      Index.ets                      // 接口请求封装

  │  │  ├─components

  │  │  │      ButtonListComp.ets             // 卡片按钮组件

  │  │  │      CommonTab.ets                  // 订单列表tab组件

  │  │  │      OrderCard.ets                  // 订单卡片组件

  │  │  │      OrderTypeComp.ets              // 订单详情顶部组件

  │  │  │      PaymentDetailsComp.ets         // 订单支付详情组件

  │  │  │      ReductionCardComp.ets          // 订单优惠详情组件

  │  │  │      StoreInfoCardComp.ets          // 商户卡片组件

  │  │  ├─mapper

  │  │  │      Index.ets                      // 数据映射

  │  │  ├─models

  │  │  │      Index.ets                      // 订单列表里的数据对象

  │  │  └─pages

  │  │         HwMapPage.ets                  // 商户位置页面

  │  │         OrderDetailPage.ets            // 订单详情页面

  │  │         OrderListPage.ets              // 订单列表页面

  │  └─resources

  │─features/personal_center/src/main  

  │  ├─ets

  │  │  ├─api

  │  │  │      Index.ets                      // 接口请求封装

  │  │  └─pages

  │  │         AnswerPage.ets                 // 常见问题页面

  │  │         FrequentQuestionPage.ets       // 问题答复页面

  │  │         MyCouponsPage.ets              // 我的优惠券页面

  │  │         MyWalletPage.ets               // 我的页面

  │  │         PersonalCenterPage.ets         // 我的钱包页面

  │  │         RechargeRecordPage.ets         // 钱包充值记录页面

  │  │         WalletTermsPage.ets            // 会员储值协议页面

  │  └─resources

  │─preload

  │      handler.js                           // 预加载函数

  │      package.json                         // 预加载函数信息

  └─products/phone/src/main  

     ├─ets

  │  │  ├─api

  │  │  │      Index.ets                      // 接口请求封装

     │  ├─components

     │  │      CustomTabBar.ets               // 自定义底部tab栏组件

     │  ├─entryability

     │  │      EntryAbility.ets               // 应用程序入口

     │  ├─entryformability

     │  │      EntryFormAbility.ets           // 卡片程序入口

  │  │  ├─mapper

  │  │  │      Index.ets                      // 数据映射

     │  ├─pages

     │  │      HomePage.ets                   // 主页面

     │  │      Index.ets                      // 入口页面

     │  └─widget/pages

     │         WidgetCard.ets                 // 卡片页面

     └─resources

2.关键代码解读

本篇代码非元服务的全量代码,只包括元服务的部分能力的关键代码。

1)二级联动列表

// 下标索引处理

currentIndexChangeAction(index: number, isClassify: boolean): void {

  if (this.currentIndex !== index) {

    this.currentIndex = index;

// 是否是分类列表

    if (isClassify) {

      this.scroller.scrollToIndex(index);

    } else {

      this.titleItemScroller.scrollToIndex(index);

    }

  }

}



// 列表头部

@Builder

titleHeader(title: string, count: number) {

  Row() {

    Text() {

      Span(`${title}`)

        .fontSize($r('sys.float.Body_M'))

        .fontWeight(FontWeight.Medium)

        .fontColor($r('sys.color.font_primary'))

      Span(`(${count})`)

        .fontSize($r('sys.float.Caption_M'))

        .fontColor($r('sys.color.font_secondary'))

        .padding({ left: 4 })

    }

  }

  .margin({ bottom: 8, top: 8 })

}

build() {

  Column() {

    // 列表页

    Row({ space: 8 }) {

      List({ scroller: this.titleItemScroller }) {

        ForEach(this.dishesList, (item: DishesTypeResp, index: number) => {

          ListItem() {

            TitleItem({

              typeName: item.typeName,

              hasIcon: item.id === Constants.GOOD_TYPE_HOT,

              isSelected: this.currentIndex === index,

              onClickAction: () => {

                if (index !== undefined) {

                  this.currentIndexChangeAction(index + 1, true);

                }

              },

            })

          }

        }, (item: DishesTypeResp) => item.typeName + this.currentIndex)

        ListItem() {

          Column().width(Constants.FULL_SIZE).height(78)

        }

      }

      .width(92)

      .height(Constants.FULL_SIZE)

      .listDirection(Axis.Vertical) // 排列方向

      .backgroundColor(Color.White)

      .scrollBar(BarState.Off)

      .divider({ strokeWidth: 1 })



      List({ scroller: this.scroller }) {

        ListItem() {

          Search({ value: $$this.searchText, placeholder: $r('app.string.search_goods') })

            .textFont({ size: $r('sys.float.Body_L') })

            .width(Constants.FULL_SIZE)

            .placeholderFont({ size: $r('sys.float.Body_L') })

            .maxLength(20)

            .margin(0)

            .onChange((value: string) => {

              if (value) {

                this.dishesList = this.dishesList.map((item) => {

                  item.goods = item.goods.filter(i => i.name?.includes(value))

                  return item

                })

              } else {

                this.dishesList = JSON.parse(JSON.stringify(this.dishesListOri))

              }

            })

        }



        ForEach(this.dishesList, (item: DishesTypeResp) => {

          ListItemGroup({

            header: this.titleHeader(item.typeName, item.goods.length),

            space: 10,

          }) {

            ForEach(item.goods, (listItem: Goods) => {

              ListItem() {

                GoodInfoComp({ item: listItem })

              }

            }, (listItem: Goods) => JSON.stringify(listItem))



          }

        }, (item: DishesTypeResp) => JSON.stringify(item))

        ListItem() {

          Column() {

            Divider().margin({ top: 12 })

            Text($r('app.string.list_bottom'))

              .fontSize(8)

              .fontWeight(300)

              .fontColor($r('sys.color.font_primary'))

              .margin({ top: 8 })

          }.width(Constants.FULL_SIZE).height(106)

        }

      }

      .layoutWeight(1)

      .height(Constants.FULL_SIZE)

      .scrollBar(BarState.Off)

      .sticky(StickyStyle.None)

      .onScrollIndex((start: number) => this.currentIndexChangeAction(start - 1, false))

    }.layoutWeight(1).margin({ right: 16 })

  }.width(Constants.FULL_SIZE).height(Constants.FULL_SIZE).constraintSize({ maxHeight: Constants.FULL_SIZE })

}

// 分类列表元素

@ComponentV2

export struct TitleItem {

  @Param @Require typeName: string;

  @Param hasIcon: boolean = false

  @Param isSelected: boolean = false;

  @Event onClickAction: () => void = () => {

  }



  build() {

    Row() {

      if (this.hasIcon) {

        Image($r('app.media.ic_hot')).width(16)

      }

      Text(this.typeName)

        .fontSize($r('sys.float.Body_S'))

        .fontColor(this.isSelected ? $r('sys.color.multi_color_09') : $r('sys.color.font_secondary'))

        .textAlign(TextAlign.Center)

    }

    .justifyContent(FlexAlign.Center)

    .backgroundColor(this.isSelected ? '#0FED6F21' : '#0D979797')

    .width(Constants.FULL_SIZE)

    .height(48)

    .padding({ left: 10, right: 10 })

    .border({ width: { right: this.isSelected ? 0.5 : 0 }, color: $r('sys.color.multi_color_09') })

    .onClick(this.onClickAction)

  }

}

2)地图封装

// 地图参数初始化

@Local mapController?: map.MapComponentController;

@Local mapEventManager ?: map.MapEventManager;

private marker: Map<string, map.Marker> = new Map<string, map.Marker>()

private mapOption?: mapCommon.MapOptions;

private callback?: AsyncCallback<map.MapComponentController>;

private style: mapCommon.MyLocationStyle = {

  anchorU: 0.5,

  anchorV: 0.5,

  radiusFillColor: 0xff00FFFFFF,

  displayType: mapCommon.MyLocationDisplayType.FOLLOW,

};



aboutToAppear(): void {

  this.mapOption = {

    position: {

      target: {

        latitude: this.selectLocation.latitude,

        longitude: this.selectLocation.longitude,

      },

      zoom: 15,

    },

  };



  this.callback = async (err, mapController) => {

    if (!err) {

      this.mapController = mapController;

      this.mapEventManager = this.mapController.getEventManager();

      this.mapEventManager?.on('markerClick', (marker: map.Marker) => {

        console.info(`on-markerClick marker = ${marker.getTitle()}`);

        this.changeStore(marker.getTitle())

      });



      this.mapController.on('mapLoad', () => {

        console.info('mapLoad success');

      });

      this.abilityEnabled();

      mapController.setMyLocationStyle(this.style);

      this.updateMakers()

    }

  };

}



// 地图选择店铺后移动镜头

@Monitor('selectStore.id')

cameraChange(monitor: IMonitor) {

  if (monitor.value()?.now) {

    this.moveCamera(this.selectLocation)

  }

}



// 监听位置变化,更新标记

@Monitor('locations')

infoChange(monitor: IMonitor) {

  if (monitor.value()?.now) {

    this.updateMakers()

  }

}



// 更新地图标记位

updateMakers() {

  this.mapController?.clear()

  this.marker?.clear()

  this.locations.forEach(location => {

    this.addMarker(location)

  })

  if (this.selectLocation) {

    this.moveCamera(this.selectLocation)

  }

}



// 创建地图标记位

async addMarker(location: MapLocation) {

  // Marker初始化参数

  let markerOptions: mapCommon.MarkerOptions = {

    position: {

      latitude: location.latitude,

      longitude: location.longitude,

    },

    rotation: 0,

    visible: true,

    zIndex: 0,

    alpha: 1,

    anchorU: 0.5,

    anchorV: 1,

    clickable: true,

    draggable: true,

    flat: false,

    icon: location.icon || 'ic_store_location.png',

  };

  // 创建Marker

  let marker = await this.mapController?.addMarker(markerOptions);

  if (marker) {

    // 设置信息窗的标题

    marker.setTitle(location.title);

    this.marker.set(location.id, marker)

  }

}



// 移动镜头

moveCamera(location: MapLocation) {

  this.mapController?.animateCameraStatus(map.newLatLng({

    latitude: location.latitude,

    longitude: location.longitude,

  }, 15), 200).then(() => {

    this.marker?.get(location.id)?.setInfoWindowVisible(true)

  });

}



// 加载地图组件

build() {

  Stack({ alignContent: Alignment.BottomStart }) {

    MapComponent({

      mapOptions: this.mapOption,

      mapCallback: this.callback,

    })

  }.height('100%').margin({ bottom: this.mapMarginBottom / 2 }).constraintSize({ maxHeight: '100%' })

}

3.模板集成

本模板提供了两种代码集成方式,供开发者自由选用。

1)整体集成下载模板

开发者可以选择直接基于模板工程开发自己的应用工程。

  • 打开模板工程,根据README说明中的快速入门章节,将自己的应用信息配置在模板工程内,即可运行并查看模板效果。

  • 对接开发者自己的服务器接口,转换数据结构,展示真实的云侧数据

将commons/lib_common/src/main/ets/httprequest/HttpRequestApi.ets文件中的mock接口替换为真实的服务器接口。

在commons/network/src/main/ets/types目录中将云侧开发者自定义的数据结构转换为端侧数据结构。

根据自己的业务内容修改模板,进行定制化开发。

2)按需集成

若开发者已搭建好自己的应用工程,但暂未实现其中的部分场景能力,可以选择取用其中的业务组件,集成在自己的工程中。

  • 组件代码获取:

   通过IDE插件下载组件源码。开发指导

   通过生态市场下载组件源码。 下载地址

  • 下载组件源码,根据README中的说明,将组件包配置在自己的工程中。
  • 根据API参考和示例代码,将组件集成在自己的对应场景中。

以上是第14期“餐饮点餐”行业优秀案例的内容,更多行业敬请期待~

欢迎下载使用行业模板“点击下载”,若您有体验和开发问题,或者迫不及待想了解XX行业的优秀案例,欢迎在评论区留言,小编会快马加鞭为您解答~

同时诚邀您添加下方二维码加入“组件模板活动社群”,精彩上新&活动不错过!

👉 HarmonyOS官方模板优秀案例系列持续更新, 点击查看往期案例汇总贴, 点击收藏 “

方便查找!

👉【集成有礼】HarmonyOS官方模板集成创新活动,挥洒创意,赢精美大礼!点击参加

👉HarmonyOS行业解决方案】为各行业鸿蒙应用提供全流程技术方案。点击查看

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐