想學(xué)一下微信小程序,發(fā)現(xiàn)文檔這東西,干看真沒啥意思。所以打算自己先動手?jǐn)]一個。摩拜單車有自己的小程序,基本功能都有,方便又小巧,甚是喜愛。于是我就萌生了一個給ofo共享單車擼一個小程序(不知道為啥ofo沒有小程序)的想法。Let's do it!
由于本文篇幅過長,影響瀏覽體驗,我對這篇文章做了一下拆分,修正了一些錯誤。有需要的可以移步瀏覽 后續(xù): 有位php攻城獅根據(jù)此前端項目添加了后臺數(shù)據(jù)支持,詳情請轉(zhuǎn): http://www.jianshu.com/p/8a5687a15648
先上一波效果圖:
微信小程序當(dāng)然屬于騰訊大佬的(給大佬遞茶):微信小程序開發(fā)者工具,騰訊開放了小程序個人開發(fā)平臺,只需要一個微信號就可以成為小程序開發(fā)者了。
打開小程序開發(fā)者工具,用微信掃碼登錄,創(chuàng)建一個默認(rèn)的小程序。界面是醬的:
pages文件夾下存放著小程序所有的業(yè)務(wù)頁面;
index文件夾就是一個頁面,index.wxml是頁面的結(jié)構(gòu)文件,類似html。
index.wxss是頁面的樣式,其實就是css;index.js是頁面的邏輯,數(shù)據(jù)請求與渲染都是都在這個頁面完成。
logs文件夾存放著小程序開發(fā)日志,目前暫時用不到。
utils.js可以編寫自己的JavaScript插件。
app.js處理全局的一些邏輯,比如定義全局變量存放獲取的用戶信息,這樣每個頁面都可以獲取用戶信息。
app.json 是全局配置文件,比如設(shè)置標(biāo)題欄的背景色等。
app.wxss 存放頁面的公共樣式,如果多個頁面需要用到同一樣式,就可以寫在這里。
項目按鈕顯示預(yù)覽二維碼,用于真機調(diào)試。必須真機調(diào)試測試代碼
上一節(jié)已經(jīng)分析了默認(rèn)的文件結(jié)構(gòu)以及它們的功能,現(xiàn)在我們要創(chuàng)建ofo小程序所需要的頁面。
{
"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", // 標(biāo)題欄背景色
"navigationBarTitleText": "ofo 共享單車", // 標(biāo)題欄文字
"navigationBarTextStyle":"black" // 標(biāo)題欄文字樣式
}
}
/**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下創(chuàng)建路徑會自動創(chuàng)建文件夾,賊方便)
先來回看一下效果圖
頁面分析:
1.整頁顯示地圖,寬高占手機窗口的100%;
2.地圖之上有五個按鈕圖標(biāo)和多個黃色ofo標(biāo)記:定位按鈕,立即用車按鈕,舉報按鈕,黃色頭像按鈕和位于地圖中心的標(biāo)記。
<!--index.wxml-->
<view class="container">
<map id="ofoMap"
latitude="{{latitude}}" // 緯度
longitude="{{longitude}}" // 經(jīng)度
scale="{{scale}}" // 縮放級別
show-location/> // 顯示帶有方向的小圓點
</view>
{{...}} 里面是數(shù)據(jù)變量,由js里的data對象定義。
//index.js
Page({
data: {
scale: 18, // 縮放級別,默認(rèn)18,數(shù)值在0~18之間
latitude: 0, // 給個默認(rèn)值
longitude: 0 // 給個默認(rèn)值
},
onLoad:function(options){
// 頁面初始化 options為頁面跳轉(zhuǎn)所帶來的參數(shù)
},
onReady:function(){
// 頁面渲染完成
},
onShow:function(){
// 頁面顯示
},
onHide:function(){
// 頁面隱藏
},
onUnload:function(){
// 頁面關(guān)閉
}
這樣我們的地圖就默認(rèn)中心位置為經(jīng)度 0,緯度0。當(dāng)然可能在開發(fā)者工具里顯示不出來,莫慌!這不是我們想要的,我們想要的是我們自己的位置,所以得先獲取我們當(dāng)前所在位置的經(jīng)緯度,在index.js里的onLoad方法里添加如下代碼:
onLoad: function(options){
// 頁面初始化 options為頁面跳轉(zhuǎn)所帶來的參數(shù)
// 調(diào)用wx.getLocation系統(tǒng)API,獲取并設(shè)置當(dāng)前位置經(jīng)緯度
wx.getLocation({
type: "gcj02", // 坐標(biāo)系類型
// 獲取經(jīng)緯度成功回調(diào)
success: (res) => { // es6 箭頭函數(shù),可以解綁當(dāng)前作用域的this指向,使得下面的this可以綁定到Page對象
this.setData({ // 為data對象里定義的經(jīng)緯度默認(rèn)值設(shè)置成獲取到的真實經(jīng)緯度,這樣就可以在地圖上顯示我們的真實位置
longitude: res.longitude,
latitude: res.latitude
})
}
});
}
res是一個數(shù)據(jù)對象,它是由調(diào)用的對應(yīng)API傳過來的,如果你想知道res的具體值,可以在成功回調(diào)函數(shù)里打印,然后在控制臺輸出:console.log(res)。在調(diào)用一個陌生API的時候可以用這種方法查看返回的對象數(shù)據(jù),對處理邏輯很有幫助。
我們在地圖上顯示了我們的真實位置,但沒有移動到中心位置,wx.moveToLocation()函數(shù)可以把當(dāng)前位置移動到地圖中心。修改index.js:
//index.js
var app = getApp()
Page({
data: {
scale: 18,
latitude: 0,
longitude: 0
},
// 頁面加載
onLoad: function(options){
// 1.頁面初始化 options為頁面跳轉(zhuǎn)所帶來的參數(shù)
// 2.調(diào)用wx.getLocation系統(tǒng)API,獲取并設(shè)置當(dāng)前位置經(jīng)緯度
wx.getLocation({
type: "gcj02", // 坐標(biāo)系類型
// 獲取經(jīng)緯度成功回調(diào)
success: (res) => { // es6 箭頭函數(shù),可以解綁當(dāng)前作用域的this指向,使得下面的this可以綁定到Page對象
this.setData({ // 為data對象里定義的經(jīng)緯度默認(rèn)值設(shè)置成獲取到的真實經(jīng)緯度,這樣就可以在地圖上顯示我們的真實位置
longitude: res.longitude,
latitude: res.latitude
})
}
});
}
// 頁面顯示
onShow: function(){
// 1.創(chuàng)建地圖上下文,移動當(dāng)前位置到地圖中心
this.mapCtx = wx.createMapContext("ofoMap"); // 地圖組件的id
this.movetoPosition()
},
// 定位函數(shù),移動位置到地圖中心
movetoPosition: function(){
this.mapCtx.moveToLocation();
}
})
這樣,頁面一顯示就在地圖中心顯示當(dāng)前位置。
/**index.wxss**/
.container{
position: relative;
width: 100%; // 寬度占滿設(shè)備
height: 100vh; // 高度占滿設(shè)備
}
#ofoMap{
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
z-index: 1;
}
保存刷新,整個屏幕就都顯示地圖了>_<
其實這里的按鈕不是真正的按鈕,它們屬于地圖上的控件屬性,并且不隨著地圖移動。這里有一個坑:
地圖組件屬于微信原生組件,層級最高,任何元素都不能在地圖之上顯示,即無論設(shè)置多大z-index都無法顯示。所以,要想在地圖上添加按鈕來滿足需求,就要用到地圖控件屬性。更多地圖控件屬性說明看這里
要添加地圖控件,先在地圖組件里聲明controls="...":
<!--index.wxml-->
<view class="container">
<map id="ofoMap"
latitude="{{latitude}}" // 緯度
longitude="{{longitude}}" // 經(jīng)度
scale="{{scale}}" // 縮放級別
controls="{{controls}}" // 地圖控件數(shù)組,多個控件存放在數(shù)組里
show-location/> // 顯示帶有方向的小圓點
</view>
然后在index.js設(shè)置controls(代碼注釋還是挺多的)
//index.js
var app = getApp()
Page({
data: {
scale: 18,
latitude: 0,
longitude: 0
},
// 頁面加載
onLoad: function(options){
// 1.頁面初始化 options為頁面跳轉(zhuǎn)所帶來的參數(shù)
// 2.調(diào)用wx.getLocation系統(tǒng)API,獲取并設(shè)置當(dāng)前位置經(jīng)緯度
...已省略
// 3.設(shè)置地圖控件的位置及大小,通過設(shè)備寬高定位
wx.getSystemInfo({ // 系統(tǒng)API,獲取系統(tǒng)信息,比如設(shè)備寬高
success: (res) => {
this.setData({
// 定義控件數(shù)組,可以在data對象初始化為[],也可以不初始化,取決于是否需要更好的閱讀
controls: [{
id: 1, // 給控件定義唯一id
iconPath: '/images/location.png', // 控件圖標(biāo)
position: { // 控件位置
left: 20, // 單位px
top: res.windowHeight - 80, // 根據(jù)設(shè)備高度設(shè)置top值,可以做到在不同設(shè)備上效果一致
width: 50, // 控件寬度/px
height: 50 // 控件高度/px
},
clickable: true // 是否可點擊,默認(rèn)為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(){
...
},
// 定位函數(shù),移動位置到地圖中心
movetoPosition: function(){
this.mapCtx.moveToLocation();
}
})
現(xiàn)在地圖上總共有四個圖標(biāo)可點擊(地圖中心的標(biāo)記控件不需要點擊),我們需要為每個控件綁定不同的事件以實現(xiàn)不同的功能:
1.點擊定位控件,觸發(fā)定位當(dāng)前位置到地圖中心,因為用戶在拖動地圖,有時需要查看當(dāng)前所在位置。
2.點擊立即用車控件,調(diào)用微信內(nèi)置掃碼功能。然后獲取開鎖密碼。
3.點擊舉報按鈕,前往維修報障頁面。
4.點擊用戶頭像按鈕,前往登錄頁面進行登錄,查看余額,充值等操作
為控件綁定事件,需要在地圖控件進行聲明:bindcontroltap
<!--index.wxml-->
<view class="container">
<map id="ofoMap"
latitude="{{latitude}}" // 緯度
longitude="{{longitude}}" // 經(jīng)度
scale="{{scale}}" // 縮放級別
controls="{{controls}}" // 地圖控件數(shù)組,多個控件存放在數(shù)組里
bindcontroltap="bindcontroltap" // 控件點擊事件
show-location/> // 顯示帶有方向的小圓點
</view>
注意: bindcontroltap事件會響應(yīng)所有控件的點擊,所以,我們需要根據(jù)控件id來區(qū)分控件,然后響應(yīng)不同的事件。
在index.js添加bindcontroltap事件:
//index.js
var app = getApp()
Page({
data: {
scale: 18,
latitude: 0,
longitude: 0
},
// 頁面加載
onLoad: function(options){
// 1.獲取定時器,用于判斷是否已經(jīng)在計費
this.timer = options.timer;
// 2.調(diào)用wx.getLocation系統(tǒng)API,獲取并設(shè)置當(dāng)前位置經(jīng)緯度
...已省略
// 3.設(shè)置地圖控件的位置及大小,通過設(shè)備寬高定位
...已省略
}
// 地圖控件點擊事件
bindcontroltap: function(e){
// 判斷點擊的是哪個控件 e.controlId代表控件的id,在頁面加載時的第3步設(shè)置的id
switch(e.controlId){
// 點擊定位控件
case 1: this.movetoPosition();
break;
// 點擊立即用車,判斷當(dāng)前是否正在計費,此處只需要知道是調(diào)用掃碼,后面會講到this.timer是怎么來的
case 2: if(this.timer === "" || this.timer === undefined){
// 沒有在計費就掃碼
wx.scanCode({
success: (res) => {
// 正在獲取密碼通知
wx.showLoading({
title: '正在獲取密碼',
mask: true
})
// 請求服務(wù)器獲取密碼和車號
wx.request({
url: 'https://www.easy-mock.com/mock/59098d007a878d73716e966f/ofodata/password',
data: {},
method: 'GET',
success: function(res){
// 請求密碼成功隱藏等待框
wx.hideLoading();
// 攜帶密碼和車號跳轉(zhuǎn)到密碼頁
wx.redirectTo({
url: '../scanresult/index?password=' + res.data.data.password + '&number=' + res.data.data.number,
success: function(res){
wx.showToast({
title: '獲取密碼成功',
duration: 1000
})
}
})
}
})
}
})
// 當(dāng)前已經(jīng)在計費就回退到計費頁
}else{
wx.navigateBack({
delta: 1
})
}
break;
// 點擊保障控件,跳轉(zhuǎn)到報障頁
case 3: wx.navigateTo({
url: '../warn/index'
});
break;
// 點擊頭像控件,跳轉(zhuǎn)到個人中心
case 5: wx.navigateTo({
url: '../my/index'
});
break;
default: break;
}
},
// 頁面顯示
onShow: function(){
...已省略
},
// 定位函數(shù),移動位置到地圖中心
movetoPosition: function(){
this.mapCtx.moveToLocation();
}
})
這里用到的API:
掃碼API: wx.scanCode({})
顯示加載框: wx.showLoading()
隱藏加載框: wx.hideLoading()
顯示提示框: wx.showToast()
隱藏提示框: wx.hideToast()
向服務(wù)器發(fā)送請求:wx.request({})
關(guān)閉當(dāng)前頁面,跳轉(zhuǎn)到指定頁面: wx.redirectTo({})
保留當(dāng)前頁面,跳轉(zhuǎn)到指定頁面: wx.navigateTo({})
回退到指定頁面: wx.naivgateBack({})
查看詳細(xì)用法,查看官方API文檔
tips: 跳轉(zhuǎn)頁面?zhèn)鲄⑹纠?/p>
let num = 1;
wx.navigateTo({
url: '../other/index?num=' + num
});
// other頁面
onLoad: function(options){
console.log(options.num); // 1
}
多個參數(shù)用&分隔,如 'index?num=' + num + '&text=' + 'text'
<!--index.wxml-->
<view class="container">
<map id="ofoMap"
latitude="{{latitude}}" // 緯度
longitude="{{longitude}}" // 經(jīng)度
scale="{{scale}}" // 縮放級別
controls="{{controls}}" // 地圖控件數(shù)組,多個控件存放在數(shù)組里
bindcontroltap="bindcontroltap" // 控件點擊事件
polyline="{{polyline}}" // 位置連線
markers="{{markers}}" // 標(biāo)記數(shù)組
bindmarkertap="bindmarkertap" // 標(biāo)記點擊事件
show-location/> // 顯示帶有方向的小圓點
</view>
然后在index.js里定義:
//index.js
var app = getApp()
Page({
data: {
scale: 18,
latitude: 0,
longitude: 0
},
// 頁面加載
onLoad: function(options){
// 1.獲取定時器,用于判斷是否已經(jīng)在計費
this.timer = options.timer;
// 2.調(diào)用wx.getLocation系統(tǒng)API,獲取并設(shè)置當(dāng)前位置經(jīng)緯度
...已省略
// 3.設(shè)置地圖控件的位置及大小,通過設(shè)備寬高定位
...已省略
// 4.請求服務(wù)器,顯示附近的單車,用marker標(biāo)記
wx.request({
url: 'https://www.easy-mock.com/mock/59098d007a878d73716e966f/ofodata/biyclePosition',
data: {},
method: 'GET', // OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT
// header: {}, // 設(shè)置請求的 header
success: (res) => {
this.setData({
markers: res.data.data
})
}
})
}
// 地圖控件點擊事件
bindcontroltap: function(e){
...已省略
},
// 地圖標(biāo)記點擊事件,連接用戶位置和點擊的單車位置
bindmarkertap: function(e){
let _markers = this.data.markers; // 拿到標(biāo)記數(shù)組
let markerId = e.markerId; // 獲取點擊的標(biāo)記id
let currMaker = _markers[markerId]; // 通過id,獲取當(dāng)前點擊的標(biāo)記
this.setData({
polyline: [{
points: [{ // 連線起點
longitude: this.data.longitude,
latitude: this.data.latitude
}, { // 連線終點(當(dāng)前點擊的標(biāo)記)
longitude: currMaker.longitude,
latitude: currMaker.latitude
}],
color:"#FF0000DD",
width: 1,
dottedLine: true
}],
scale: 18
})
},
// 頁面顯示
onShow: function(){
...已省略
},
// 定位函數(shù),移動位置到地圖中心
movetoPosition: function(){
this.mapCtx.moveToLocation();
}
})
我們已經(jīng)為地圖控件和標(biāo)記響應(yīng)了不同的事件,現(xiàn)在如果用戶拖動地圖,我們需要在拖動附件顯示單車,在地圖組件聲明地圖拖動事件:
<!--index.wxml-->
<view class="container">
<map id="ofoMap"
latitude="{{latitude}}" // 緯度
longitude="{{longitude}}" // 經(jīng)度
scale="{{scale}}" // 縮放級別
controls="{{controls}}" // 地圖控件數(shù)組,多個控件存放在數(shù)組里
bindcontroltap="bindcontroltap" // 控件點擊事件
polyline="{{polyline}}" // 位置連線
markers="{{markers}}" // 標(biāo)記數(shù)組
bindmarkertap="bindmarkertap" // 標(biāo)記點擊事件
bindregionchange="bindregionchange" // 拖動地圖事件
show-location/> // 顯示帶有方向的小圓點
</view>
在index.js里實現(xiàn)這個事件方法:
//index.js
var app = getApp()
Page({
data: {
scale: 18,
latitude: 0,
longitude: 0
},
// 頁面加載
onLoad: function(options){
// 1.獲取定時器,用于判斷是否已經(jīng)在計費
this.timer = options.timer;
// 2.調(diào)用wx.getLocation系統(tǒng)API,獲取并設(shè)置當(dāng)前位置經(jīng)緯度
...已省略
// 3.設(shè)置地圖控件的位置及大小,通過設(shè)備寬高定位
...已省略
// 4.請求服務(wù)器,顯示附近的單車,用marker標(biāo)記
...已省略
}
// 地圖控件點擊事件
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
})
}
},
// 地圖標(biāo)記點擊事件,連接用戶位置和點擊的單車位置
bindmarkertap: function(e){
...已省略
},
// 頁面顯示
onShow: function(){
...已省略
},
// 定位函數(shù),移動位置到地圖中心
movetoPosition: function(){
this.mapCtx.moveToLocation();
}
})
至此,首頁地圖已經(jīng)完成了,接下來要編寫響應(yīng)的跳轉(zhuǎn)頁面
上一節(jié)我們?yōu)榱⒓从密図憫?yīng)了掃碼事件,掃碼成功后的頁面是醬的:
頁面分析
1.后臺需要拿到開鎖密碼,然后顯示在頁面上
2.我們需要一個定時器,規(guī)定多長時間用來檢查車輛,這期間可以點擊回首頁去車輛報障鏈接,當(dāng)然也就取消了本次掃碼。
3.檢查時長完成后,自動跳轉(zhuǎn)到計費頁面
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.頁面數(shù)據(jù)邏輯
// pages/scanresult/index.js
Page({
data:{
time: 9 // 默認(rèn)計時時長,這里設(shè)短一點,用于調(diào)試,ofo app是90s
},
// 頁面加載
onLoad:function(options){
// 獲取解鎖密碼
this.setData({
password: options.password
})
// 設(shè)置初始計時秒數(shù)
let time = 9;
// 開始定時器
this.timer = setInterval(() => {
this.setData({
time: -- time
});
// 讀完秒后攜帶單車號碼跳轉(zhuǎn)到計費頁
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函數(shù)里,被傳參到首頁的定時器是計費頁的定時器,后面會講到
tips: onload函數(shù)參數(shù)說明: options的值是掃碼成功后請求服務(wù)器獲取的單車編號和開鎖密碼
// pages/index/index.js
// 點擊立即用車,判斷當(dāng)前是否正在計費
case 2: if(this.timer === "" || this.timer === undefined){
// 沒有在計費就掃碼
wx.scanCode({
success: (res) => {
// 正在獲取密碼通知
wx.showLoading({
title: '正在獲取密碼',
mask: true
})
// 請求服務(wù)器獲取密碼和車號
wx.request({
url: 'https://www.easy-mock.com/mock/59098d007a878d73716e966f/ofodata/password',
data: {},
method: 'GET',
success: function(res){
// 請求密碼成功隱藏等待框
wx.hideLoading();
// 攜帶密碼和車號跳轉(zhuǎn)到密碼頁
wx.redirectTo({
url: '../scanresult/index?password=' + res.data.data.password + '&number=' + res.data.data.number,
success: function(res){
wx.showToast({
title: '獲取密碼成功',
duration: 1000
})
}
})
}
})
}
})
// 當(dāng)前已經(jīng)在計費就回退到計費頁
}else{
wx.navigateBack({
delta: 1
})
}
break;
// pages/scanresult/index.js
onload: function(options){
console.log(options); // { password: "", number: "" }
}
上節(jié)中我們設(shè)置了計時器完成后,跳轉(zhuǎn)到計費頁,它是醬的:
頁面分析:
1.后臺需要拿到單車編號,并顯示在頁面上
2.我們需要一個計時器累加騎行事件用來計費,而且可以顯示最大單位是小時
3.兩個按鈕:結(jié)束騎行,回到地圖 。其中,點擊結(jié)束騎行,關(guān)閉計時器,根據(jù)累計時長計費;點擊回到地圖,如果計時器已經(jīng)關(guān)閉了,就關(guān)閉計費頁,跳轉(zhuǎn)到地圖。如果計時器仍然在計時,保留當(dāng)前頁面,跳轉(zhuǎn)到地圖。
4.點擊回到地圖會把計時器狀態(tài)帶給首頁,首頁做出判斷,判定再次點擊立即用車響應(yīng)合理邏輯(已經(jīng)在計費,不能重復(fù)掃碼。已經(jīng)停止計費了,需要重新掃碼)
1.頁面結(jié)構(gòu)
<!--pages/billing/index.wxml-->
<view class="container">
<view class="number">
<text>當(dāng)前單車編號: {{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">結(jié)束騎行</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.頁面數(shù)據(jù)邏輯
// pages/billing/index.js
Page({
data:{
hours: 0,
minuters: 0,
seconds: 0,
billing: "正在計費"
},
// 頁面加載
onLoad:function(options){
// 獲取車牌號,設(shè)置定時器
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)
},
// 結(jié)束騎行,清除定時器
endRide: function(){
clearInterval(this.timer);
this.timer = "";
this.setData({
billing: "本次騎行耗時",
disabled: true
})
},
// 攜帶定時器狀態(tài)回到地圖
moveToIndex: function(){
// 如果定時器為空
if(this.timer == ""){
// 關(guān)閉計費頁跳到地圖
wx.redirectTo({
url: '../index/index'
})
// 保留計費頁跳到地圖
}else{
wx.navigateTo({
url: '../index/index?timer=' + this.timer
})
}
}
})
頁面分析的第4步,主要實現(xiàn)在moveToIndex函數(shù)里。結(jié)束騎行之后,設(shè)置定時器值為空,在點擊回到地圖時判斷計時器的狀態(tài)(值是否為空)。如果為空,關(guān)閉計費頁,結(jié)束本次騎行。如果不為空,攜帶定時器狀態(tài)跳轉(zhuǎn)到首頁,首頁立即用車點擊事件就會對傳過來的參數(shù)(計時器狀態(tài))響應(yīng)合理邏輯。
點擊舉報控件,頁面是醬的:
頁面分析:
1.頁面可以勾選故障類型,所以需要用到復(fù)選框組件;可以選擇上傳或拍攝圖片,所以要使用wx.chooseImage({})選取圖片API;可以輸入車牌號好備注,所以需要使用input輸入組件。
2.勾選類型,選擇圖片,輸入備注信息完成后,后臺需要獲取這些輸入的數(shù)據(jù)提交到服務(wù)器以獲得反饋。
3.必須勾選類型和選擇周圍環(huán)境圖片才能提交,否則彈窗提示??梢赃x擇多張圖片,也可以取消選擇的圖片。
1.頁面結(jié)構(gòu)
<!--pages/warn/index.wxml-->
<view class="container">
<view class="choose">
<view class="title">請選擇故障類型</view>
<checkbox-group bindchange="checkboxChange" class="choose-grids">
<!-- itemsValue是data對象里定義的數(shù)組,item代表數(shù)組的每一項,此處語法為循環(huán)輸出數(shù)組的每一項并渲染到每一個復(fù)選框。下面還有類似語法 -->
<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">拍攝單車周圍環(huán)境,便于維修師傅找車</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.頁面數(shù)據(jù)邏輯
// pages/wallet/index.js
Page({
data:{
// 故障車周圍環(huán)境圖路徑數(shù)組
picUrls: [],
// 故障車編號和備注
inputValue: {
num: 0,
desc: ""
},
// 故障類型數(shù)組
checkboxValue: [],
// 選取圖片提示
actionText: "拍照/相冊",
// 提交按鈕的背景色,未勾選類型時無顏色
btnBgc: "",
// 復(fù)選框的value,此處預(yù)定義,然后循環(huán)渲染到頁面
itemsValue: [
{
checked: false,
value: "私鎖私用",
color: "#b9dd08"
},
{
checked: false,
value: "車牌缺損",
color: "#b9dd08"
},
{
checked: false,
value: "輪胎壞了",
color: "#b9dd08"
},
{
checked: false,
value: "車鎖壞了",
color: "#b9dd08"
},
{
checked: false,
value: "違規(guī)亂停",
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
}
})
},
// 提交到服務(wù)器
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: {}, // 設(shè)置請求的 header
success: function(res){
wx.showToast({
title: res.data.data.msg,
icon: 'success',
duration: 2000
})
}
})
}else{
wx.showModal({
title: "請?zhí)顚懛答佇畔?quot;,
content: '看什么看,趕快填反饋信息,削你啊',
confirmText: "我我我填",
cancelText: "勞資不填",
success: (res) => {
if(res.confirm){
// 繼續(xù)填
}else{
console.log("back")
wx.navigateBack({
delta: 1 // 回退前 delta(默認(rèn)為1) 頁面
})
}
}
})
}
},
// 選擇故障車周圍環(huán)境圖 拍照或選擇相冊
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: "+"
});
}
}
})
},
// 刪除選擇的故障車周圍環(huán)境圖
delPic: function(e){
let index = e.target.dataset.index;
let _picUrls = this.data.picUrls;
_picUrls.splice(index,1);
this.setData({
picUrls: _picUrls
})
}
})
注意: 這里選擇的圖片,路徑為本地路徑,如果要上傳到服務(wù)器,需要調(diào)用API上傳圖片而不是上傳本地路徑。即不能把picUrls數(shù)組上傳到服務(wù)器。
點擊頭像控件,未登錄,頁面是醬的
點擊頭像控件,已登錄,頁面是醬的
頁面分析
1.個人中心頁有兩種狀態(tài),即未登錄和已登錄,所以要求數(shù)據(jù)驅(qū)動頁面表現(xiàn)形式
2.點擊登錄/退出登錄按鈕需要響應(yīng)合理邏輯,并改變按鈕樣式
3.只有登錄狀態(tài)下才會顯示我的錢包按鈕
1.頁面結(jié)構(gòu)(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.頁面數(shù)據(jù)邏輯
// pages/my/index.js
Page({
data:{
// 用戶信息
userInfo: {
avatarUrl: "",
nickName: "未登錄"
},
bType: "primary", // 按鈕類型
actionText: "登錄", // 按鈕文字提示
lock: false //登錄按鈕狀態(tài),false表示未登錄
},
// 頁面加載
onLoad:function(){
// 設(shè)置本頁導(dǎo)航標(biāo)題
wx.setNavigationBarTitle({
title: '個人中心'
})
// 獲取本地數(shù)據(jù)-用戶信息
wx.getStorage({
key: 'userInfo',
// 能獲取到則顯示用戶信息,并保持登錄狀態(tài),不能就什么也不做
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("存儲成功")
}
})
}
})
}
})
// 如果已經(jīng)登錄,退出登錄按鈕操作
}else{
wx.showModal({
title: "確認(rèn)退出?",
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
})
}
}
})
}
},
// 跳轉(zhuǎn)至錢包
movetoWallet: function(){
wx.navigateTo({
url: '../wallet/index'
})
}
})
我們將用戶信息使用wx.setStorage({})和wx.getStorage({})這兩個API來設(shè)置和獲取本地存儲,用于模擬維護用戶登錄狀態(tài)。真實情況下需要使用session
假設(shè)用戶已登錄,點擊錢包,頁面是醬的:
頁面分析
1.需要獲取錢包余額數(shù)據(jù)并顯示在頁面上,充值后數(shù)據(jù)會自動更新
2.其他可點擊按鈕分別顯示對應(yīng)的模態(tài)框,因為微信只允許五個頁面層級,避免過多頁面層級造成用戶迷失。
1.頁面結(jié)構(gòu)
<!--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>關(guān)于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.頁面數(shù)據(jù)邏輯
// 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: "我知道了",
})
},
// 跳轉(zhuǎn)到充值頁面
movetoCharge: function(){
// 關(guān)閉當(dāng)前頁面,跳轉(zhuǎn)到指定頁面,返回時將不會回到當(dāng)前頁面
wx.redirectTo({
url: '../charge/index'
})
},
// 用車券
showTicket: function(){
wx.showModal({
title: "",
content: "你沒有用車券了",
showCancel: false,
confirmText: "好吧",
})
},
// 押金退還
showDeposit: function(){
wx.showModal({
title: "",
content: "押金會立即退回,退款后,您將不能使用ofo共享單車確認(rèn)要進行此退款嗎?",
cancelText: "繼續(xù)使用",
cancelColor: "#b9dd08",
confirmText: "押金退款",
confirmColor: "#ccc",
success: (res) => {
if(res.confirm){
wx.showToast({
title: "退款成功",
icon: "success",
duration: 2000
})
}
}
})
},
// 關(guān)于ofo
showInvcode: function(){
wx.showModal({
title: "ofo共享單車",
content: "微信服務(wù)號:ofobike,網(wǎng)址:m.ofo.so",
showCancel: false,
confirmText: "玩的6"
})
}
})
我們將金額信息使用wx.setStorage({})和wx.getStorage({})這兩個API來設(shè)置和獲取本地存儲,用于模擬充值邏輯。
設(shè)置本地存儲API官方文檔
點擊充值按鈕,頁面是醬的
頁面分析
1.輸入金額,存儲在data對象里,點擊充值后,設(shè)置本地金額數(shù)據(jù)
2.點擊充值按鈕后自動跳轉(zhuǎn)到錢包頁。
1.頁面結(jié)構(gòu)
<!--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.頁面數(shù)據(jù)邏輯
// 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的數(shù)字
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)
}
})
},
// 如果沒有本地金額,則設(shè)置本地金額
fail: (res) => {
wx.setStorage({
key: 'overage',
data: {
overage: parseInt(this.data.inputValue)
},
})
}
})
}
})
充值頁面關(guān)閉時更新本地金額數(shù)據(jù),所以需要在unLoad事件里執(zhí)行
小程序多次請求了服務(wù)器“發(fā)送/接受”數(shù)據(jù),其實這里使用了easy-mock這個網(wǎng)站偽造的數(shù)據(jù)。
easy-mock可以作為前端開發(fā)的偽后端,自己構(gòu)造數(shù)據(jù)來測試前端代碼。方便又快捷。官網(wǎng)戳這里。
比如我們這個小程序用到了后端api接口
1.提交報障信息的反饋
2.單車編號和解鎖密碼
3.單車經(jīng)緯度
到這里,ofo小程序的制作就到了尾聲了。開篇我們創(chuàng)建了多個頁面,然后一個一個頁面從頁面分析,到完成數(shù)據(jù)邏輯,分別響應(yīng)著不同的業(yè)務(wù)邏輯,有的頁面與頁面之間有數(shù)據(jù)往來,我們就通過跳轉(zhuǎn)頁面?zhèn)鲄⒒蛟O(shè)置本地存儲來將它們建立起聯(lián)系,環(huán)環(huán)相扣,構(gòu)建起了整個小程序的基本功能。
通過這個小程序,我們發(fā)現(xiàn)文檔提供的API在不知不覺中已經(jīng)失去了它的神秘感,它們就是不同的工具,為小程序?qū)崿F(xiàn)業(yè)務(wù)請求搭建肢體骨架。
源碼在我的github主頁上,有需要的歡迎fork
工作日 8:30-12:00 14:30-18:00
周六及部分節(jié)假日提供值班服務(wù)