💡 鴻蒙生態為開發者提供海量的HarmonyOS模板/組件,助力開發效率原地起飛 💡
★ 一鍵直達生態市場組件&模板市場 , 快速應用DevEco Studio插件市場集成組件&模板 ★
貼心的用餐體驗能夠為美味加分,本期為大家介紹點餐元服務模板
★ 一鍵直達 HarmonyOS 行業解決方案 ,美食行業解決方案 ★
👉 覆蓋20+行業,點擊查看 往期案例彙總貼,持續更新,點擊收藏!一鍵三連!常看常新!
【第14期】美食行業 · 餐飲點餐**
一、概述
1.行業洞察
1)行業訴求:
- 傳統餐飲私域流量缺失,依賴第三方平台導流,佣金成本高且較難沉澱用户;
- 第三方平台都在推各家的點餐碼,導致餐飲門店一張桌上多張碼,不僅增加了布碼和維護成本,還給顧客帶來不好的點餐體驗。
- 低頻使用的獨立App極易被用户遺忘刪除;公眾號/小程序需主動打開,入口深且觸達率低。
2)行業常用三方SDK
| 分類 | 三方庫名稱 | 功能 | 支持情況 |
|---|---|---|---|
| 媒體 | 阿里雲視頻播放器SDK | 音視頻 | 已支持 |
| 登錄認證 | 中國移動一鍵登錄SDK/易盾一鍵登錄SDK/創藍閃驗/極光安全認證/阿里雲號碼認證SDK/中國電信一鍵登錄SDK | 登錄 | 已支持 |
| 分享 | 友盟/ShareSDK/微信分享/QQ分享/新浪微博SDK/MobTech ShareSDK | 統計/推送/分享 | 已支持 |
| 支付 | 支付寶支付/微信支付/銀聯支付 | 支付 | 已支持 |
| 數據分析 | 友盟移動統計SD/神策數據SDK | 數據收集、處理、分析、運用 | 已支持 |
| 性能監控 | 騰訊Bugly SDK/聽雲SDK/嶽鷹全景監控SDK | 異常上報和運營統計 | 已支持 |
| 地圖 | 高德地圖SDK | 地圖 | 已支持 |
| 推送 | 個推/華為推送/極光PUSH/阿里推送SDK | 消息推送 | 已支持 |
| 媒體 | 阿里雲視頻播放器SDK | 音視頻 | 已支持 |
説明:"以上三方庫及鏈接僅為示例,三方庫由三方開發者獨立提供,以其官方內容為準"
SDK鏈接:
支付寶SDK
微信支付SDK
銀聯SDK
騰訊QQ SDK
新浪微博SDK
極光PUSH SDK
友盟移動統計SDK
騰訊微信SDK
高德地圖SDK
個推
Bugly
ShareSDK
聽雲SDK
2.案例概覽(下載模板)
基於以上行業分析,本期將介紹鴻蒙生態市場生活服務類行業模板--點餐元服務模板,為行業提供常用功能的開發案例,模板主要分點餐、訂單和我的三大模塊。
- Stage開發模型 + 聲明式UI開發範式。
- 分層架構設計 + 組件化拆分,支持開發者在開發時既可以選擇完整使用模板,也可以根據需求單獨選用其中的業務組件。
- 本模板為餐飲點餐類元服務提供了常用功能的開發樣例,已集成預加載、華為賬號、地圖、華為支付、通話等服務,只需做少量配置和定製即可快速實現頁面的快速加載、華為賬號的登錄、商家位置定位導航、購買餐飲和聯繫商家等功能。
本模板主要頁面及核心功能如下所示:
餐飲點餐模板
|-- 點餐
| |-- 店鋪信息
| | |-- 店鋪選擇
| | |-- 店鋪詳情
| | |-- 店鋪位置和導航
| | └-- 店鋪電話
| |-- 優惠券
| | |-- 店鋪優惠
| | └-- 優惠券列表
| |-- 商品列表
| | |-- 搜索商品
| | |-- 商品詳情
| | |-- 商品規格
| | └-- 加入購物車
| |-- 購物車
| | |-- 清空購物車
| | |-- 修改購物車商品
| | └-- 下單
| └-- 提交訂單
| └-- 超值加購
| └-- 錢包支付
| └-- 訂單提交
|-- 訂單列表
| └-- 訂單詳情
| └-- 訂單支付
└-- 我的
|-- 用户信息
| |-- 修改頭像
| └-- 關聯解綁賬號
|-- 我的中心
| |-- 我的錢包
| | |-- 錢包充值
| | └-- 充值記錄
| |-- 我的優惠券
| └-- 我的積分
└-- 幫助中心
|-- 常見問題
└-- 客服電話
二、應用架構設計
1.分層模塊化設計
-
產品定製層:專注於滿足不同設備或使用場景的個性化需求,作為應用的入口,是用户直接互動的界面。
- 本實踐暫時只支持直板機,為單HAP包形式,包含路由根節點、底部導航欄等。
-
基礎特性層:用於存放相對獨立的功能UI和業務邏輯實現。
- 本實踐的基礎特性層將應用底部導航欄的每個選項拆分成一個獨立的業務功能模塊。
- 每個功能模塊都具備高內聚、低耦合、可定製的特點,支持產品的靈活部署。
-
公共能力層:存放公共能力,包括公共UI組件、數據管理、外部交互和工具庫等共享功能。
- 本實踐的公共能力層分為公共基礎能力和可分可合組件,均打包為HAR包被上層業務組件引用。
- 公共基礎能力包含日誌、文件處理等工具類,公共類型定義,網絡庫,以及彈窗、加載等公共組件。
- 可分可合組件將包含行業特點、可完全自閉環的能力抽出獨立的組件模塊,支持開發者在開發中單獨集成使用,詳見業務組件設計章節。
2.業務組件設計
為支持開發者單獨獲取特定場景的頁面和功能,本模板將功能完全自閉環的部分能力抽離出獨立的行業組件模塊,不依賴公共基礎能力包,開發者可以單獨集成,開箱即用,降低使用難度。
三、行業場景技術方案
1.商品列表
1)場景説明
- 商品列表左側為分類信息,右側為商品信息。左側和右側列表支持聯動滾動,支持搜索和分類商品快速定位。
2)技術方案
- 使用左右各用一個List實現商品列表的展示。分別設置其onScrollIndex()事件,使左右List實現連續滾動和快速定位
- 通過過濾列表數據實現搜索結果展示。
2.店鋪選擇
1)場景説明
- 用户可以通過選擇店鋪實現店鋪切換功能。頁面地圖通過華為地圖可以展示店鋪位置和距離;頁面列表展示店鋪信息,並且支持店鋪導航和撥打電話。
2)技術方案
- 使用MapComponent實現地圖繪製能力
- 使用makeCall實現快速撥號能力。
四、模板代碼
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)整體集成(下載模板)
開發者可以選擇直接基於模板工程開發自己的應用工程。
-
模板代碼獲取:
- 通過IDE插件創建模板工程,開發指導。
- 通過生態市場下載源碼, 下載模板。
- 通過開源倉訪問源碼,倉庫地址。
- 打開模板工程,根據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行業解決方案】為各行業鴻蒙應用提供全流程技術方案。點擊查看