此項目爲拼團商城類型,主要功能包括商品分類、商品詳情、商品搜索、拼團、訂單管理等。 項目源碼在 https://github.com/apicloudcom/group-ec 倉庫的 widget 目錄下。html
項目中前端採用 avm 多端開發技術進行開發,要點包括 TabLayout 佈局、swiper 輪播圖、rich-text 富文本、scroll-view 滾動視圖、下拉刷新、組件封裝等。使用 APICloud 多端技術進行開發,實現一套代碼多端運行,支持編譯成 Android & iOS App 以及微信小程序。前端
項目後端則是使用的 APICloud 數據雲 3.0 自定義雲函數來構建的。node
使用步驟
1. 使用 APICloud Studio 3 做爲開發工具。git
2. 下載本項目源碼。github
3. 在開發工具中新建項目,並將本源碼導入新建的項目中,注意更新 config.xml 中的 appid 爲你項目的 appid。ajax
4. 使用 AppLoader 進行真機同步調試預覽。json
5. 或者提交項目源碼,併爲當前項目雲編譯自定義 Loader 進行真機同步調試預覽。小程序
6. 雲編譯 生成 Android & iOS App 以及微信小程序源碼包。後端
若是以前未接觸過 APICloud 開發,建議先了解一個簡單項目的初始化、預覽、調試和打包等操做,請參考 APICloud 多端開發快速上手教程。微信小程序
網絡請求接口封裝
在 script/util.js 中,封裝了統一的網絡請求接口 ajax 方法,對整個項目的請求進行統一管理,包括處理傳入參數、拼裝完整請求 url、設置請求頭等,最後調用 api.ajax 方法發起請求,在請求的回調方法裏面還對 cookie 是否過時作了全局判斷,過時後會清除本地用戶登陸信息,並提示從新登陸。
// util.js ajax(p, callback) { var param = p; if (!param.headers) { param.headers = {}; } param.headers['X-AppToken'] = UserCenter.getAccessToken(); if (param.data && param.data.body) { param.headers['Content-Type'] = 'application/json; charset=utf-8'; } if (param.url) { var baseUrl = 'https://a6047344573226-dev.apicloud-saas.com/api/'; param.url = baseUrl + param.url; } api.ajax(param, (res, err)=> { let ret = res; if (err && err.body && err.body.errCode) { ret = err.body; callback(ret); } else { callback(ret, err); } let sessionExpiration = false; if (ret && ret.errCode && ret.errCode>100) { sessionExpiration = true; } if (sessionExpiration) { var didShowLogoutAlert = api.getGlobalData({ key: 'didShowLogoutAlert' }); if (!didShowLogoutAlert) { api.setGlobalData({ key: 'didShowLogoutAlert', value: true }); UserCenter.setUserInfo(''); api.confirm({ msg: '登陸已失效,請從新登陸', buttons: ['取消', '從新登陸'] }, (ret)=> { api.setGlobalData({ key: 'didShowLogoutAlert', value: false }); if (ret.buttonIndex == 2) { this.goLogin(); } }); } } }); }
用戶登陸信息管理
在 script/user.js 中,對用戶登陸信息進行了封裝,作了統一管理,能夠方便地判斷是否登陸、保存和獲取用戶信息、以及判斷登陸是否過時的 accessToken 等。
const UserCenter = { isLogin(){ let access_token = this.getAccessToken(); return access_token?true:false; }, setUserInfo(userInfo){ delete userInfo.addtime; api.setPrefs({ key: 'userInfo', value: userInfo }); api.setPrefs({ key: 'access_token', value: userInfo.access_token?userInfo.access_token:'' }); }, getUserInfo(){ let userInfo = api.getPrefs({ sync: true, key: 'userInfo' }); return userInfo?JSON.parse(userInfo):''; }, getAccessToken(){ return api.getPrefs({ sync: true, key: 'access_token' }); } }; export default UserCenter;
TabBar 和導航欄的實現
首頁使用了 TabLayout 佈局來實現 TabBar 和導航欄,在 config.xml 裏面配置 content 字段,值爲 json 文件路徑,在 json 文件中配置 TabBar、導航欄和頁面信息。
// config.xml <content src="config.json" />
config.json 文件內容以下,設置了 navigationBar 的背景色和標題文字顏色,設置了 tabBar 每項的 icon 和文字,以及每項對應的頁面。
{ "name": "root", "hideNavigationBar": false, "navigationBar": { "background": "#fff", "color": "#000", "shadow": "#f1f1f1", "hideBackButton": true }, "tabBar": { "scrollEnabled": false, "background": "#fff", "shadow": "#f1f1f1", "color": "#aaa", "selectedColor": "#339DFF", "preload": 0, "frames": [{ "name": "page1", "url": "pages/main1/main1.stml", "title": "拼團商城" }, { "name": "page2", "url": "pages/main2/main2.stml", "title": "分類" }, { "name": "page4", "url": "pages/main4/main4.stml", "title": "個人" }], "list": [{ "text": "首頁", "iconPath": "images/common/main1_1.png", "selectedIconPath": "images/common/main1.png" }, { "text": "分類", "iconPath": "images/common/main2_1.png", "selectedIconPath": "images/common/main2.png" }, { "text": "個人", "iconPath": "images/common/main4_1.png", "selectedIconPath": "images/common/main4.png" }] } }
這裏」個人「頁面隱藏了導航欄,而其它頁面沒有隱藏。」個人「頁面路徑爲 pages/main4/main4.stml,咱們參照微信小程序的語法,在同目錄下放置了 main4.json 文件,在裏面配置 navigationStyle 字段爲 custom。
{ "navigationStyle":"custom" }
在首頁 main1.stml 的 apiready 方法裏面則監聽了 tabBar 每項的點擊事件,在 App 端,咱們須要在點擊事件裏面動態設置頁面顯示、隱藏導航欄。
// index.stml api.addEventListener({ name:'tabitembtn' }, function(ret){ var hideNavigationBar = ret.index == 2; api.setTabLayoutAttr({ hideNavigationBar: hideNavigationBar, animated: false }); api.setTabBarAttr({ index: ret.index }); });
輪播圖實現
首頁和商品詳情頁面都使用了輪播圖,這裏以首頁爲例,首頁路徑爲 pages/main1/main1.stml,裏面輪播圖使用 swiper 組件實現,使用 v-for 指令循環 swiper-item,bannersList 爲定義的數組類型的屬性。這裏監聽了圖片的 click 事件,點擊後須要跳轉到對應的詳情頁面。這裏使用了自定義的指示器,經過設置指示器容器的 position 定位屬性爲 absolute,來讓指示器顯示到當前輪播圖的上面。
<view class="banner_box" style={'height:'+swiperHeight+'px;'}> <swiper class="banner_swiper" circular autoplay11 onchange="fnSwiperChange"> <swiper-item v-for="(item_, index_) in bannerList"> <image class="banner_img" src={item_.icon} mode="aspectFill" onclick="fnBannerPage" data-index={index_}></image> </swiper-item> </swiper> <view class="banner_dots"> <view v-for="(item, index) in bannerList" class={current == index ? 'banner_dot-on' : 'banner_dot'}></view> </view> </view>
爲保持不一樣分辨率設備上面圖片顯示比例不變,須要讓輪播圖的寬度跟隨屏幕寬度變化,高度則經過計算屬性 swiperHeight 來動態計算獲得。
computed:{ swiperHeight(){ return Math.floor((api.winWidth - 30)*0.4+20); } }
rich-text 富文本的使用
在商品詳情頁中,商品詳情部分就是使用的 rich-text 來展現的,使用時若是沒爲 rich-text 設置高度,其高度就爲裏面內容的高度。
<rich-text nodes={html}></rich-text>
rich-text 用於展現 HTML String 片斷,在從服務器獲取到 HTML String 後,咱們調用 $util.fitRichText 方法處理了一下 HTML String,在 fitRichText 方法中爲 img 標籤加了最大寬度的限制,以防止圖片寬度過大致使顯示溢出。
// util.js fitRichText(richtext, width){ var str = `<img style="max-width:${width}px;"`; var result = richtext.replace(/\<img/gi, str); return result; }
下拉刷新、滾動到底部加載更多
在」分類商品列表「頁面(pages/goodslist/goodslist.stml),經過 scroll-view 實現了商品列表展現,同時實現了下拉刷新、滾動到底部加載更多功能。
<scroll-view class="scroll-view" scroll-y enable-back-to-top refresher-enabled refresher-triggered={refresherTriggered} onrefresherrefresh="onrefresherrefresh" onscrolltolower="onscrolltolower"> <list-item v-for="(item) in goodsList" item={item} showOriginalPrice onitemClick="fnOpenDetails"></list-item> <no-data v-if={showNoData} image="../../images/common/nolist.png" desc="暫無商品"></no-data> </scroll-view>
下拉刷新使用了 scroll-view 默認的下拉刷新樣式,使用 refresher-enabled 字段來開啓下拉刷新,爲 refresher-triggered 字段綁定了 refresherTriggered 屬性來控制下拉刷新狀態,須要注意的是,在刷新的事件回調方法裏面,咱們須要主動設置 refresherTriggered 的值爲 true,在數據加載完成後再設置爲 false,這樣綁定的值有變化,刷新狀態才能通知到原生裏面。
onrefresherrefresh(){ this.data.refresherTriggered = true; this.getData(false); }
滾動到底部監聽了 scroll-view 的 scrolltolower 事件,在滾動到底部後自動加載更多數據,加載更多和下拉刷新都是調用 loadData 方法請求數據,經過 loadMore 參數來進行區分,作分頁請求處理。
getData(loadMore){ let that = this; if (!loadMore) { that.data.page = 1; } this.data.loading = true; var url = "homes/getGoodsList?classid=" + that.data.classId + "&page=" + that.data.page; $util.ajax({ url: url }, function(res, err){ if (res && res.errcode == 0) { let list = res.data; that.data.haveMore = list.length > 0; if (loadMore) { that.data.goodsList = that.data.goodsList.concat(list); } else { that.data.goodsList = list; } if (list.length > 0) { that.data.page += 1; } } that.data.loading = false; that.data.refresherTriggered = false; that.data.showNoData = that.data.goodsList.length == 0; $util.hideProgress(); }); }
自定義三級聯動城市選擇器組件
在填寫收貨地址頁面(pages/address_edit/address_edit.stml)裏面有一需求,爲選擇收貨地址城市區域,爲此咱們在 picker 組件的基礎上封裝了一個 region-picker 組件(components/region-picker.stml),使用時監聽該組件的 change 事件,就能夠獲取到選擇的城市區域的名稱和城市代碼。
// address_edit.stml <region-picker region={qustr||''} onchange="fnChooseStr"></region-picker> fnChooseStr(e){ let code = e.detail.code; let val = e.detail.value; this.data.quid = code.join(","); this.data.qustr = val.join(","); }
平臺差別化處理
在多端開發中,不免會遇到不一樣平臺差別化的地方,須要在運行期間作判斷處理,爲此在 utils/util.js 中封裝了 isApp、isMp 方法,裏面經過 api.platform 屬性判斷當前運行環境是 App 端仍是小程序端。
// util.js isApp(){ if (api.platform && api.platform == 'app') { return true; } return false; }, isMp(){ if (api.platform && api.platform == 'mp') { return true; } return false; }