想學一下微信小程序,發現文檔這東西,干看真沒啥意思。所以打算自己先動手擼一個。摩拜單車有自己的小程序,基本功能都有,方便又小巧,甚是喜愛。於是我就萌生了一個給ofo共享單車擼一個小程序(不知道為啥ofo沒有小程序)的想法。Let's do it!
由於本文篇幅過長,影響瀏覽體驗,我對這篇文章做了一下拆分,修正了一些錯誤。有需要的可以移步瀏覽
后續: 有位php攻城獅根據此前端項目添加了后台數據支持,詳情請轉: http://www.jianshu.com/p/8a5687a15648
先上一波效果圖:







1.准備工作
微信小程序當然屬於騰訊大佬的(給大佬遞茶):微信小程序開發者工具,騰訊開放了小程序個人開發平台,只需要一個微信號就可以成為小程序開發者了。
2.小程序頁面
打開小程序開發者工具,用微信掃碼登錄,創建一個默認的小程序。界面是醬的:

pages文件夾下存放着小程序所有的業務頁面;
index文件夾就是一個頁面,index.wxml是頁面的結構文件,類似html。
index.wxss是頁面的樣式,其實就是css;index.js是頁面的邏輯,數據請求與渲染都是都在這個頁面完成。
logs文件夾存放着小程序開發日志,目前暫時用不到。
utils.js可以編寫自己的JavaScript插件。
app.js處理全局的一些邏輯,比如定義全局變量存放獲取的用戶信息,這樣每個頁面都可以獲取用戶信息。
app.json 是全局配置文件,比如設置標題欄的背景色等。
app.wxss 存放頁面的公共樣式,如果多個頁面需要用到同一樣式,就可以寫在這里。
項目按鈕顯示預覽二維碼,用於真機調試。必須真機調試測試代碼
3.創建頁面結構
上一節已經分析了默認的文件結構以及它們的功能,現在我們要創建ofo小程序所需要的頁面。
- 1.刪除pages下默認的index文件夾,logs/utils文件夾可選擇性刪除
- 2.在與pages同級目錄下創建images文件夾,存放頁面需要用到的圖標,下載圖標
- 3.本小程序不需要在app.js里面編寫內容,可以注釋這里面的代碼
- 4.在app.json里,刪掉默認代碼,編寫如下代碼(app.json文件里不能有任何注釋,這里是為了描述頁面功能更直觀):
{
"pages":[ "pages/index/index", // 地圖頁 "pages/warn/index", // 車輛報障頁 "pages/scanresult/index", // 掃碼成功頁 "pages/billing/index", // 開始計費頁 "pages/my/index", // 賬戶頁 "pages/wallet/index", // 錢包頁 "pages/charge/index", // 充值頁 "pages/logs/logs" // 日志頁 ], "window":{ "backgroundTextStyle":"light", "navigationBarBackgroundColor": "#b9dd08", // 標題欄背景色 "navigationBarTitleText": "ofo 共享單車", // 標題欄文字 "navigationBarTextStyle":"black" // 標題欄文字樣式 } }
- 5.app.wxss是通用樣式,先添加幾個通用樣式,以后用得到:
/**app.wxss**/ .container{ background-color: #f2f2f2; height: 100vh; } .title{ background-color: #f2f2f2; padding: 30rpx 0 30rpx 50rpx; font-size: 28rpx; color: #000; } .tapbar{ display: flex; align-items: center; justify-content: space-between; background-color: #fff; padding: 40rpx; } .btn-charge{ width: 90%; background-color: #b9dd08; margin: 40rpx auto 30rpx; text-align: center; }
保存后,你的pages文件夾下就是這樣的界面了(在app.json下創建路徑會自動創建文件夾,賊方便)

4.編寫地圖首頁 (index文件夾)
先來回看一下效果圖

頁面分析:
1.整頁顯示地圖,寬高占手機窗口的100%;
2.地圖之上有五個按鈕圖標和多個黃色ofo標記:定位按鈕,立即用車按鈕,舉報按鈕,黃色頭像按鈕和位於地圖中心的標記。
4.1 要在整頁顯示地圖,我們可以在index.wxml引入地圖組件:
<!--index.wxml--> <view class="container"> <map id="ofoMap" latitude="{{latitude}}" // 緯度 longitude="{{longitude}}" // 經度 scale="{{scale}}" // 縮放級別 show-location/> // 顯示帶有方向的小圓點 </view>
{{...}} 里面是數據變量,由js里的data對象定義。
4.2 初始化數據,在index.js的data對象里添加如下代碼:
//index.js Page({ data: { scale: 18, // 縮放級別,默認18,數值在0~18之間 latitude: 0, // 給個默認值 longitude: 0 // 給個默認值 }, onLoad:function(options){ // 頁面初始化 options為頁面跳轉所帶來的參數 }, onReady:function(){ // 頁面渲染完成 }, onShow:function(){ // 頁面顯示 }, onHide:function(){ // 頁面隱藏 }, onUnload:function(){ // 頁面關閉 }
這樣我們的地圖就默認中心位置為經度 0,緯度0。當然可能在開發者工具里顯示不出來,莫慌!這不是我們想要的,我們想要的是我們自己的位置,所以得先獲取我們當前所在位置的經緯度,在index.js里的onLoad方法里添加如下代碼:
onLoad: function(options){ // 頁面初始化 options為頁面跳轉所帶來的參數 // 調用wx.getLocation系統API,獲取並設置當前位置經緯度 wx.getLocation({ type: "gcj02", // 坐標系類型 // 獲取經緯度成功回調 success: (res) => { // es6 箭頭函數,可以解綁當前作用域的this指向,使得下面的this可以綁定到Page對象 this.setData({ // 為data對象里定義的經緯度默認值設置成獲取到的真實經緯度,這樣就可以在地圖上顯示我們的真實位置 longitude: res.longitude, latitude: res.latitude }) } }); }
res是一個數據對象,它是由調用的對應API傳過來的,如果你想知道res的具體值,可以在成功回調函數里打印,然后在控制台輸出:console.log(res)。在調用一個陌生API的時候可以用這種方法查看返回的對象數據,對處理邏輯很有幫助。
我們在地圖上顯示了我們的真實位置,但沒有移動到中心位置,wx.moveToLocation()函數可以把當前位置移動到地圖中心。修改index.js:
//index.js var app = getApp() Page({ data: { scale: 18, latitude: 0, longitude: 0 }, // 頁面加載 onLoad: function(options){ // 1.頁面初始化 options為頁面跳轉所帶來的參數 // 2.調用wx.getLocation系統API,獲取並設置當前位置經緯度 wx.getLocation({ type: "gcj02", // 坐標系類型 // 獲取經緯度成功回調 success: (res) => { // es6 箭頭函數,可以解綁當前作用域的this指向,使得下面的this可以綁定到Page對象 this.setData({ // 為data對象里定義的經緯度默認值設置成獲取到的真實經緯度,這樣就可以在地圖上顯示我們的真實位置 longitude: res.longitude, latitude: res.latitude }) } }); } // 頁面顯示 onShow: function(){ // 1.創建地圖上下文,移動當前位置到地圖中心 this.mapCtx = wx.createMapContext("ofoMap"); // 地圖組件的id this.movetoPosition() }, // 定位函數,移動位置到地圖中心 movetoPosition: function(){ this.mapCtx.moveToLocation(); } })
這樣,頁面一顯示就在地圖中心顯示當前位置。
4.3 滿屏顯示地圖,在index.wxss里編寫樣式:
/**index.wxss**/ .container{ position: relative; width: 100%; // 寬度占滿設備 height: 100vh; // 高度占滿設備 } #ofoMap{ position: absolute; left: 0; top: 0; right: 0; bottom: 0; width: 100%; height: 100%; z-index: 1; }
保存刷新,整個屏幕就都顯示地圖了>_<
4.4 添加地圖上的按鈕
其實這里的按鈕不是真正的按鈕,它們屬於地圖上的控件屬性,並且不隨着地圖移動。這里有一個坑:
地圖組件屬於微信原生組件,層級最高,任何元素都不能在地圖之上顯示,即無論設置多大z-index都無法顯示。所以,要想在地圖上添加按鈕來滿足需求,就要用到地圖控件屬性。更多地圖控件屬性說明看這里
要添加地圖控件,先在地圖組件里聲明controls="...":
<!--index.wxml--> <view class="container"> <map id="ofoMap" latitude="{{latitude}}" // 緯度 longitude="{{longitude}}" // 經度 scale="{{scale}}" // 縮放級別 controls="{{controls}}" // 地圖控件數組,多個控件存放在數組里 show-location/> // 顯示帶有方向的小圓點 </view>
然后在index.js設置controls(代碼注釋還是挺多的)
//index.js var app = getApp() Page({ data: { scale: 18, latitude: 0, longitude: 0 }, // 頁面加載 onLoad: function(options){ // 1.頁面初始化 options為頁面跳轉所帶來的參數 // 2.調用wx.getLocation系統API,獲取並設置當前位置經緯度 ...已省略 // 3.設置地圖控件的位置及大小,通過設備寬高定位 wx.getSystemInfo({ // 系統API,獲取系統信息,比如設備寬高 success: (res) => { this.setData({ // 定義控件數組,可以在data對象初始化為[],也可以不初始化,取決於是否需要更好的閱讀 controls: [{ id: 1, // 給控件定義唯一id iconPath: '/images/location.png', // 控件圖標 position: { // 控件位置 left: 20, // 單位px top: res.windowHeight - 80, // 根據設備高度設置top值,可以做到在不同設備上效果一致 width: 50, // 控件寬度/px height: 50 // 控件高度/px }, clickable: true // 是否可點擊,默認為true,可點擊 }, { id: 2, iconPath: '/images/use.png', position: { left: res.windowWidth/2 - 45, top: res.windowHeight - 100, width: 90, height: 90 }, clickable: true }, { id: 3, iconPath: '/images/warn.png', position: { left: res.windowWidth - 70, top: res.windowHeight - 80, width: 50, height: 50 }, clickable: true }, { id: 4, iconPath: '/images/marker.png', position: { left: res.windowWidth/2 - 11, top: res.windowHeight/2 - 45, width: 22, height: 45 }, clickable: false }, { id: 5, iconPath: '/images/avatar.png', position: { left: res.windowWidth - 68, top: res.windowHeight - 155, width: 45, height: 45 }, clickable: true }] }) } }); } // 頁面顯示 onShow: function(){ ... }, // 定位函數,移動位置到地圖中心 movetoPosition: function(){ this.mapCtx.moveToLocation(); } })
4.5 為地圖控件綁定事件
現在地圖上總共有四個圖標可點擊(地圖中心的標記控件不需要點擊),我們需要為每個控件綁定不同的事件以實現不同的功能:
1.點擊定位控件,觸發定位當前位置到地圖中心,因為用戶在拖動地圖,有時需要查看當前所在位置。
2.點擊立即用車控件,調用微信內置掃碼功能。然后獲取開鎖密碼。
3.點擊舉報按鈕,前往維修報障頁面。
4.點擊用戶頭像按鈕,前往登錄頁面進行登錄,查看余額,充值等操作
為控件綁定事件,需要在地圖控件進行聲明:bindcontroltap
<!--index.wxml--> <view class="container"> <map id="ofoMap" latitude="{{latitude}}" // 緯度 longitude="{{longitude}}" // 經度 scale="{{scale}}" // 縮放級別 controls="{{controls}}" // 地圖控件數組,多個控件存放在數組里 bindcontroltap="bindcontroltap" // 控件點擊事件 show-location/> // 顯示帶有方向的小圓點 </view>
注意: bindcontroltap事件會響應所有控件的點擊,所以,我們需要根據控件id來區分控件,然后響應不同的事件。
在index.js添加bindcontroltap事件:
//index.js var app = getApp() Page({ data: { scale: 18, latitude: 0, longitude: 0 }, // 頁面加載 onLoad: function(options){ // 1.獲取定時器,用於判斷是否已經在計費 this.timer = options.timer; // 2.調用wx.getLocation系統API,獲取並設置當前位置經緯度 ...已省略 // 3.設置地圖控件的位置及大小,通過設備寬高定位 ...已省略 } // 地圖控件點擊事件 bindcontroltap: function(e){ // 判斷點擊的是哪個控件 e.controlId代表控件的id,在頁面加載時的第3步設置的id switch(e.controlId){ // 點擊定位控件 case 1: this.movetoPosition(); break; // 點擊立即用車,判斷當前是否正在計費,此處只需要知道是調用掃碼,后面會講到this.timer是怎么來的 case 2: if(this.timer === "" || this.timer === undefined){ // 沒有在計費就掃碼 wx.scanCode({ success: (res) => { // 正在獲取密碼通知 wx.showLoading({ title: '正在獲取密碼', mask: true }) // 請求服務器獲取密碼和車號 wx.request({ url: 'https://www.easy-mock.com/mock/59098d007a878d73716e966f/ofodata/password', data: {}, method: 'GET', success: function(res){ // 請求密碼成功隱藏等待框 wx.hideLoading(); // 攜帶密碼和車號跳轉到密碼頁 wx.redirectTo({ url: '../scanresult/index?password=' + res.data.data.password + '&number=' + res.data.data.number, success: function(res){ wx.showToast({ title: '獲取密碼成功', duration: 1000 }) } }) } }) } }) // 當前已經在計費就回退到計費頁 }else{ wx.navigateBack({ delta: 1 }) } break; // 點擊保障控件,跳轉到報障頁 case 3: wx.navigateTo({ url: '../warn/index' }); break; // 點擊頭像控件,跳轉到個人中心 case 5: wx.navigateTo({ url: '../my/index' }); break; default: break; } }, // 頁面顯示 onShow: function(){ ...已省略 }, // 定位函數,移動位置到地圖中心 movetoPosition: function(){ this.mapCtx.moveToLocation(); } })
這里用到的API:
掃碼API: wx.scanCode({})
顯示加載框: wx.showLoading()
隱藏加載框: wx.hideLoading()
顯示提示框: wx.showToast()
隱藏提示框: wx.hideToast()
向服務器發送請求:wx.request({})
關閉當前頁面,跳轉到指定頁面: wx.redirectTo({})
保留當前頁面,跳轉到指定頁面: wx.navigateTo({})
回退到指定頁面: wx.naivgateBack({})
查看詳細用法,查看官方API文檔
tips: 跳轉頁面傳參示例
let num = 1; wx.navigateTo({ url: '../other/index?num=' + num }); // other頁面 onLoad: function(options){ console.log(options.num); // 1 }
多個參數用&分隔,如 'index?num=' + num + '&text=' + 'text'
4.6 在地圖上添加單車標記makers和位置連線,還是在地圖組件里先聲明:
<!--index.wxml--> <view class="container"> <map id="ofoMap" latitude="{{latitude}}" // 緯度 longitude="{{longitude}}" // 經度 scale="{{scale}}" // 縮放級別 controls="{{controls}}" // 地圖控件數組,多個控件存放在數組里 bindcontroltap="bindcontroltap" // 控件點擊事件 polyline="{{polyline}}" // 位置連線 markers="{{markers}}" // 標記數組 bindmarkertap="bindmarkertap" // 標記點擊事件 show-location/> // 顯示帶有方向的小圓點 </view>
然后在index.js里定義:
//index.js var app = getApp() Page({ data: { scale: 18, latitude: 0, longitude: 0 }, // 頁面加載 onLoad: function(options){ // 1.獲取定時器,用於判斷是否已經在計費 this.timer = options.timer; // 2.調用wx.getLocation系統API,獲取並設置當前位置經緯度 ...已省略 // 3.設置地圖控件的位置及大小,通過設備寬高定位 ...已省略 // 4.請求服務器,顯示附近的單車,用marker標記 wx.request({ url: 'https://www.easy-mock.com/mock/59098d007a878d73716e966f/ofodata/biyclePosition', data: {}, method: 'GET', // OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT // header: {}, // 設置請求的 header success: (res) => { this.setData({ markers: res.data.data }) } }) } // 地圖控件點擊事件 bindcontroltap: function(e){ ...已省略 }, // 地圖標記點擊事件,連接用戶位置和點擊的單車位置 bindmarkertap: function(e){ let _markers = this.data.markers; // 拿到標記數組 let markerId = e.markerId; // 獲取點擊的標記id let currMaker = _markers[markerId]; // 通過id,獲取當前點擊的標記 this.setData({ polyline: [{ points: [{ // 連線起點 longitude: this.data.longitude, latitude: this.data.latitude }, { // 連線終點(當前點擊的標記) longitude: currMaker.longitude, latitude: currMaker.latitude }], color:"#FF0000DD", width: 1, dottedLine: true }], scale: 18 }) }, // 頁面顯示 onShow: function(){ ...已省略 }, // 定位函數,移動位置到地圖中心 movetoPosition: function(){ this.mapCtx.moveToLocation(); } })
4.7 用戶拖動地圖事件
我們已經為地圖控件和標記響應了不同的事件,現在如果用戶拖動地圖,我們需要在拖動附件顯示單車,在地圖組件聲明地圖拖動事件:
<!--index.wxml--> <view class="container"> <map id="ofoMap" latitude="{{latitude}}" // 緯度 longitude="{{longitude}}" // 經度 scale="{{scale}}" // 縮放級別 controls="{{controls}}" // 地圖控件數組,多個控件存放在數組里 bindcontroltap="bindcontroltap" // 控件點擊事件 polyline="{{polyline}}" // 位置連線 markers="{{markers}}" // 標記數組 bindmarkertap="bindmarkertap" // 標記點擊事件 bindregionchange="bindregionchange" // 拖動地圖事件 show-location/> // 顯示帶有方向的小圓點 </view>
在index.js里實現這個事件方法:
//index.js var app = getApp() Page({ data: { scale: 18, latitude: 0, longitude: 0 }, // 頁面加載 onLoad: function(options){ // 1.獲取定時器,用於判斷是否已經在計費 this.timer = options.timer; // 2.調用wx.getLocation系統API,獲取並設置當前位置經緯度 ...已省略 // 3.設置地圖控件的位置及大小,通過設備寬高定位 ...已省略 // 4.請求服務器,顯示附近的單車,用marker標記 ...已省略 } // 地圖控件點擊事件 bindcontroltap: function(e){ ...已省略 }, // 地圖視野改變事件 bindregionchange: function(e){ // 拖動地圖,獲取附件單車位置 if(e.type == "begin"){ wx.request({ url: 'https://www.easy-mock.com/mock/59098d007a878d73716e966f/ofodata/biyclePosition', data: {}, method: 'GET', success: (res) => { this.setData({ _markers: res.data.data }) } }) // 停止拖動,顯示單車位置 }else if(e.type == "end"){ this.setData({ markers: this.data._markers }) } }, // 地圖標記點擊事件,連接用戶位置和點擊的單車位置 bindmarkertap: function(e){ ...已省略 }, // 頁面顯示 onShow: function(){ ...已省略 }, // 定位函數,移動位置到地圖中心 movetoPosition: function(){ this.mapCtx.moveToLocation(); } })
至此,首頁地圖已經完成了,接下來要編寫響應的跳轉頁面
5.編寫掃碼之后的獲取密碼頁(scanresult文件夾)
上一節我們為立即用車響應了掃碼事件,掃碼成功后的頁面是醬的:

頁面分析
1.后台需要拿到開鎖密碼,然后顯示在頁面上
2.我們需要一個定時器,規定多長時間用來檢查車輛,這期間可以點擊回首頁去車輛報障鏈接,當然也就取消了本次掃碼。
3.檢查時長完成后,自動跳轉到計費頁面
1.頁面布局
<!--pages/scanresult/index.wxml--> <view class="container"> <view class="password-title"> <text>開鎖密碼</text> </view> <view class="password-content"> <text>{{password}}</text> </view> <view class="tips"> <text>請使用密碼解鎖,{{time}}s后開始計費</text> <view class="tips-action" bindtap="moveToWarn"> 車輛有問題? <text class="tips-href">回首頁去車輛報障</text> </view> </view> </view>
2.頁面樣式
.container{ width: 100%; display: flex; flex-direction: column; align-items: center; justify-content: space-between; background-color: #fff; } .password-title,.tips{ width: 100%; flex: 1; text-align: center; padding: 60rpx 0; } .password-content{ width: 100%; flex: 8; text-align: center; font-size: 240rpx; font-weight: 900; } .tips{ font-size: 32rpx; } .tips .tips-action{ margin-top: 20rpx; } .tips .tips-href{ color: #b9dd08 }
3.頁面數據邏輯
// pages/scanresult/index.js Page({ data:{ time: 9 // 默認計時時長,這里設短一點,用於調試,ofo app是90s }, // 頁面加載 onLoad:function(options){ // 獲取解鎖密碼 this.setData({ password: options.password }) // 設置初始計時秒數 let time = 9; // 開始定時器 this.timer = setInterval(() => { this.setData({ time: -- time }); // 讀完秒后攜帶單車號碼跳轉到計費頁 if(time = 0){ clearInterval(this.timer) wx.redirectTo({ url: '../billing/index?number=' + options.number }) } },1000) }, // 點擊去首頁報障 moveToWarn: function(){ // 清除定時器 clearInterval(this.timer) wx.redirectTo({ url: '../index/index' }) } })
注意:這里的this.timer不會被傳參到pages/index/index.js里的onload函數里,被傳參到首頁的定時器是計費頁的定時器,后面會講到
tips: onload函數參數說明: options的值是掃碼成功后請求服務器獲取的單車編號和開鎖密碼
// pages/index/index.js // 點擊立即用車,判斷當前是否正在計費 case 2: if(this.timer === "" || this.timer === undefined){ // 沒有在計費就掃碼 wx.scanCode({ success: (res) => { // 正在獲取密碼通知 wx.showLoading({ title: '正在獲取密碼', mask: true }) // 請求服務器獲取密碼和車號 wx.request({ url: 'https://www.easy-mock.com/mock/59098d007a878d73716e966f/ofodata/password', data: {}, method: 'GET', success: function(res){ // 請求密碼成功隱藏等待框 wx.hideLoading(); // 攜帶密碼和車號跳轉到密碼頁 wx.redirectTo({ url: '../scanresult/index?password=' + res.data.data.password + '&number=' + res.data.data.number, success: function(res){ wx.showToast({ title: '獲取密碼成功', duration: 1000 }) } }) } }) } }) // 當前已經在計費就回退到計費頁 }else{ wx.navigateBack({ delta: 1 }) } break; // pages/scanresult/index.js onload: function(options){ console.log(options); // { password: "", number: "" } }
6.編寫計費頁(billing文件夾)
上節中我們設置了計時器完成后,跳轉到計費頁,它是醬的:

頁面分析:
1.后台需要拿到單車編號,並顯示在頁面上
2.我們需要一個計時器累加騎行事件用來計費,而且可以顯示最大單位是小時
3.兩個按鈕:結束騎行,回到地圖 。其中,點擊結束騎行,關閉計時器,根據累計時長計費;點擊回到地圖,如果計時器已經關閉了,就關閉計費頁,跳轉到地圖。如果計時器仍然在計時,保留當前頁面,跳轉到地圖。
4.點擊回到地圖會把計時器狀態帶給首頁,首頁做出判斷,判定再次點擊立即用車響應合理邏輯(已經在計費,不能重復掃碼。已經停止計費了,需要重新掃碼)
1.頁面結構
<!--pages/billing/index.wxml--> <view class="container"> <view class="number"> <text>當前單車編號: {{number}}</text> </view> <view class="time"> <view class="time-title"> <text>{{billing}}</text> </view> <view class="time-content"> <text>{{hours}}:{{minuters}}:{{seconds}}</text> </view> </view> <view class="endride"> <button type="warn" disabled="{{disabled}}" bindtap="endRide">結束騎行</button> <button type="primary" bindtap="moveToIndex">回到地圖</button> </view> </view>
2.頁面樣式
.container{ width: 100%; display: flex; flex-direction: column; align-items: center; justify-content: space-between; background-color: #fff; } .number,.endride{ padding: 60rpx 0; flex: 2; width: 100%; text-align: center; } .time{ text-align: center; width: 100%; flex: 6; } .time .time-content{ font-size: 100rpx; } .endride button{ width: 90%; margin-top: 40rpx; }
3.頁面數據邏輯
// pages/billing/index.js Page({ data:{ hours: 0, minuters: 0, seconds: 0, billing: "正在計費" }, // 頁面加載 onLoad:function(options){ // 獲取車牌號,設置定時器 this.setData({ number: options.number, timer: this.timer }) // 初始化計時器 let s = 0; let m = 0; let h = 0; // 計時開始 this.timer = setInterval(() => { this.setData({ seconds: s++ }) if(s == 60){ s = 0; m++; setTimeout(() => { this.setData({ minuters: m }); },1000) if(m == 60){ m = 0; h++ setTimeout(() => { this.setData({ hours: h }); },1000) } }; },1000) }, // 結束騎行,清除定時器 endRide: function(){ clearInterval(this.timer); this.timer = ""; this.setData({ billing: "本次騎行耗時", disabled: true }) }, // 攜帶定時器狀態回到地圖 moveToIndex: function(){ // 如果定時器為空 if(this.timer == ""){ // 關閉計費頁跳到地圖 wx.redirectTo({ url: '../index/index' }) // 保留計費頁跳到地圖 }else{ wx.navigateTo({ url: '../index/index?timer=' + this.timer }) } } })
頁面分析的第4步,主要實現在moveToIndex函數里。結束騎行之后,設置定時器值為空,在點擊回到地圖時判斷計時器的狀態(值是否為空)。如果為空,關閉計費頁,結束本次騎行。如果不為空,攜帶定時器狀態跳轉到首頁,首頁立即用車點擊事件就會對傳過來的參數(計時器狀態)響應合理邏輯。
7.編寫維修報障頁(warn文件夾)
點擊舉報控件,頁面是醬的:


頁面分析:
1.頁面可以勾選故障類型,所以需要用到復選框組件;可以選擇上傳或拍攝圖片,所以要使用wx.chooseImage({})選取圖片API;可以輸入車牌號好備注,所以需要使用input輸入組件。
2.勾選類型,選擇圖片,輸入備注信息完成后,后台需要獲取這些輸入的數據提交到服務器以獲得反饋。
3.必須勾選類型和選擇周圍環境圖片才能提交,否則彈窗提示。可以選擇多張圖片,也可以取消選擇的圖片。
1.頁面結構
<!--pages/warn/index.wxml--> <view class="container"> <view class="choose"> <view class="title">請選擇故障類型</view> <checkbox-group bindchange="checkboxChange" class="choose-grids"> <!-- itemsValue是data對象里定義的數組,item代表數組的每一項,此處語法為循環輸出數組的每一項並渲染到每一個復選框。下面還有類似語法 --> <block wx:for="{{itemsValue}}" wx:key="{{item}}"> <view class="grid"> <checkbox value="{{item.value}}" checked="{{item.checked}}" color="{{item.color}}" />{{item.value}} </view> </block> </checkbox-group> </view> <view class="action"> <view class="title">拍攝單車周圍環境,便於維修師傅找車</view> <view class="action-photo"> <block wx:for="{{picUrls}}" wx:key="{{item}}" wx:index="{{index}}"> <image src="{{item}}"><icon type="cancel" data-index="{{index}}" color="red" size="18" class ="del" bindtap="delPic" /></image> </block> <text class="add" bindtap="bindCamera">{{actionText}}</text> </view> <view class="action-input"> <input bindinput="numberChange" name="number" placeholder="車牌號(車牌損壞不用填)" /> <input bindinput="descChange" name="desc" placeholder="備注" /> </view> <view class="action-submit"> <button class="submit-btn" type="default" loading="{{loading}}" bindtap="formSubmit" style="background-color: {{btnBgc}}">提交</button> </view> </view> </view>
2.頁面樣式
/* pages/wallet/index.wxss */ .choose{ background-color: #fff; } .choose-grids{ display: flex; flex-wrap: wrap; justify-content: space-around; padding: 50rpx; } .choose-grids .grid{ width: 45%; height: 100rpx; margin-top: 36rpx; border-radius: 6rpx; line-height: 100rpx; text-align: center; border: 2rpx solid #b9dd08; } .choose-grids .grid:first-child, .choose-grids .grid:nth-of-type(2){ margin-top: 0; } .action .action-photo{ background-color: #fff; padding: 40rpx 0px 40rpx 50rpx; } .action .action-photo image{ position: relative; display: inline-block; width: 120rpx; height: 120rpx; overflow: visible; margin-left: 25rpx; } .action .action-photo image icon.del{ display: block; position: absolute; top: -20rpx; right: -20rpx; } .action .action-photo text.add{ display: inline-block; width: 120rpx; height: 120rpx; line-height: 120rpx; text-align: center; font-size: 24rpx; color: #ccc; border: 2rpx dotted #ccc; margin-left: 25rpx; vertical-align: top; } .action .action-input{ padding-left: 50rpx; margin-top: 30rpx; background-color: #fff; } .action .action-input input{ width: 90%; padding-top: 40rpx; padding-bottom: 40rpx; } .action .action-input input:first-child{ border-bottom: 2rpx solid #ccc; padding-bottom: 20rpx; } .action .action-input input:last-child{ padding-top: 20rpx; } .action .action-submit{ padding: 40rpx 40rpx; background-color: #f2f2f2; }
3.頁面數據邏輯
// pages/wallet/index.js Page({ data:{ // 故障車周圍環境圖路徑數組 picUrls: [], // 故障車編號和備注 inputValue: { num: 0, desc: "" }, // 故障類型數組 checkboxValue: [], // 選取圖片提示 actionText: "拍照/相冊", // 提交按鈕的背景色,未勾選類型時無顏色 btnBgc: "", // 復選框的value,此處預定義,然后循環渲染到頁面 itemsValue: [ { checked: false, value: "私鎖私用", color: "#b9dd08" }, { checked: false, value: "車牌缺損", color: "#b9dd08" }, { checked: false, value: "輪胎壞了", color: "#b9dd08" }, { checked: false, value: "車鎖壞了", color: "#b9dd08" }, { checked: false, value: "違規亂停", color: "#b9dd08" }, { checked: false, value: "密碼不對", color: "#b9dd08" }, { checked: false, value: "剎車壞了", color: "#b9dd08" }, { checked: false, value: "其他故障", color: "#b9dd08" } ] }, // 頁面加載 onLoad:function(options){ wx.setNavigationBarTitle({ title: '報障維修' }) }, // 勾選故障類型,獲取類型值存入checkboxValue checkboxChange: function(e){ let _values = e.detail.value; if(_values.length == 0){ this.setData({ btnBgc: "" }) }else{ this.setData({ checkboxValue: _values, btnBgc: "#b9dd08" }) } }, // 輸入單車編號,存入inputValue numberChange: function(e){ this.setData({ inputValue: { num: e.detail.value, desc: this.data.inputValue.desc } }) }, // 輸入備注,存入inputValue descChange: function(e){ this.setData({ inputValue: { num: this.data.inputValue.num, desc: e.detail.value } }) }, // 提交到服務器 formSubmit: function(e){ if(this.data.picUrls.length > 0 && this.data.checkboxValue.length> 0){ wx.request({ url: 'https://www.easy-mock.com/mock/59098d007a878d73716e966f/ofodata/msg', data: { // picUrls: this.data.picUrls, // inputValue: this.data.inputValue, // checkboxValue: this.data.checkboxValue }, method: 'get', // POST // header: {}, // 設置請求的 header success: function(res){ wx.showToast({ title: res.data.data.msg, icon: 'success', duration: 2000 }) } }) }else{ wx.showModal({ title: "請填寫反饋信息", content: '看什么看,趕快填反饋信息,削你啊', confirmText: "我我我填", cancelText: "勞資不填", success: (res) => { if(res.confirm){ // 繼續填 }else{ console.log("back") wx.navigateBack({ delta: 1 // 回退前 delta(默認為1) 頁面 }) } } }) } }, // 選擇故障車周圍環境圖 拍照或選擇相冊 bindCamera: function(){ wx.chooseImage({ count: 4, sizeType: ['original', 'compressed'], sourceType: ['album', 'camera'], success: (res) => { let tfps = res.tempFilePaths; let _picUrls = this.data.picUrls; for(let item of tfps){ _picUrls.push(item); this.setData({ picUrls: _picUrls, actionText: "+" }); } } }) }, // 刪除選擇的故障車周圍環境圖 delPic: function(e){ let index = e.target.dataset.index; let _picUrls = this.data.picUrls; _picUrls.splice(index,1); this.setData({ picUrls: _picUrls }) } })
注意: 這里選擇的圖片,路徑為本地路徑,如果要上傳到服務器,需要調用API上傳圖片而不是上傳本地路徑。即不能把picUrls數組上傳到服務器。
8.編寫登錄/未登錄頁(my文件夾)
點擊頭像控件,未登錄,頁面是醬的

點擊頭像控件,已登錄,頁面是醬的

頁面分析
1.個人中心頁有兩種狀態,即未登錄和已登錄,所以要求數據驅動頁面表現形式
2.點擊登錄/退出登錄按鈕需要響應合理邏輯,並改變按鈕樣式
3.只有登錄狀態下才會顯示我的錢包按鈕
1.頁面結構(wx:if 是條件語句)
<!--pages/my/index.wxml--> <view class="container"> <view class="user-info"> <!-- 用戶未登錄就沒有頭像--> <block wx:if="{{userInfo.avatarUrl != ''}}"> <image src="{{userInfo.avatarUrl}}"></image> </block> <text>{{userInfo.nickName}}</text> </view> <!-- 用戶未登錄就沒有錢包按鈕--> <block wx:if="{{userInfo.avatarUrl != ''}}"> <view class="my-wallet tapbar" bindtap="movetoWallet"> <text>我的錢包</text> <text>></text> </view> </block> <button bindtap="bindAction" class="btn-login" hover-class="gray" type="{{bType}}" >{{actionText}}</button> </view>
2.頁面樣式
/* pages/my/index.wxss */ .user-info{ background-color: #fff; padding-top: 60rpx; } .user-info image{ display: block; width: 180rpx; height: 180rpx; border-radius: 50%; margin: 0 auto 40rpx; box-shadow: 0 0 20rpx rgba(0,0,0,.2) } .user-info text{ display: block; text-align: center; padding: 30rpx 0; margin-bottom: 30rpx; } .btn-login{ position: absolute; bottom: 60rpx; width: 90%; left: 50%; margin-left: -45%; } .gray{ background-color: #ccc; }
3.頁面數據邏輯
// pages/my/index.js Page({ data:{ // 用戶信息 userInfo: { avatarUrl: "", nickName: "未登錄" }, bType: "primary", // 按鈕類型 actionText: "登錄", // 按鈕文字提示 lock: false //登錄按鈕狀態,false表示未登錄 }, // 頁面加載 onLoad:function(){ // 設置本頁導航標題 wx.setNavigationBarTitle({ title: '個人中心' }) // 獲取本地數據-用戶信息 wx.getStorage({ key: 'userInfo', // 能獲取到則顯示用戶信息,並保持登錄狀態,不能就什么也不做 success: (res) => { wx.hideLoading(); this.setData({ userInfo: { avatarUrl: res.data.userInfo.avatarUrl, nickName: res.data.userInfo.nickName }, bType: res.data.bType, actionText: res.data.actionText, lock: true }) } }); }, // 登錄或退出登錄按鈕點擊事件 bindAction: function(){ this.data.lock = !this.data.lock // 如果沒有登錄,登錄按鈕操作 if(this.data.lock){ wx.showLoading({ title: "正在登錄" }); wx.login({ success: (res) => { wx.hideLoading(); wx.getUserInfo({ withCredentials: false, success: (res) => { this.setData({ userInfo: { avatarUrl: res.userInfo.avatarUrl, nickName: res.userInfo.nickName }, bType: "warn", actionText: "退出登錄" }); // 存儲用戶信息到本地 wx.setStorage({ key: 'userInfo', data: { userInfo: { avatarUrl: res.userInfo.avatarUrl, nickName: res.userInfo.nickName }, bType: "warn", actionText: "退出登錄" }, success: function(res){ console.log("存儲成功") } }) } }) } }) // 如果已經登錄,退出登錄按鈕操作 }else{ wx.showModal({ title: "確認退出?", content: "退出后將不能使用ofo", success: (res) => { if(res.confirm){ console.log("確定") // 退出登錄則移除本地用戶信息 wx.removeStorageSync('userInfo') this.setData({ userInfo: { avatarUrl: "", nickName: "未登錄" }, bType: "primary", actionText: "登錄" }) }else { console.log("cancel") this.setData({ lock: true }) } } }) } }, // 跳轉至錢包 movetoWallet: function(){ wx.navigateTo({ url: '../wallet/index' }) } })
我們將用戶信息使用wx.setStorage({})和wx.getStorage({})這兩個API來設置和獲取本地存儲,用於模擬維護用戶登錄狀態。真實情況下需要使用session
9.編寫我的錢包頁
假設用戶已登錄,點擊錢包,頁面是醬的:

頁面分析
1.需要獲取錢包余額數據並顯示在頁面上,充值后數據會自動更新
2.其他可點擊按鈕分別顯示對應的模態框,因為微信只允許五個頁面層級,避免過多頁面層級造成用戶迷失。
1.頁面結構
<!--pages/wallet/index.wxml--> <view class="container"> <view class="overage"> <view> <text class="overage-header">我的余額(元)</text> </view> <view> <text class="overage-amount">{{overage}}</text> </view> <view> <text bindtap="overageDesc" class="overage-desc">余額說明</text> </view> </view> <button bindtap="movetoCharge" class="btn-charge">充值</button> <view bindtap="showTicket" class="my-ticket tapbar"> <text>我的用車券</text> <text><text class="c-g">{{ticket}}張</text>></text> </view> <view bindtap="showDeposit" class="my-deposit tapbar"> <text>我的押金</text> <text><text class="c-y">99元,押金退款</text>></text> </view> <view bindtap="showInvcode" class="my-invcode tapbar"> <text>關於ofo</text> <text>></text> </view> </view>
2.頁面樣式
/* pages/wallet/index.wxss */ .overage{ background-color: #fff; padding: 40rpx 0; text-align: center; } .overage-header{ font-size: 24rpx; } .overage-amount{ display: inline-block; padding: 20rpx 0; font-size: 100rpx; font-weight: 700; } .overage-desc{ padding: 10rpx 30rpx; font-size: 24rpx; border-radius: 40rpx; border: 1px solid #666; } .my-deposit{ margin-top: 2rpx; } .my-invcode{ margin-top: 40rpx; } .c-y{ color: #b9dd08; padding-top: -5rpx; padding-right: 10rpx; } .c-g{ padding-top: -5rpx; padding-right: 10rpx; }
3.頁面數據邏輯
// pages/wallet/index.js Page({ data:{ overage: 0, ticket: 0 }, // 頁面加載 onLoad:function(options){ wx.setNavigationBarTitle({ title: '我的錢包' }) }, // 頁面加載完成,更新本地存儲的overage onReady:function(){ wx.getStorage({ key: 'overage', success: (res) => { this.setData({ overage: res.data.overage }) } }) }, // 頁面顯示完成,獲取本地存儲的overage onShow:function(){ wx.getStorage({ key: 'overage', success: (res) => { this.setData({ overage: res.data.overage }) } }) }, // 余額說明 overageDesc: function(){ wx.showModal({ title: "", content: "充值余額0.00元+活動贈送余額0.00元", showCancel: false, confirmText: "我知道了", }) }, // 跳轉到充值頁面 movetoCharge: function(){ // 關閉當前頁面,跳轉到指定頁面,返回時將不會回到當前頁面 wx.redirectTo({ url: '../charge/index' }) }, // 用車券 showTicket: function(){ wx.showModal({ title: "", content: "你沒有用車券了", showCancel: false, confirmText: "好吧", }) }, // 押金退還 showDeposit: function(){ wx.showModal({ title: "", content: "押金會立即退回,退款后,您將不能使用ofo共享單車確認要進行此退款嗎?", cancelText: "繼續使用", cancelColor: "#b9dd08", confirmText: "押金退款", confirmColor: "#ccc", success: (res) => { if(res.confirm){ wx.showToast({ title: "退款成功", icon: "success", duration: 2000 }) } } }) }, // 關於ofo showInvcode: function(){ wx.showModal({ title: "ofo共享單車", content: "微信服務號:ofobike,網址:m.ofo.so", showCancel: false, confirmText: "玩的6" }) } })
我們將金額信息使用wx.setStorage({})和wx.getStorage({})這兩個API來設置和獲取本地存儲,用於模擬充值邏輯。
設置本地存儲API官方文檔
10.編寫充值頁面(charge文件夾)
點擊充值按鈕,頁面是醬的

頁面分析
1.輸入金額,存儲在data對象里,點擊充值后,設置本地金額數據
2.點擊充值按鈕后自動跳轉到錢包頁。
1.頁面結構
<!--pages/charge/index.wxml--> <view class="container"> <view class="title">請輸入充值金額</view> <view class="input-box"> <input bindinput="bindInput" /> </view> <button bindtap="charge" class="btn-charge">充值</button> </view>
2.頁面樣式
/* pages/charge/index.wxss */ .input-box{ background-color: #fff; margin: 0 auto; padding: 20rpx 0; border-radius: 10rpx; width: 90%; } .input-box input{ width: 100%; height: 100%; text-align: center; }
3.頁面數據邏輯
// pages/charge/index.js Page({ data:{ inputValue: 0 }, // 頁面加載 onLoad:function(options){ wx.setNavigationBarTitle({ title: '充值' }) }, // 存儲輸入的充值金額 bindInput: function(res){ this.setData({ inputValue: res.detail.value }) }, // 充值 charge: function(){ // 必須輸入大於0的數字 if(parseInt(this.data.inputValue) <= 0 || isNaN(this.data.inputValue)){ wx.showModal({ title: "警告", content: "咱是不是還得給你錢?!!", showCancel: false, confirmText: "不不不不" }) }else{ wx.redirectTo({ url: '../wallet/index', success: function(res){ wx.showToast({ title: "充值成功", icon: "success", duration: 2000 }) } }) } }, // 頁面銷毀,更新本地金額,(累加) onUnload:function(){ wx.getStorage({ key: 'overage', success: (res) => { wx.setStorage({ key: 'overage', data: { overage: parseInt(this.data.inputValue) + parseInt(res.data.overage) } }) }, // 如果沒有本地金額,則設置本地金額 fail: (res) => { wx.setStorage({ key: 'overage', data: { overage: parseInt(this.data.inputValue) }, }) } }) } })
充值頁面關閉時更新本地金額數據,所以需要在unLoad事件里執行
擴展:使用easy-mock偽造數據
小程序多次請求了服務器“發送/接受”數據,其實這里使用了easy-mock這個網站偽造的數據。
easy-mock可以作為前端開發的偽后端,自己構造數據來測試前端代碼。方便又快捷。官網戳這里。
比如我們這個小程序用到了后端api接口
1.提交報障信息的反饋
2.單車編號和解鎖密碼
3.單車經緯度
結語
到這里,ofo小程序的制作就到了尾聲了。開篇我們創建了多個頁面,然后一個一個頁面從頁面分析,到完成數據邏輯,分別響應着不同的業務邏輯,有的頁面與頁面之間有數據往來,我們就通過跳轉頁面傳參或設置本地存儲來將它們建立起聯系,環環相扣,構建起了整個小程序的基本功能。
通過這個小程序,我們發現文檔提供的API在不知不覺中已經失去了它的神秘感,它們就是不同的工具,為小程序實現業務請求搭建肢體骨架。