『改良版青桔单车』微信小程序

初衷

据说2018将是小程序爆发的一年,也许是随波追流吧,做为一名前端学习者,我也开始玩起了小程序,从原先在掘金看别人写的小程序项目,到现在不知不觉就本身倒腾了一个多月,也想作点东西练练手,因而有了这个小项目。css

项目介绍

今年,青桔单车登陆了我所在的城市,外形简约时尚,反正是特别喜欢。恰好我又在研究小程序,因而就想仿写一个青桔单车小程序的前端实现,大厂的项目仍是很牛逼的,有很多提高体验的细节,值得学习,我也在其中注入了一些本身的想法,整体来讲,我认为共享单车类小程序,其实体验还能够作得更好。html

在这个项目过程当中,踩了挺多坑的,很值得记录下来。因而行文,将我实现过程的种种,做为分享,但愿能帮助到一些同窗。前端

模拟单车刷新 git

重置定位
判断距离最近单车
点击单车自动路径规划
输入框手机号自动分割
模拟登陆
模拟扫码骑行
结束骑行并支付

2018.6.13更新 修复了手机号显示为null的Buggithub

Github源码地址:改良版青🍊单车 有须要的欢迎fork ,若是喜欢,请给个Starjson


具体内容

接下来是具体内容介绍小程序

目录结构

●
┣━ config # 存放伪造数据的mock
┣━ images # 图片素材
┣━ libs   # 引入的高德地图SDK
┣━ pages  ● 页面
          ┣━ init                   //主界面
          ┣━ login                  //登陆界面
          ┣━ userCenter             //我的中心
          ┣━ messageCenter          //消息中心
          ┣━ unlock                 //解锁
          ┣━ charge                 //计价
          ┣━ end                    //结束行程
          ┣━ repair                 //单车报修
          ┗━ record                 //行程记录
┣━ utils
┣━ app.js
┣━ app.wxss
┣━ app.json
┗━ project.config.json
复制代码

构建和谐一致的地图主界面

主界面很简洁,上部分为一个map组件,下方为一个扫码解锁按钮,map组件中有三个小控件,一条横幅。看起来挺简单吧,可是想要作出这样的界面,得先稍思考一下。

map组件堪称小程序最复杂的一个组件,它是由客户端建立的原生组件,而且它的层级是最高的,不能经过 z-index 控制层级。这句话意味着,普通组件,没法覆盖在它的上方。不过cover-viewcover-image 组件例外,接下来就要用到。后端

更多细节能够查看map组件的官方文档map组件微信小程序

使用弹性布局,安排上方地图,底部扫码解锁按钮

实测发现,青桔单车的底部按钮并不是button组件,而是使用view组件 不过是添加了一些样式。底部按钮的高度是固定的,使用相对单位rpx。在不一样的设备上,map组件高度要能跟据设备的屏幕高度自动拉伸或者收窄,而不影响到显示效果。使用弹性布局是最佳的解决方案,只要设置下方按钮flex:1,上方自适应便可api

使用cover-image打造体验和谐的控件

咱们可使用map组件得controls 作制做控件。也可使用cover-image来制做。

体验了一下其余的地图类小程序,发现大部分的地图控件使用了map组件的controls来制做,controls控件自带按压的交互效果,可是只能使用图片,没法设置样式,且它们的宽高大小单位默认是px,在不一样设备上,实际体验很诡异。

青桔单车使用cover-image是来制做覆盖在地图表面的控件,在样式中经过rpx相对单位来设置控件大小,这能带来使人舒心的效果,不只如此,这几天忽然发现官方文档更新了,cover-image之后将彻底代替controls

在map组件上作出阴影效果

在开发地图首页的过程当中,最令我印象深入的莫过于此了,毕竟这深深折磨过我好长一段时间。随意举两个例子,先来看看这些小程序中的效果。

有没有阴影效果,对于实际使用来讲,彻底没有任何影响,但也许这就是前端吧,即便在map组件上实现起来各类坑,前端开发者都得去想办法实现,正如雷布斯说过得那句话:由于咱们是工程师。哪怕它的实际体验只能好百分之一,咱们都愿意付出百分之九十九的努力

前面有讲到map组件层级最高。这也是最坑的一点。起初我天真地觉得使用css box-shadow属性就能搞定,坑爹的开发者工具中确实也会显示出阴影效果,可是一到真机测试,全部的阴影都会被map组件覆盖,尝试了各类方法无果,而cover-view和cover-image可以支持的css样式又只有简单的几种,想要在map组件上使用css作出阴影效果基本上是不可能的

目前解决方案只有一个,就是使用cover-image,添加一张能覆盖在map组件之上的图片来模拟阴影。实际上,这些大厂都是这样作的。

分析完了上述的问题,咱们就能顺利作出这个主界面的效果,附上主页的wxml你就会明白怎么作了,样式具体实现方法能够查看个人源码

<view class='map-box'>
  <map id='myMap' latitude='{{latitude}}' longitude='{{longitude}}' markers='{{markers}}' polyline='{{polyline}}' scale='{{scale}}' bindcontroltap='controltap' bindregionchange='regionchange' bindmarkertap='toVisit' show-location>
      <!-- 地图上下阴影 -->
      <cover-image class='map-shadow-top' src='/images/map-shadow-top.png'/>
      <cover-image class='map-shadow-btm' src='/images/map-shadow-btm.png'/>
      <!-- 顶部横幅 -->
      <cover-view class='top-tips'>
        <cover-image class='top-icon' src='/images/top-tip.png'/>
        <cover-view class='top-text'>{{topText}}</cover-view>
      </cover-view>
      <!-- 中心坐标 -->
      <cover-image class='map-icon_point' src='/images/point_in_map.png'/>
      <!-- 控件 -->
      <cover-image class='map-icon map-icon_msg' src='/images/icon-msg.png' bindtap='toMsg'/>
      <cover-image class='map-icon map-icon_user' src='/images/icon-user.png' bindtap='toUser'/>
      <cover-image class='map-icon map-icon_reset' src='/images/reset.png' bindtap='toReset'/>
  </map>
</view>
<view class='main-btn' bindtap='toScan'>
  <text class='main-text'>扫码解锁</text>
</view>
复制代码

为地图添加定位功能

小程序为咱们提供了不少好用的API,开发时能够去查看 小程序API

只须要调用一下 wx.getLocation(OBJECT) 这个API就能够很轻松地获取到当前所在位置

wx.getLocation({
  type: 'gcj02',
  success: (res) => {
    let longitude = res.longitude;
    let latitude = res.latitude;
    this.setData({
      longitude,
      latitude
    })
})
复制代码

作出体验良好的地图交互

地图类小程序中,map组件上最主要地交互,莫过于重置定位这个按钮

重置定位功能实现起来很简单,只须要先建立一个map上下文,再调用moveToLocation()API就能够实现

onReady() {
    // 建立map上下文  保存map信息的对象
    this.mapCtx = wx.createMapContext('myMap');
  }
复制代码

在使用摩拜单车小程序地时候,若是缩放过地图视野,那么每次重置定位后,都要再去手动缩放地图寻找单车,由于单车扎堆在一块儿了

在青桔单车中,体验就好多了,重置定位后,也会重置地图视野地缩放级别,就能很快速判断附件单车位置,实现方法很简单,只须要在重置定位后设置1s后调回缩放比

toReset(){
    //调回缩放比,提高体验
    setTimeout(()=>{
      this.setData({
        scale: 18
      })
    },1000)
    this.mapCtx.moveToLocation();
}
复制代码

这也是一个小小的细节,地图类的小程序均可以用得上,实现的效果以下,这个体验很

写一个随机函数来生成伪造单车

为了实现一些更加高级的功能,我不得不作一些假数据,来模拟更加逼真的体验。

我简单的写了一个方法用来在当前定位的坐标点附件随机生成一批单车。

tocreate(res) {
// 随机单车数量设置 这里设置为1-20辆
    let ran = Math.ceil(Math.random() * 20);
    let markers = this.data.markers;
    for(let i = 0; i < ran; i++) {
      // 定义一个临时单车对象
      var t_bic = {
        "id": 0,
        "title":'去这里',
        "iconPath": "/images/map-bicycle.png",
        "callout":{},
        "latitude": 0,
        "longitude": 0,
        "width": 52.5,
        "height": 30
      }
      // 随机
      var sign_a = Math.random();
      var sign_b = Math.random();
      // 单车分布密集度设置
      var a = (Math.ceil(Math.random() * 99)) * 0.00002;
      var b = (Math.ceil(Math.random() * 99)) * 0.00002;
      t_bic.id = i;
      t_bic.longitude = (sign_a > 0.5 ? res.longitude + a : res.longitude - a);
      t_bic.latitude = (sign_b > 0.5 ? res.latitude + b : res.latitude - b);
      markers.push(t_bic);
    }
    //将模拟的单车数据暂时存储到本地
    wx.setStorage({
      key: 'bicycle',
      data: markers
    })
    this.setData({
      markers
    })
}
复制代码

接在来只要在map组件的bindregionchange事件中调用伪造单车的函数就好了

bindregionchange事件能在map视野发送变化时触发,可是我不但愿地图稍做移动就会刷新单车,因此还须要简单模拟一下移动刷新单车的阈值

regionchange(e){ 
    // 拿到起点经纬度
    if(e.type == 'begin') {
      this.mapCtx.getCenterLocation({
        type: 'gcj02',
        success: (res) => {
          this.setData({
            lastLongitude: res.longitude,
            lastLatitude: res.latitude
          })
        }
      })
    }
    // 拿到当前经纬度
    if (e.type == 'end') {
      this.mapCtx.getCenterLocation({
        type: 'gcj02',
        success: (res) => {
          let lon_distance = res.longitude - this.data.lastLongitude;
          let lat_distance = res.latitude - this.data.lastLatitude;
          // console.log(lon_distance,lat_distance)
          // 判断屏幕移动距离,若是超过设定的阈值,模拟刷新单车
          if (Math.abs(lon_distance) >= 0.0035 || Math.abs(lat_distance) >= 0.0022){
            console.log('刷新单车')
            this.setData({
              // 刷新单车以前先清空原来的单车
              markers: []
            })
            this.tocreate(res)
          }
        }
      })
    }
}
复制代码

这样,就作出了以下的效果

实现判断距离最近单车的功能

大家应该早就发现,地图上的单车中,距离最近的那辆单车头上会有离我最近一个小气泡。 这个就是检索出最近的单车的功能,摩拜单车就实现了这个功能,但是青桔单车官方并无加入这个小的体验,之后应该也会有吧。这里我尝试去实现了一下

实现逻辑

  1. 遍历当前地图上的每一辆单车和中心坐标点的距离,存到一个数组中

  2. 遍历数组,找出其中的最小值,并返回最小值的索引

  3. 在最小值的索引对应的单车中添加气泡提示

    nearestBic(res) {
        // 找出最近的单车
        let markers = this.data.markers;
        let min_index = 0;
        let distanceArr = [];        //存放单车距离的数组
        for (let i = 0; i < markers.length; i++) {
          let lon = markers[i].longitude;
          let lat = markers[i].latitude;
          // 计算距离  sqrt((x1-x2)^2 + (y1-y2)^2 )
          let t = Math.sqrt((lon - res.longitude) * (lon - res.longitude) + (lat - res.latitude) * (lat - res.latitude));
          let distance = t;
          // 将每一次计算的距离加入数组 distanceArr
          distanceArr.push(distance)
        }
        //从距离数组中找出最小值
        let min = distanceArr[0];
        for (let i = 0; i < distanceArr.length; i++) {
          if (parseFloat(distanceArr[i]) < parseFloat(min)) {
            min = distanceArr[i];
            min_index = i;
          }
        }
        let callout = "markers[" + min_index + "].callout";
        // 清除旧的气泡,设置新气泡
        wx.getStorage({
          key: 'bicycle',
          success: (res) => {
            this.setData({
              markers: res.data,
              [callout]: {
                "content": '离我最近',
                "color": "#ffffff",
                "fontSize": "16",
                "borderRadius": "50",
                "padding": "10",
                "bgColor": "#0082FCaa",
                "display": 'ALWAYS'
              }
            })
          }
        })
    }
    复制代码

将这个函数在每次刷新单车map视野改变的时候调用,就能看到以下的效果了,详细调用过程请移步:源码

实现手动选中单车自动规划步行至路径

嗯。。。这个功能我以为仍是有必要的,在一些场景中会遇到。

好比:我想骑车,眼前没有车。

或者只有一辆车,打开微信扫码,这时糟糕的结果出现了:该单车暂时没法使用。

我仍是想骑车,不想走路,地图的功能就发挥做用了,我会查看地图附近别的单车,这时候看到了一些单车,可是得走一段路才能找到它,若是能够点一下这辆单车,就自动规划步行的路线就行了。

因而乎,我大胆地作了一个实现,以下图

接下来说讲,怎么去实现它

想要实现自动路径规划的功能,本身去实现基本上不可能,咱们须要借助第三方强大的力量来作到。

引入高德地图SDK

首先不知道你会不会这样想:What?腾讯地图里面用高德SDK?

这没有什么不能够的,在微信小程序中,不管是百度地图、高德地图、仍是腾讯地图,都为小程序专门提供了Javascript SDK

高德地图微信小程序 SDK 能帮助咱们在小程序中获取到丰富的地址描述POI实时天气数据,以及实现地址解析逆地址解析等功能,很是强大,不过这里咱们只须要使用到它路径规划的功能

高德地图微信小程序SDK

腾讯地图和百度地图都没有为微信小程序提供自动路径规划的功能,因此高德地图仍是很贴心的。

想要使用它,必须前往高德地图开放平台进行注册,获取到本身的key,详细的步骤在高德地图微信小程序SDK入门指南中介绍得很清楚

SDK下载地址

下载好后把它解压,在项目目录新建一个libs文件夹把它放进去

接着在须要用到得js文件顶部引入

var amapFile = require('../../libs/amap-wx.js');
var myAmapFun = new amapFile.AMapWX({ key: '你的key' });
复制代码

有了它,就能够写一个专门负责规则路径得方法

route(bic){
    // 获取当前中心经纬度
    this.mapCtx.getCenterLocation({
      success: (res) => {
        // 调用高德地图步行路径规划API
        myAmapFun.getWalkingRoute({
          origin: `${res.longitude},${res.latitude}`,
          destination: `${bic.longitude},${bic.latitude}`,
          success: (data) => {
            let points = [];
            if (data.paths && data.paths[0] && data.paths[0].steps) {
              let steps = data.paths[0].steps;
              for (let i = 0; i < steps.length; i++) {
                let poLen = steps[i].polyline.split(';');
                for (let j = 0; j < poLen.length; j++) {
                  points.push({
                    longitude: parseFloat(poLen[j].split(',')[0]),
                    latitude: parseFloat(poLen[j].split(',')[1])
                  })
                }
              }
            }
            // 设置map组件polyline,绘制线路
            this.setData({
              polyline: [{
                points: points,
                color: "#ffffffaa",
                arrowLine:true,
                borderColor: "#3CBCA3",
                borderWidth:2,
                width: 5,
              }]
            });
          }
        })
      }
    })
}
复制代码

微信小程序map组件提供了polyline属性,它能在map组件上方跟据设置好的点来绘制路径

路径的颜色和样式均可以设置,哇~简直有点酷

在这里,为了致敬青桔单车,我尽可能的把路径的风格作得青桔单车类似😀,而后咱们再来回顾一下效果

打造体验良好的登陆界面

地图主界面打理好了,接下来写一下登陆界面吧。

登陆页面看似简单,可是想要作出一个体验不错的登陆界面,实际实现起来,里面的逻辑仍是蛮多的

自动分割手机号

在青桔单车小程序中,发现了这样一个小细节,输入框中输入的手机号会自动进行分割,感受这是一个不错的用户体验,分割显示的手机号,能使得输入过程当中的错误一目了然,看起来更爽

为了模仿出这个体验,我按照本身的逻辑去实现了它。

个人实现逻辑思路

  1. 手机号码都是11位的,分为三段 XXX 空格 XXXX 空格 XXXX,咱们在第3次输入第7次输入的数字后追加空格,那不就能实现这个效果了么

  2. 由于加入了两个空格,因此设置输入框最大长度为13位

  3. input的value属性绑定到逻辑层的data中的定义的phoneText,以后就能够用js来改变它的显示 重要!!

  4. 设置bindinput属性,让每次输入都执行一下input 函数

    <input class='input' placeholder='请输入手机号' maxlength="13" value='{{phoneText}}' bindinput='input'/>

我写了这个input方法来实现手机号的分割

input(e) {
        let value = e.detail.value;
        //正则过滤
        value = value.replace(/[\u4E00-\u9FA5`~!@#$%^&*()_+<>?:"{},.\/;'[\]\-\sa-zA-Z]*/g, "");
        let result = [];
        for (let i = 0; i < value.length; i++) {
          if (i == 3 || i == 7) {
            result.push(" ", value.charAt(i));
          }
          else {
            result.push(value.charAt(i));
          }
        }
        this.setData({
          phoneText: result.join("")
        })
    }
复制代码

注:不过为了作出这个效果,仍是作出一些了妥协,那就是不能调用微信内置的数字键盘输入。不然将会看不到这个分割的效果

其实使用微信内置的键盘,能够很方便的规避掉非法字符的输入,也就是数字之外的字符,如:英文字母标点符号等。

按钮 可用&不可用 逻辑

在输入框未完成基本的填写以前,按钮应该是不可用的,验证码输入框应该要作隐藏,待用户填写完以后,验证码输入框出现,验证码输入完毕后按钮亮起,这样的设定应该更加符合用户的心理暗示

这个页面,涉及到两个按钮 获取验证码 以及下一步 和 一个 清楚输入框图标

  • 手机号输入框应是不能输入非数字之外的字符的,虽然这里确定不会存在xss,可是为了严谨,仍是用正则来过滤一下
  • 当输入框中存在内容的时候,清除内容按钮出现,清空内容后,清除内容按钮消失,全部按钮不可用
  • 在手机号码填写完后,获取验证码按钮变为可用状态,验证码输入框出现
  • 手机号码和验证码同时知足填写条件后下一步按钮变为可用状态
  • 点击下一步,或者获取验证码时,要校验手机号码是否正确

实现效果以下,具体实现代码请移步源码

扫码解锁功能

实现扫码解锁,只须要调用小程序的 wx.scanCode() 这个API,就能调用相机的扫码功能,固然,扫码以前先进行登陆检查,若未登陆,切换到登陆界面,因为只是前端功能的实现,因此扫码后直接跳转到解锁界面

toScan(){
    if (!app.globalData.loginStatus) {
      wx.showModal({
        title: '提示',
        content: '请先登陆',
        success: (res) => {
          if (res.confirm) {
            wx.navigateTo({
              url: '/pages/login/login'
            })
          }
        }
      })
    } else {
      wx.scanCode({
        success: (res) => {
          onlyFromCamera: false,
          console.log('扫码成功');
          wx.navigateTo({
            url: '/pages/unlock/unlock',
          })
        }
      })
    }
}
复制代码

解锁后进入骑行状态,效果以下:

骑行状态下,只显示当前骑行车辆,并在车辆上方添加气泡,代表骑行中

骑行计费

共享单车计费都是跟据使用时长来判断的,因为没有后端数据,这里也只能写一个计时器方法Time()来模拟计费

Time(){
    let s = 0;
    let m = 0
    // 计时开始
    this.timer = setInterval(() => {
      this.setData({
        second: s++
      })
      if (s == 60) {
        s = 0;
        m++;
        setTimeout(() => {
          this.setData({
            minute: m
          });
        }, 1000)
      };
    }, 1000)
  }
复制代码

当骑行开始时,调用计时器,开始计时,点击结束骑行,计时器中止,跟据时长计价,并跳转到支付页面

结语

由于时间比较短,项目有一些功能还未加入,也有一些待改进的地方,后续会抽时间慢慢打磨,若是你有更好的想法,也能够联系我一块儿完善。

最后,再次附上个人项目地址:改良版青🍊单车

若是喜欢,别吝啬你的Star哦!

原文连接:行无忌的成长小屋:撸一个『改良版青🍊单车』

相关文章
相关标签/搜索