给ofo共享单车撸一个微信小程序

作者: 这昵称好帅嘞 | 来源:发表于2017-05-07 18:47 被阅读15657次

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

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

    先上一波效果图:

    1.首页地图页 2.维修报障页 3.登录页 4.钱包余额页 5.充值页 6.获取了密码页 7.计费页

    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.首页地图页

    页面分析:

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

    相关文章

      网友评论

      • gsunneverdie:真的写得很好,条理,斯路非常清晰,在这里得到帮助了
      • 3038cb2d49d6:你好楼主~我要做个类似于ofo的共享单车的小程序,你留个方式我们看看能不能合作一下?功能和楼主做的差不多不过我们要进一步讨论下
      • 夏日清风_期待:感谢,看了感觉很有帮助
      • 6af75e073216:题主能不能把行驶路程的源码写一下,还有就是问下计时器在手机熄屏的情况下还能计时吗?
        这昵称好帅嘞: @Ctum 该写的代码都已经写过了,屏幕息屏计时器依然计时
      • yexiansen:你好,我是搞php的准备借用你的写的前端,把后端完善一下可以吗,还有设置了markets数据,页面怎么显示不出来呢
      • 那个阳光下奔跑的少年:因为ofo和支付宝合作了:smile:
      • _小田:向大佬低头
      • 管大侠:数据都是模拟的,我以为楼主抓包了ofo的请求找到接口了呢。不过做的不错,赞一个
        YBDSup:现在百度地图、高德地图都和共享单车合作了,在百度地图或是高德地图那边有对应的附近共享单车的API吗?
      • 天下雪:请问博主是否可以转载分享一下你的文章?
        微信小程序联盟网站,会注明作者及原文出处的:smile:
        这昵称好帅嘞:可以,我对这篇文章做了一下拆分,可以转载拆分篇,链接地址:http://www.jianshu.com/p/68e3b8927a77
      • 馥_郁:挺不错,可以去ofo面试下了
        这昵称好帅嘞: @馥_郁 😂
      • 程序员王大可:传说中,痴心的眼泪会倾城,霓虹熄了,世界渐冷清!
        这昵称好帅嘞: @挥刀斩马 烟花会谢,笙歌会停,显得这故事尾声更动听
      • 4cf4f22f7aae:楼主是东华理工的吗 没想到东华理工还有你这么一个大神,希望校友可以和我交一个朋友
        b1e8f16f2ffe::heart_eyes: 厉害了我也是东华理工的资源勘查专业的
        4cf4f22f7aae:@这昵称好帅嘞 私信你了 校友
        这昵称好帅嘞: @helloJeremy 哟小伙子,这都被你看出来了😂
      • 飘零之雪:点赞点赞
      • 知晓程序:你好!我们是爱范儿旗下专注于小程序生态的公众号知晓程序(微信号 zxcx0101)。我们很赞赏你的文章,希望能获得转载授权。授权后,你的文章将会在知晓程序社区(minapp.com)、爱范儿、AppSo 等渠道发布。此外,由于第三方同步抓取功能,您的内容也可能会被同步发表到今日头条、搜狐、网易号等,我们会注明来源和作者姓名。
        非常感谢~~~
        followyounger1:Setting data field "markers" to undefined is invalid.
        Setting data field "_markers" to undefined is invalid.
        你好,我用你的项目报错啊
        这昵称好帅嘞: @知晓程序 欢迎与我联系
        这昵称好帅嘞: @知晓程序 好的

      本文标题:给ofo共享单车撸一个微信小程序

      本文链接:https://www.haomeiwen.com/subject/psrutxtx.html