本文地址: github
https://github.com/apicloudcom/ordering-food/tree/main/docs
《點餐》項目是一個餐飲商戶單商家堂食下單應(yīng)用。 主要功能包括瀏覽商家主頁信息、查看推薦菜品、下單商品、取餐等號等功能。 可以適用于小吃快餐餐飲商戶的堂食點單管理,也可以進行稍微二開成為外賣、店鋪或者是虛擬服務(wù)等電商小應(yīng)用。
項目中前端技術(shù)要點包括跨頁面通信、全局購物車數(shù)據(jù)管理、自定義復(fù)用組件編寫和輔助助手函數(shù)等等。 使用 APICloud
多端技術(shù)實現(xiàn)了一套代碼,多端運行。 支持編譯成 Android
& iOS
App
以及微信小程序。
項目后端使用的是 APICloud
數(shù)據(jù)云3.0(未處理) 來構(gòu)建的: 通過編寫云函數(shù)自動管理維護接口和數(shù)據(jù),詳細可以參考數(shù)據(jù)云的文檔。也可以自定義后端接口,通過自寫服務(wù)器完成開發(fā)。
項目的初始化、預(yù)覽、調(diào)試和打包等操作請參考 APICloud多端開發(fā)快速上手教程 。 本文側(cè)重于《點餐》項目的技術(shù)要點分析和說明。
項目源碼位于 https://github.com/apicloudcom/ordering-food 。具體的項目源碼是在倉庫代碼的 widget
目錄下面,此目錄也就是應(yīng)用的根目錄。
[2021-5-22更新] 本項目數(shù)據(jù)云模型代碼已經(jīng)更新到線上的預(yù)置模型中,如有需要請參考本文檔第三大節(jié):《數(shù)據(jù)云模型導(dǎo)入和快速上手》
項目源碼在本倉庫的 widget
目錄下。其中該目錄下的文件結(jié)構(gòu)如下:
┌─component/ 項目公共組件目錄
│ ├─empty-block.shtml 空數(shù)據(jù)占位圖組件
│ ├─goods-action.shtml 商品下單動作組件
│ ├─goods-counter.shtml 商品加購計數(shù)器組件
│ ├─goods-list-item.shtml 主頁商品列表單品組件
│ ├─order-item.shtml 訂單列表單品組件
│ ├─radio-box.shtml 自定義選擇器組件
├─css/ css樣式目錄
├─image/ 圖片素材圖標(biāo)資源目錄
├─pages/ 新版的AVM頁面目錄
│ ├─goods_add
│ │ └─goods_add.stml 加購浮層
│ ├─goods_detail
│ │ └─goods_detail.stml 商品詳情頁
│ ├─main_cart
│ │ └─main_cart.stml 主tab-2 購物車頁面
│ ├─main_home
│ │ └─main_home.stml 主tab-0 商家主頁
│ ├─main_menu
│ │ └─main_menu.stml 主tab-1 點餐菜單頁面
│ ├─main_user
│ │ └─main_user.stml 主tab-3 用戶主頁
│ ├─pay_result
│ │ └─pay_result.stml 支付結(jié)果頁
│ ├─pending_order
│ │ └─pending_order.stml 待付款結(jié)算頁
├─script/ JavaScript腳本目錄
└─config.xml 應(yīng)用配置文件
app.json
配置文件
《點餐》項目的首頁是由一個可以同級切換窗口組構(gòu)成的。 在 APP
原生端 上面, 我們可以借助 FrameGroup
來實現(xiàn)這樣的切換組。 小程序原生上則是使用 app.json
配置文件來 配置定義 TabBar
的相關(guān)屬性 。 為了統(tǒng)一兩端的差異問題,通過在 weight
根目錄下定義一個 app.json
文件,具體字段說明請參考《openTabLayout布局文檔》(未處理) 。 所以,如果只書寫原生端 APP
,而不計劃支持小程序的話,這個配置文件就是可選的了。
在這個配置文件中,可以聲明底部欄的標(biāo)簽文案、對應(yīng)圖標(biāo)的選中和未選中狀態(tài)以及對應(yīng)需要跳轉(zhuǎn)的頁面路徑。 所以需要準(zhǔn)備四個主頁面。 在 pages
目錄準(zhǔn)備建立這四個頁面。 分別是 “商家主頁” main_home
、 “菜單頁面” main_menu
、 “購物車頁面” main_cart
和 “用戶主頁” main_user
。 為了兼容小程序目錄結(jié)構(gòu),需要使用同名文件夾對其包裹一層。
main_home
的編寫
先看到主頁效果圖,然后大致分析一下頁面結(jié)構(gòu)。源代碼在 /widget/pages/main_home/main_home.stml
。 頁面主要部分是一個滾動效果,需要使用一個 scroll-view
來做滾動部分的容器。 頭部有一個固定頭部,并跟隨上面提到的 scroll-view
的滾動高度來做透明度反饋。
布局結(jié)構(gòu)使用系統(tǒng)推薦的 flex
布局。有一點需要注意的是, flex
布局的 flex-direction
默認是 column
, 也就是豎著排列的方向,這一點是和傳統(tǒng)網(wǎng)頁中不一定地方。另外,每一個組件默認會附帶 display:flex;
屬性。
在頁面的生命周期 apiready
中,有一個 this.getData()
的方法,就是在請求數(shù)據(jù)。
function getData() {
GET('shops/getInfo')
.then(data => {
this.data.shopInfo = data;
})
}
這個函數(shù)主要使用一個 GET
方法實現(xiàn)的。這個方法來自于:
import {GET} from "../../script/req";
這個文件中,主要處理了應(yīng)用的請求、會話和異常處理等邏輯。 相關(guān)業(yè)務(wù)代碼可以只是作為參考,具體項目中根據(jù)實際的會話認證方式、服務(wù)接口模式以及個人偏好等方式去組織。
拿到數(shù)據(jù)以后,通過 this.data.shopInfo = data
將數(shù)據(jù)交給到頁面的數(shù)據(jù)域中,以便于接下來的數(shù)據(jù)綁定顯示。
頭部主圖是不會和 scroll-view
一起滾動的,所以它應(yīng)該在滾動容器的外部。使用一個 img
圖片標(biāo)簽來顯示圖片。 其數(shù)據(jù)是來自服務(wù)器接口的數(shù)據(jù), 使用 avm.js
提供的《數(shù)據(jù)綁定》(未處理) 來處理數(shù)據(jù)。
<img class="shop-photo" style={{'height:'+photoRealHeight+'px'}} src={{shopInfo.img}} alt=""/>
商家的營業(yè)信息也同上,按照接口數(shù)據(jù)綁定出相應(yīng)字段,即可顯示出來。
<view class="shop"
style={{'margin-top:'+photoRealHeight+'px'}}>
<view class="shop-header flex-h">
<text class="shop-name flex-1 ellipsis-1">{{ shopInfo.name }}</text>
<img class="shop-phone" @click="callPhone" src="../../image/icon/icon-home-phone.png" alt=""/>
</view>
<view class="content-wrap">
<text class="shop-text shop-address">
{{ shopInfo.city }} {{ shopInfo.country }} {{ shopInfo.address }}
</text>
</view>
<view class="shop-operation content-wrap">
<text class="shop-text">營業(yè)中 09:00 - 13:00,16:00 - 22:00</text>
</view>
</view>
其中電話的圖標(biāo)點擊以后,需要實現(xiàn)撥打電話的效果。為其綁定一個點擊事件,叫做 callPhone
,并在 methods
去實現(xiàn):
function callPhone() {
if (isMP()) {
wx.makePhoneCall({
phoneNumber: this.data.shopInfo.phone
})
} else {
api.call({
type: 'tel_prompt',
number: this.data.shopInfo.phone
});
}
}
仔細觀察這里的模板和數(shù)據(jù),實際上可以分解為 一個主標(biāo)題
加上 一組菜品
這樣的結(jié)構(gòu)來循環(huán)。 其中 一組菜品
再使用循環(huán),渲染出單品。
使用循環(huán)來展示三個分組數(shù)據(jù)。
<view class="list" v-for="item in classifyList">
<goods-list-item class="goods-item" :list="item.togc" :title="item.name"></goods-list-item>
</view>
每一個循環(huán)中包含一個 <goods-list-item />
組件。這個組件來自于自定義組件:
import goodsListItem from '../../components/goods-list-item.stml';
在自定義組件中,完成組件內(nèi)部的組件樣式、數(shù)據(jù)管理和事件響應(yīng)等,符合組件化開發(fā)思想和提高項目的開發(fā)效率和維護性。 在這個組件中,同樣的使用了循環(huán)來處理每個欄目的單品數(shù)據(jù)。 每個單品綁定了一個 intoGoodsDetail
事件來實現(xiàn)跳轉(zhuǎn)到商品詳情頁。
function intoGoodsDetail(item) {
api.openWin({
name: 'goods_detail',
url: '../../pages/goods_detail/goods_detail.stml',
pageParam: {
item
}
})
}
<view class="header-bar"
style={{'opacity:'+this.data.opacity+';padding-top:'+safeAreaTop+'px'}}>
<text class="nav-title shop-name">{{ shopInfo.name }}</text>
</view>
頭部是一個普通的 view
+ text
的結(jié)構(gòu)。為了實現(xiàn)滾動處理透明度,為其綁定一個動態(tài)的 style
屬性。 動態(tài)改變其透明度 opacity
。
而這個 opacity
的取值依賴于 scroll-view
的滾動高度。 scroll-view
的滾動會觸發(fā)相關(guān)數(shù)據(jù)的變動,所以為其綁定上一個滾動事件 @scroll="onScroll"
和相關(guān)處理邏輯 onScroll
。
function onScroll(e) {
const y = isMP() ? e.detail.scrollTop : e.detail.y;
let threshold = this.photoRealHeight - y;
if (threshold < 0) {
threshold = 0;
}
this.data.opacity = 1 - threshold / this.photoRealHeight;
api.setStatusBarStyle && api.setStatusBarStyle({
style: this.statusBarStyle
});
}
在 onScroll
中能夠拿到相應(yīng)的滾動高度,并且計算出透明度的最終結(jié)果。 同時發(fā)現(xiàn)透明度的更改也會伴隨著頂部狀態(tài)欄文本的顏色變化。使用端能力 api.setStatusBarStyle
來進行相應(yīng)設(shè)置。
如此一來,商家主頁的相關(guān)邏輯的數(shù)據(jù)處理的差不多了,同時介紹了基礎(chǔ)的事件和數(shù)據(jù)處理等。
頁面加載的時候,通過頁面?zhèn)鲄⒛玫缴唐吩斍閿?shù)據(jù)。另外一個商品的加購數(shù)量是存在名為 CART-DATA
的全局?jǐn)?shù)據(jù)中,在頁面生命周期函數(shù) apiready
中拿到相關(guān)數(shù)據(jù):
this.data.goods = api.pageParam.item.togoods; // 拿到商品主數(shù)據(jù)
let cartList = api.getPrefs({sync: true, key: 'CART-DATA'}); // 獲取加購數(shù)量
if (cartList) {
cartList = JSON.parse(cartList)
this.data.cartData = cartList[this.data.goods.id];
if (this.data.cartData) {
this.data.count = this.data.cartData.count;
}
}
goods_counter
商品詳情頁使用了兩個自定義組件,一個是 goods_counter
,是一個商品計數(shù)器。 以后其他頁面可能也會使用到,所以將其封裝起來。
<goods-counter onCountChange={this.countChange.bind(this)} :count="count"></goods-counter>
使用一個動態(tài)屬性 :count="count"
將剛剛獲取到的當(dāng)前商品的加購數(shù)量傳入。 在 goods_counter
內(nèi)部,點擊加減按鈕觸發(fā) countChange
事件。在事件中向父頁面?zhèn)鬟f:
function countChange(change) {
if (this.props.count + change === 0) {
return api.toast({
msg: '不能再減少了\n可在購物車編輯模式下移除',
location: 'middle'
})
}
this.fire('CountChange', {
change,
props: this.props
})
}
所以在組件調(diào)用的時候,綁定一個 onCountChange={this.countChange.bind(this)}
。 這里的 this.countChange
是 goods_detail
的函數(shù),在創(chuàng)建組件的時候作為 props
傳遞到了子組件中, 在子組件中可以直接執(zhí)行這個函數(shù),或者是使用 fire
的方式“引燃”這個函數(shù)。
goods_action
商品詳情頁使用了兩個自定義組件,另一個是 goods_action
,是一個商品加購動作條。 主體是兩個按鈕,一個加購,一個結(jié)算。
結(jié)算就是攜帶當(dāng)前單品數(shù)據(jù)到預(yù)付款頁面。邏輯很簡單,就是攜帶數(shù)據(jù)到新頁面。
加購稍微復(fù)雜一點,不過邏輯依然使用 fire
的方式上拋給一個 addCart
的事件到父頁面,因為可能不同的頁面的加購后續(xù)邏輯不太一樣,具體實現(xiàn)就交給父級。 所以視線還是轉(zhuǎn)回到 goods_detail
的 addCart
的實現(xiàn)。
function addCart() {
let cartList = api.getPrefs({sync: true, key: 'CART-DATA'}) || '{}'
cartList = JSON.parse(cartList)
cartList[this.data.goods.id] = {
goods: this.data.goods, count: this.data.count
};
api.setPrefs({
key: 'CART-DATA',
value: cartList
});
api.toast({
msg: '成功加入' + this.data.count + '個到購物車', location: 'middle'
})
setTabBarBadge(2, Object.keys(cartList).length);
}
加購后考慮到相關(guān)購物車頁面和底部小紅點的數(shù)據(jù)。此時如果不考慮小程序的話,也可以直接發(fā)送全局廣播,自行處理相關(guān)邏輯。
這個頁面是一個左右分欄的布局。左邊是菜單分類,右邊的菜品。 有一組比較常見的交互:
其中第一個交互相關(guān)邏輯類似于在開發(fā)商家主頁的滾動 scroll-view
觸發(fā)頭部透明度的邏輯。 所以同樣地為右側(cè)的 scroll-view
綁定上 @scroll="onScroll"
函數(shù)。
具體邏輯請參考源碼的實現(xiàn)部分,獲取滾動高度等和主頁類似。
重點關(guān)注第二個交互的核心在于點擊對應(yīng)分類,右側(cè)的 scroll-view
需要滾動到指定位置。 使用屬性來進行位置綁定: scroll-top={scrollTo}
。此時只需要在左邊的分類點擊事件 @click="switchCategory(index)"
計算出正確的 scrollTo
即可實現(xiàn)。
function switchCategory(index) {
this.data.categoryIndex = index;
this.data.CD = new Date().getTime() + 500; // 手動切換分類后需要鎖定500毫秒 避免右側(cè)scroll-view滾動時帶來次生問題
this.data.scrollTo = this.offsetList[index];
}
右側(cè)的菜品有一個 @click="openAdd(goods)"
事件,用于打開加購頁面。
function openAdd(goods) {
if (isMP()) {
this.data.currentGoods = goods;
wx.hideTabBar();
} else {
api.openFrame({
name: 'goods_add',
url: '../goods_add/goods_add.stml',
pageParam: {goods}
})
}
}
這個函數(shù)中展示了端差異上的處理。因為小程序沒有類似 APICloud
的 frame
的概念, 所以新彈出的頁面在小程序上,是一個頁面內(nèi)部組件實現(xiàn)的。
當(dāng)然這種方式 APP
原生端也是支持的。如果需要進一步提高性能,發(fā)揮原生優(yōu)勢,則可以使用原生端的frame
來完成。 此時,將目標(biāo)頁面封裝在一個自定義組件中,并把當(dāng)前菜品數(shù)據(jù)傳遞進去。
目前組件和 frame
頁面的獲參形式暫時不同。在 goods_add
這個組件中的 installed
生命周期中可以看到如下的兼容片段:
this.data.goods = this.props.goods ? this.props.goods : api.pageParam.goods;
在新展開的加購浮層上,看到了之前定義的 goods_action
,所以大致邏輯也是獲取商品數(shù)據(jù)和加購數(shù),并實現(xiàn)一下addCart函數(shù)。 實際上這個頁面很類似商品詳情頁,只是展示UI不太相同。
safe-area
在這個頁面中,自己實現(xiàn)了一個頂部導(dǎo)航欄。沉浸式狀態(tài)欄一般會需要獲取狀態(tài)欄高度等處理能力。 在 avm.js
中提供一個 safe-area
組件,用于自動處理異形屏的邊界問題。
<safe-area>
<view class="header">
<text class="title">菜單</text>
</view>
</safe-area>
在主頁中,也看到相關(guān)編程式獲取安全區(qū)域數(shù)據(jù)的代碼:
this.data.safeAreaTop = api.safeArea ? api.safeArea.top : 0;
購物車頁面是一個比較經(jīng)典的展示相關(guān)頁面內(nèi)部邏輯的案例。
在頁面初始化的時候, this.getCartData()
拿到本地存儲的購物車所有的數(shù)據(jù)。
function getCartData() {
let cartData = api.getPrefs({sync: true, key: 'CART-DATA'});
if (cartData) {
cartData = JSON.parse(cartData);
this.data.cartData = cartData;
this.generateCartList();
setTabBarBadge(2, Object.keys(cartData).length);
}
}
其中還混合了一個 generateCartList
邏輯。
function generateCartList() {
let cartData = this.data.cartData;
let arr = [];
for (let i in cartData) {
arr.push({checked: true, ...cartData[i]});
}
this.data.cartList = arr;
}
這是一個生成函數(shù),是將保存的對象構(gòu)建為頁面所需要的數(shù)組結(jié)構(gòu),同時增加每一個元素的 checked
屬性。 然后再頁面部分通過 v-for
來循環(huán)當(dāng)前購物車的數(shù)據(jù)。
<view class="main-cart-goods-item" v-for="item in cartList">
<radio-box class="main-cart-radio-box" :checked="item.checked"
onChange={this.radioToggle.bind(this)}
:item="item"></radio-box>
<img class="main-cart-goods-pic" mode="aspectFill" src={{item.goods.thumbnail}} alt=""/>
<view class="main-cart-goods-info">
<text class="main-cart-goods-name">{{ item.goods.name }}</text>
<view class="main-cart-flex-h">
<text class="main-cart-goods-price-signal">¥</text>
<text class="main-cart-goods-price-num">{{ item.goods.curt_price }}</text>
<goods-counter onCountChange={this.countChange.bind(this)}
:count="item.count" :item="item"></goods-counter>
</view>
</view>
</view>
注意到每一個條目的開頭嵌套了一個 <radio-box/>
自定義組件。 這個組件擔(dān)負的任務(wù)很簡單,就是使用自定的樣式來渲染一個單選框。當(dāng)然 avm.js
自帶的系統(tǒng)組件 radio
(未處理) 也是可以實現(xiàn)的。
下面有一個全選按鈕,用于控制是否全選。
function checkAll() {
const checked = !this.allChecked;
for (let i = 0; i < this.data.cartList.length; i++) {
this.data.cartList[i].checked = checked;
}
}
而這個函數(shù)第一行以來的 this.allChecked
則是一個計算屬性。在 computed
中能找到它的實現(xiàn):
function allChecked() {
return !this.cartList.some((item) => { // 也可以使用 every 來修改相反邏輯實現(xiàn)
return !item.checked;
})
}
緊接著它下面還有另外一個計算屬性: totalPrice
:
function totalPrice() {
// 先篩選出選中項
let list = this.data.cartList.filter(item => {
return item.checked;
})
// 再計算總和并且格式化結(jié)果
return (list.length ? list.reduce((total, item) => {
return total + item.goods.curt_price * item.count;
}, 0) : 0).toFixed(2);
}
然后再模板中直接使用這個結(jié)果,即可完成總價的顯示:
<view class="text-group">
<text class="main-cart-footer-text">合計</text>
<text class="main-cart-footer-price">¥{{ totalPrice }}</text>
</view>
可以看到,計算屬性 computed
是可以通過一些邏輯計算出需要的結(jié)果,并且會暴露給實例本身, 在模板中能夠同數(shù)據(jù)一樣綁定。 同時能夠自動處理所依賴的數(shù)據(jù)變化,做出實時的更新。
在頁面中,有一個變量標(biāo)記 isEdit
,用來表示當(dāng)前頁面是否是在處于編輯狀態(tài)。
<view @click="toggleEdit">
<text class="main-cart-finnish-text" v-if="isEdit">完成</text>
<view v-else class="main-cart-action">
<img class="main-cart-action-icon" src="../../image/icon/icon-cart-edit.png" alt=""/>
<text class="main-cart-action-text">編輯</text>
</view>
</view>
根據(jù)編輯狀態(tài)的切換,右上角的按鈕文案變化為“完成”和“編輯”兩種狀態(tài)。這個時候就可以通過 v-if
來判斷渲染。 下面的結(jié)算、移除按鈕也是一樣,只不過是在模板中使用了三元表達式來做顯示。
<text class="main-cart-footer-btn-text">{{ isEdit ? '移除' : '去結(jié)算' }}</text>
這個頁面主要有兩個要點:頭部用戶信息區(qū)域和訂單列表。
頭部的用戶信息需要在初始化的時候讀取本地用戶數(shù)據(jù)。
/**
* 獲取用戶信息
* @returns {boolean|any}
*/
function getUser() {
let user = api.getPrefs({
sync: true,
key: 'USER'
});
if (user) {
return JSON.parse(user)
}
return false;
}
把獲取到的用戶數(shù)據(jù)作為一個普通的頁面數(shù)據(jù),用來渲染用戶信息面板。 如果用戶數(shù)據(jù)不存在,也就是未登錄模式,則需要使用 v-if
條件渲染來展示登錄界面。
<view class="user-info flex flex-h flex-center-v" v-if="userInfo" @click="logout">
<img class="user-avatar" src={{userInfo.avatarUrl}} alt=""/>
<text class="user-name">{{ userInfo.nickName }}</text>
</view>
<view class="user-info flex flex-h flex-center-v" v-else @click="wxLogin">
<img class="user-avatar" src="../../image/icon/icon-user-avatar.png" alt=""/>
<text class="user-name">使用微信登錄</text>
</view>
在未登錄的情況下,上面的第二塊會展示,點擊觸發(fā) wxLogin
方法:
function wxLogin() {
if (isMP()) {
this.mpLogin();
} else {
this.doLogin({ssid: getDeviceId()});
}
}
這里依然需要對特性平臺差異化處理。因為原生端和小程序端使用微信登錄是兩個不同的邏輯。 源代碼 /widget/pages/main_user/main_user.stml
中還展示了一些使用原生模塊來調(diào)用微信來登錄的邏輯。
登錄成功以后,開始執(zhí)行 loginSuccess
,可以保存相關(guān)用戶信息和會話信息,以備以后的使用。同時還需要刷新用戶的購物列表。 如果在真實項目中其他已經(jīng)打開的頁面也需要監(jiān)測用戶狀態(tài)變化,可以借助廣播事件來處理詳細的邏輯。
function loginSuccess(userInfo) {
api.setPrefs({
key: 'USER',
value: userInfo
});
this.data.userInfo = userInfo;
this.getOrderList();
}
頁面下拉刷新和觸底加載依賴于 scroll-view
的相關(guān)事件綁定和實現(xiàn)。
<scroll-view scroll-y class="flex-1 main-user-scroll-view"
enable-back-to-top refresher-enabled
refresher-triggered={{loading}}
@refresherrefresh="onRefresh">
<view v-if="orderList.length">
<order-item :order="order" v-for="order in orderList"
onOrderAction={this.orderAction.bind(this)}></order-item>
</view>
<view class="empty-block" v-else>
<empty-block text="暫無訂單哦~" type="order"></empty-block>
</view>
</scroll-view>
其中 @refresherrefresh="onRefresh"
就是在下拉刷新需要觸發(fā)的邏輯。 refresher-triggered={{loading}}
就是下拉刷新的狀態(tài)。(用于通知回彈和設(shè)置刷新中)。
function onRefresh() {
this.data.loading = true; // 設(shè)置正在刷新
if (this.data.userInfo) { //有用戶信息了才刷新
this.getOrderList();
} else {
setTimeout(_ => {
this.data.loading = false;
api.toast({
msg: '請登錄后查看歷史訂單'
})
}, 1000)
}
}
主頁的開發(fā)大致就完成了,下面關(guān)注一下付款下單的過程。
該頁面也比較簡單,大多數(shù)實現(xiàn)的邏輯在前面的頁面已經(jīng)提及。 此外有一個輸入框表單 ,用來收集用戶的輸入備注信息。
<view class="order-note">
<text class="order-note-key">備注</text>
<input class="order-note-input" placeholder="如需備注請輸入"
onBlur="onBlur" maxlength="30" id="remark"/>
</view>
通過失去焦點事件 onBlur="onBlur"
來動態(tài)獲取數(shù)據(jù)。
function onBlur(e) {
this.data.remark = e.target.value;
}
獲取數(shù)據(jù)也還有其他多種方式,可以進一步參考組件 input
以及其他表單組件文檔。
開始提交訂單,和服務(wù)器通信下單并且支付。下單完成后做一些聯(lián)動處理:
function addOrder() {
POST('orders/app_addorder', this.formData).then(data => {
// 打開結(jié)果頁
api.openWin({
name: 'pay_result',
url: '../pay_result/pay_result.stml'
});
// 通知支付成功 刷新訂單頁面
api.sendEvent({
name: 'PAY-SUCCESS'
})
// 清空購物車
api.setPrefs({
key: 'CART-DATA',
value: {}
});
setTabBarBadge(2, 0);
})
}
下單支付后跳轉(zhuǎn)到支付結(jié)果頁面。(這個過程是模擬成功下單,中間可以參考微信登錄過程嵌套第三方支付)
至此,所有的頁面邏輯主線已經(jīng)完成。應(yīng)用中還有一些細節(jié)處理,可以參考源碼和文檔進一步學(xué)習(xí)研究。
《堂食點餐》模板的數(shù)據(jù)云模型和云函數(shù)現(xiàn)已上線到數(shù)據(jù)云預(yù)置模型中了。進入項目的控制面板,選擇“云開發(fā)”中的“云設(shè)置”。 如果是第一次打開這個界面,數(shù)據(jù)云默認是么有開啟的。需要點擊歡迎頁的開啟按鈕,即可開啟數(shù)據(jù)云。
開啟數(shù)據(jù)云之后,可以在“云設(shè)置”頁面進行一些基礎(chǔ)設(shè)置。 接下來重點關(guān)注到“數(shù)據(jù)模型”頁面。點擊“數(shù)據(jù)模型”打開相關(guān)頁面,我們可以自行創(chuàng)建模型和云函數(shù),也可以在右側(cè)“預(yù)制模型”中看到“堂食點餐"同名模型。 點擊右下角綠色小加號,將該模型進行導(dǎo)入。
導(dǎo)入成功以后,可以在左側(cè)看到相應(yīng)的數(shù)據(jù)模型已經(jīng)顯示出來。點選模型,可以進入相關(guān)模型數(shù)據(jù)的預(yù)覽。 或者是點擊左側(cè)底部的“云函數(shù)開發(fā)”會彈出云函數(shù)管理浮層,浮層中間是使用引導(dǎo)和文檔鏈接??梢渣c選左側(cè)頂部的綠色按鈕進行創(chuàng)作新的云函數(shù), 也可以點選已有的云函數(shù),學(xué)習(xí)研究預(yù)置的函數(shù)和接口是如何設(shè)計的。
以左側(cè)的 shop
模型為例,點擊模型打開“遠程函數(shù)”。在遠程函數(shù)中找到 getInfo
接口,點選后右側(cè)就會展現(xiàn)相關(guān)代碼實現(xiàn)。 此時需要進行一次全量發(fā)布,點擊右側(cè)上方的發(fā)布右側(cè)的下拉箭頭,選擇全量發(fā)布,將剛剛導(dǎo)入的所有模型和云函數(shù)發(fā)布并生效。
接下來可以點擊接口聯(lián)調(diào),打開API接口生成列表。在 shop
分組下找到 getInfo
接口,并可以點擊 “Try it out”進行接口測試。
請求后將會看到完整的請求地址。
接下來打開App端的源碼,找到 script/req.js
大約第三行的位置,將代碼中的請求二級前綴更改為項目的真實API路徑。 例如:
const config = {
schema: 'https',
- host: 'a7777777777777-pd.apicloud-saas.com',
+ host: 'a6176110219206-dev.apicloud-saas.com',
path: 'api'
}
保存后,打開首頁開始測試一下: 進入 pages/main_home/main_home.stml
頁面,右鍵點擊空白區(qū)域,選擇“實時預(yù)覽”。
稍等片刻,在右側(cè)的預(yù)覽區(qū)域?qū)霈F(xiàn)預(yù)覽畫面。 點擊地址后面的復(fù)制圖標(biāo),拿到預(yù)覽地址。放置到chrome等瀏覽器中可以觀察請求,確認渲染數(shù)據(jù)的確是來自當(dāng)前項目的數(shù)據(jù)云接口的模型數(shù)據(jù)。
云模型也就是云數(shù)據(jù)庫??梢源嫒I(yè)務(wù)數(shù)據(jù),還提供了數(shù)據(jù)訪問的接口和相關(guān)API。
在一個項目中可以建立業(yè)務(wù)所需要的數(shù)據(jù)表模型。還是以 shop
為例:打開模型后,是一個表格的形式展現(xiàn)了模型內(nèi)存在的數(shù)據(jù)。
表頭的內(nèi)容是該模型的字段,表中的數(shù)據(jù)是模型下保存的記錄??梢栽陬^部的按鈕中進行添加數(shù)據(jù)、刪除數(shù)據(jù)、添加字段、設(shè)置關(guān)聯(lián)等管理操作。
獲取商家信息
GET` `/shops/getInfo
shop.getInfo = async()=> {
try{
const data = await shop.findOne({where: {"status":1}});
return {status:0,msg:"成功",data:data};
}catch(err){
return {status:1,msg:"獲取商家信息失敗!",data:err};
}
};
通過閱讀上面的云函數(shù)源代碼,可以看到一個云函數(shù)組成是十分簡單的。在編輯狀態(tài)下,可以看到表單中顯示出了一個云函數(shù)的一些必要元素: 選擇 Model
,確定函數(shù)類型為“遠程函數(shù)”,選擇請求類型為“get”。完善函數(shù)名稱和描述,最后設(shè)置一個函數(shù)(方法)名, 作為函數(shù)名稱,也是遠程接口的訪問地址。
在函數(shù)中,通過模型的數(shù)據(jù)操作api來對接口做具體的功能實現(xiàn):在上面的代碼中, 就是從 shop
模型中找到一個 status
為 1
的一條符合條件的數(shù)據(jù)??梢钥吹剑檎覘l件是以 JSON
的形式放在 where
條件中的。
正常情況下,成功找到數(shù)據(jù)并使用 return
關(guān)鍵字為函數(shù)返回值。而這個值也會作為云函數(shù)生成的接口的 response
的數(shù)據(jù)域, 來返回給前端。
使用 try-catch
代碼塊來捕獲相關(guān)錯誤,如果查找失敗的情況下,并且也會返回給前端。
關(guān)于更多的模型方法可以參考數(shù)據(jù)云3的完整文檔。
除了模型和云函數(shù)之外,數(shù)據(jù)云3還提供一個快速后臺生成管理系統(tǒng)。
為方便用戶使用,我們內(nèi)置了管理后臺模塊,用戶開啟服務(wù)后可通過 "https://appid-dev.apicloud-saas.com/admin/" 在測試環(huán)境進行訪問。
此功能需要全局配置開啟session服務(wù)以及開通文件存儲,請在全局配置進行相關(guān)操作。
還是上面的例子:假設(shè)當(dāng)前的 APPID
是 a6176110219206
,那么對應(yīng)的管理地址就是:https://a6176110219206-dev.apicloud-saas.com/admin/ 默認的賬號和密碼是: 賬號:admin
密碼:123456
在后臺可以進行相關(guān)數(shù)據(jù)設(shè)置和頁面快速開發(fā)。后臺頁面開發(fā)的規(guī)則是引入了低代碼框架 AMIS 。詳細可以參閱:https://baidu.github.io/amis/zh-CN/docs/
回到數(shù)據(jù)云面板,可以查看示例模型和云函數(shù)。還可以通過數(shù)據(jù)云完整文檔學(xué)習(xí)完整的數(shù)據(jù)云使用方法。 數(shù)據(jù)云文檔鏈接:https://docs.apicloud.com/Cloud-API/sentosa(未處理)
更多建議: