时间: 2020-08-23|58次围观|0 条评论

想学一下微信小程序,发现文档这东西,干看真没啥意思。所以打算自己先动手撸一个。摩拜单车有自己的小程序,基本功能都有,方便又小巧,甚是喜爱。于是我就萌生了一个给ofo共享单车撸一个小程序(不知道为啥ofo没有小程序)的想法。Let's do it!

由于本文篇幅过长,影响浏览体验,我对这篇文章做了一下拆分,修正了一些错误。有需要的可以移步浏览
后续: 有位php攻城狮根据此前端项目添加了后台数据支持,详情请转: http://www.jianshu.com/p/8a5687a15648

先上一波效果图:

给ofo共享单车撸一个微信小程序插图
1.首页地图页
给ofo共享单车撸一个微信小程序插图1
2.维修报障页
给ofo共享单车撸一个微信小程序插图2
3.登录页
给ofo共享单车撸一个微信小程序插图3
4.钱包余额页
给ofo共享单车撸一个微信小程序插图4
5.充值页
给ofo共享单车撸一个微信小程序插图5
6.获取了密码页
给ofo共享单车撸一个微信小程序插图6
7.计费页

1.准备工作

微信小程序当然属于腾讯大佬的(给大佬递茶):微信小程序开发者工具,腾讯开放了小程序个人开发平台,只需要一个微信号就可以成为小程序开发者了。

2.小程序页面

打开小程序开发者工具,用微信扫码登录,创建一个默认的小程序。界面是酱的:

给ofo共享单车撸一个微信小程序插图7
小程序开发者工具页面

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下创建路径会自动创建文件夹,贼方便)

给ofo共享单车撸一个微信小程序插图8
页面结构

4.编写地图首页 (index文件夹)

先来回看一下效果图

给ofo共享单车撸一个微信小程序插图
1.首页地图页

页面分析:

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.jsPage({  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.jsvar 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.jsvar 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.jsvar 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.jsvar 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.jsvar 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文件夹)

上一节我们为立即用车响应了扫码事件,扫码成功后的页面是酱的:

给ofo共享单车撸一个微信小程序插图5
获取了密码的页面

页面分析

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.jsPage({  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.jsonload: function(options){  console.log(options); // { password: "", number: "" }}

6.编写计费页(billing文件夹)

上节中我们设置了计时器完成后,跳转到计费页,它是酱的:

给ofo共享单车撸一个微信小程序插图6
计费页

页面分析:

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.jsPage({  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文件夹)

点击举报控件,页面是酱的:

给ofo共享单车撸一个微信小程序插图1
维修报障页1
给ofo共享单车撸一个微信小程序插图9
维修报障页2

页面分析:

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.jsPage({  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文件夹)

点击头像控件,未登录,页面是酱的

给ofo共享单车撸一个微信小程序插图10
未登录页

点击头像控件,已登录,页面是酱的

给ofo共享单车撸一个微信小程序插图2
登录页

页面分析

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.jsPage({  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.编写我的钱包页

假设用户已登录,点击钱包,页面是酱的:

给ofo共享单车撸一个微信小程序插图3
钱包余额页

页面分析

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.jsPage({  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文件夹)

点击充值按钮,页面是酱的

给ofo共享单车撸一个微信小程序插图4
充值页

页面分析

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.jsPage({  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在不知不觉中已经失去了它的神秘感,它们就是不同的工具,为小程序实现业务请求搭建肢体骨架。
源码在我的github主页上,有需要的欢迎fork

文章转载于:https://www.jianshu.com/p/3f9b78c68887

原著是一个有趣的人,若有侵权,请通知删除

本博客所有文章如无特别注明均为原创。
复制或转载请以超链接形式注明转自起风了,原文地址《给ofo共享单车撸一个微信小程序
   

还没有人抢沙发呢~