马蜂窝旅游小程序

前言

前段时间学习了关于微信小程序的开发,光说不练假把式,所以就打算自己手撸一个微信小程序,而网上电商类小程序太多了,所以就选择了旅游攻略类小程序来练手。这是我第一次写小程序和第一次写文章,不足之处请多包涵,谢谢。下面我会分享我在写小程序的时候遇到的问题和获得的经验,希望能给你带来帮助,也欢迎大佬指正。最后,我要感谢我在写小程序的时候给我帮助的老师和同学,还有百度上所有给过我帮助的有名的无名的作者。我的废话说完了,先上项目效果图。


开发前的准备

  • 微信开发者工具
  • VsCode
  • Easy Mock

项目的所有页面

自定义顶部导航栏组件

微信小程序自带的 [顶部导航栏](https://developers.weixin.qq.com/miniprogram/dev/reference/configuration/page.html) 满足不了实际的需求所以就自己写了一个组件,顶部导航栏要想使用 [自定义组件](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/)(链接中详细介绍了关于自定义组件的使用方法)必须把app.json中window属性设置为:
"window": {
    "navigationBarTextStyle": "black",//导航栏标题颜色,仅支持 black / white
    "navigationStyle": "custom" //导航栏样式,仅支持以下值:default 默认样式custom 自定义导航栏,只保留右上角胶囊按钮
  }

wxml


  
    
    
      
        {{navbarData.cityName}}
        
      
    
    
    
    
      // 这里的搜索框不是一个input组件,只是一个view可供点击然后跳到搜索页
      
        
      
      搜索目的地/景点/攻略
    
    
  
  
  
    {{navbarData.title}}
  
  
  
    
      
        
      
      
        
          
        
    
  

组件中的元素都可以通过当前页面传入组件的数据控制显示与否

js就写了两个 路由跳转 函数,微信小程序官方文档有很详细的介绍,这里就不多赘述了。


登录界面


初进小程序,会跳到登录授权页面,因为微信小程序不再支持wx.getUserInfo 接口直接弹出授权框的开发方式,所以这里直接使用 button 组件,并将 open-type 指定为 getUserInfo 类型,获取用户基本信息。

小程序在授权允许访问用户信息后,又会弹出位置授权框用来获取用户当前所在地,来渲染主页面的数据。调用小程序给的接口 wx.getLocation(需要用户授权) 来获取经纬度,再把获取到的经纬度利用 百度地图开放平台 提供给小程序使用的API来获取当前城市的名字,并将城市名字放入缓存,好让主页面获取到。

注意: 使用wx.getLocation()需要在app.json中配置

"permission": {
    "scope.userLocation": {
      "desc": "小程序将获取你的位置信息"
    }
  }

登录界面js

// miniprogram/pages/login/login.js
const app = getApp()
Page({

  /**
   * 页面的初始数据
   */
  data: {
    show: false,
    // 顶部导航栏数据
    navbarData: {
      showCapsule: 0, //是否显示左上角图标   1表示显示    0表示不显示
      title: '马蜂窝旅游', //导航栏 中间的标题
      backgroundColor: '#354a98', //'#354a98'
      opacity: 1,
      showMain: 0,
    },
    // 此页面 页面内容距最顶部的距离
    height: app.globalData.height * 2 + 20,
  },
  bindGetUserInfo(res) {
    let that =this
    let info = res;
    if (info.detail.userInfo) {
      wx.login({
        success: function (res) {
          that.getPlaceData()
        }
      })
    }
  },
  /**
   * 生命周期函数--监听页面加载
   */
  onLoad: function (options) {
    let that = this;
    //页面加载时判断用户是否授权过,如果授权过直接跳到主页面,没有就显示授权按钮
    wx.getUserInfo({
      success: function (res) {
        wx.switchTab({
          url: '/pages/main/index'
        })
      },
      fail(err) {
        that.setData({
          show: true
        })
      }
    })
  },
  // 获取城市名字
  getCityName(location) {
    return new Promise((resolve, reject) => {
      let that = this;
      var e = {
        coord_type: "gcj02",
        output: "json",
        pois: 0,
        ak: '',//放上自己的ak密钥 密钥申请见上文百度地图开方平台链接
        sn: "",
        timestamp: ""
      };
      e.location = location;
      wx.request({
        url: "https://api.map.baidu.com/geocoder/v2/",
        data: e,
        header: {
          "content-type": "application/json"
        },
        method: "GET",
        success: function (t) {
          let currentCity = t.data.result.addressComponent.city;
          if (currentCity.slice(currentCity.length - 1) == "市") {
            currentCity = currentCity.slice(0, currentCity.length - 1)
          }
          wx.setStorageSync('currentCity', currentCity)
          resolve(currentCity) //通过城市名字 请求城市数据
        }
      })
    })
  },
  // 获取经纬度
  getLocation() {
    return new Promise((resolve, reject) => {
      wx.getLocation({
        type: 'wgs84',
        success(res) {
          const latitude = res.latitude
          const longitude = res.longitude
          let location = latitude + ',' + longitude
          console.log(location)
          resolve(location) //获取城市名字
        }
      })
    })
  },
  getPlaceData() { // 获取地理信息
    let that = this
    this.getLocation().then((val) => {
      return that.getCityName(val)
    }).then(()=>{
      wx.switchTab({
        url: '/pages/main/index'
      })
    })
  }
})

主页面

写小程序的时候我不知道主页面有两种样式,等我知道的时候已经写了不少东西了,所以就没有写成组件了,代码看起来就很冗长,这是我的失误(MangFu),希望你在想好写什么小程序的时候,一定要把小程序的页面结构想好来否则就会和我一样,要改的话就要改很多地方。

  • 普通城市页面

  • 热门城市页面

进入主页是,页面会先获取到缓存中的城市名字,再通过城市名字去请求数据,再根据请求到的数据中的ishot属性,如果ishot属性为真,就显示热门城市的页面 ,反之就显示普通城市的页面


‘我的’页面


‘我的’页面中主要是为了显示用户收藏的内容


景点详情页

因为种种原因(lan)页面中的大半数据没有放到Easy Mock里,马蜂窝本来就以大数据出名,数据ttm多了。


洲/国家/城市列表页

这个页面的布局分为三部分,头部搜索框用绝对定位定死、左部各大洲的列表用绝对定位定死,右部各大洲的国家是一个微信小程序自带的组件scroll-view

wxml





  
  
    
      
        
      
      搜索目的地
    
  
  
  
  
    
      {{continent.name}}
    
  
  
  
    
      
        {{des_city.title}}
        
          {{item.city_name}}
        
      
    
    
      
          
            
          
          {{des_city.city_name}}
        
    
  

js

// pages/destination/destination.js
const app = getApp()
Page({

  /**
   * 页面的初始数据
   */
  data: {
  
    navbarData: {
      showCapsule: 1, //是否显示左上角图标   1表示显示    0表示不显示
      title: '目的地切换', //导航栏 中间的标题
      backgroundColor: '#fff',//背景颜色
      showMain: 0 ///显示搜索框
    },
    height: app.globalData.height * 2 + 20,
    continents: [],
    curIndex: 0 //当前洲的索引值
  },
  
  switch_des(e) {
    let curIndex = e.currentTarget.dataset.index;
    this.setData({
      curIndex,
    })
  },
  
  goMain(e){
    const city_name = e.currentTarget.dataset.city_name;
    wx.setStorageSync('currentCity', city_name)
    wx.switchTab({
      url: '/pages/main/index'
    })
  },
  /**
   * 生命周期函数--监听页面加载
   */
  onLoad: function (options) {
    let that = this
    
    wx.request({
      url: 'https://www.easy-mock.com/mock/5ca457f04767c3737055c868/example/mafengwo/continents',
      success:(res)=>{
        that.setData({
          continents: res.data.continents
        })
      }
    })
  }
}

搜索页


实现的功能

点击切换列表

以主页为例

其实所有的切换列表功能都差不多,实现方法就是在被点击元素上设置一个 自定义属性 ( data-* ) 为唯一索引值,用 bind-tap 绑定一个点击事件,通过点击事件获取这个唯一索引值,再通过唯一索引值去数据源找到想要的内容,然后通过数据控制页面上显示的内容,在data数据源中设置一个数据如mcurIndex,表示当前选择的元素,用来区别于其他元素,显示不同的样式。

wxml


    
    
        {{item.name}}
    

js

selected_menu(e) {
    this.setData({
      mcurIndex: e.target.dataset.mindex,
      size: 0,
      showend: false
    })
    
    this.bitiyan()
  }

滑动页面改变顶部导航栏的可见度和上拉加载

  • 滑动页面改变顶部导航栏的可见度

    以主页为例


这里的实现方法是使用 scroll-view 组件,组件中有个 bindscroll 属性,会在页面滚动时触发bindscroll 绑定的事件还会给函数传递一个对象event,其中的scrollTop属性是我们需要的,根据scrollTop知道页面滚动了多少,然后动态设置要传给组件的数据里的 opacity 属性。


js

scroll(e) {
    let opacity = 0;
    if (e.detail.scrollTop < 60) {
      opacity = (e.detail.scrollTop / 100).toFixed(1);
    } else {
      opacity = 1;
    }
    this.data.navbarData.opacity = opacity;
    if (e.detail.scrollTop<10){
      this.setData({
        shownav: false
      })
    }else{
      this.setData({
        shownav: true
      })
    }
    this.setData({
      navbarData: this.data.navbarData,
    })
  }
  • 上拉加载

    以主页为例

这里的实现方法在 scroll-view 组件中加 bindscrolltolower 属性,会在页面触底时触发bindscrolltolower 绑定的事件。


bindDownLoad() {
    let part = 0; //已经显示的数据长度
    let all = 0; //总的数据长度
    
    if (this.data.ishot) {
      // 待完善 因为效果相同就没写了
    } else {
      if (this.data.mcurIndex === 0) {
        part = this.data.cur_view.length * 2;
        all = this.data.placeData.allGuide[this.data.mcurIndex].content[this.data.hlcurIndex].content.length;
      } else {
        part = this.data.cur_view.length;
        all = this.data.placeData.allGuide[this.data.mcurIndex].content.length;
      }

      if (part < all) {
        wx.showLoading({
          title: '正在加载'
        })
        setTimeout(() => {
          this.bitiyan(this.data.placeData)
          wx.hideLoading()
        }, 1000)
      } else {
        
        this.setData({
          showend: true
        })
      }
    }
  }

关于scroll-view组件有几点需要注意的

  • 设置竖向滚动的时后一定要设高度,有时候会发现设置了高度100%后,当滑到底部的时候,会显示不完整,这时候要看下你是否设置了margin/padding,或者父元素设置了margin/padding,这时的scroll-view组件的高度就要减去相应的margin/padding
  • 当设置为横向滚动时需要注意,scroll-view 中需要滑动的元素不可以用 float 浮动;scroll-view 中的包裹需要滑动的元素的大盒子用 display:flex 是没有作用的;scroll-view 中的需要滑动的元素要用 dislay:inline-block 进行元素的横向编排;包裹 scroll-view 的大盒子有明确的宽和加上样式--> overflow:hidden;white-space:nowrap;

收藏功能

收藏功能我是写在一个组件里,本来是想和顶部组件一样,供多个页面使用,后来因为写的页面中就只有一个有用到这个组件,这里就不单独说明这个组件了,而且这个组件和顶部组件基本差不多。

收藏功能的实现,当点击某一个景点时会触发点击事件,相信你看了列表切换功能,已经知道了 bind-tap 的使用方法,这里就不重复了。这里就是获取元素上的自定义属性,通过路由传参的方法传给详情页,详情页根据传递过来的数据,去数据源里获取相应的数据,再将数据传递给组件,当点击详情页上的收藏按钮时,会触发绑定的事件,然后会更新缓存中的collectData收藏夹数据。‘我的’页面会显示收藏夹中的数据

详情页js


onLoad: function(options) {
    
    let name = options.name;
    this.getinfo(name)
},

getinfo(name){
    
    let collectData = wx.getStorageSync('collectData') || [];
    if (collectData.filter(e => e.name === name).length > 0) {
      this.setData({
        placeData: collectData.filter(e => e.name === name)[0]
      })
    } else {
      let placeData = wx.getStorageSync('placeData')
      let view = placeData.allGuide[0].content.map(e => e.content)
      let newView = []
      for (let i = 0; i < view.length; i++) {
        newView.push(...view[i])
      }
      this.setData({
        placeData: newView.find(e => e.name === name)
      })
    }
    this.setBottom();
  },
  
  setBottom(){
    this.data.bottomData.placeData = this.data.placeData;
    let bottomData = this.data.bottomData;
    this.setData({
      bottomData
    })
  }

bottom组件的js

// components/bottom/bottom.js
const app = getApp()
Component({
  /**
   * 组件的属性列表
   */
  properties: {
    bottomData: {   //   由父页面传递的数据,变量名字自命名
      type: Object,
      value: {},
      observer: function (newVal, oldVal) {
       }
    }
  },

  /**
   * 组件的初始数据
   */
  data: {
    height: ''
  },
  attached: function () {
    // 获取是否是通过分享进入的小程序
    this.setData({
      share: app.globalData.share
    })
    // 定义导航栏的高度   方便对齐
    this.setData({
      height: app.globalData.height
    })
  },
  /**
   * 组件的方法列表
   */
  methods: {
    
    collected(){
      
      let {isCollect,collectors} = this.data.bottomData.placeData;
      isCollect = !isCollect;
      this.data.bottomData.placeData.isCollect = isCollect;
      let collectData = wx.getStorageSync('collectData') || [];
      if(isCollect){
        wx.showToast({
          title: '收藏成功',
          icon: 'success',
          duration: 2000
        })
        collectors++;
        collectData.push(this.data.bottomData.placeData);
      }else{
        wx.showToast({
          title: '已取消收藏',
          icon: 'success',
          duration: 2000
        })
        collectors--;
        collectData = collectData.filter(e => e.name != this.data.bottomData.placeData.name)
      }
      this.data.bottomData.placeData.collectors = collectors;
      
      wx.setStorageSync('collectData', collectData)
      let bottomData = this.data.bottomData;
      this.setData({
        bottomData
      })
    }
  }
})

搜索功能

效果一

效果二

搜索功能的实现是通过原生组件 input 上的bindinput属性,当键盘输入时触发bindinput属性绑定的方法,实时获取中输入的值,然后将获取到的值放入请求地址中请求数据,再将请求获得的数据放入页面的data数据源中,当请求到的数据不为空时,页面上会显示得到的所有相关数据,如效果一。当按下搜索按钮时会触发input框上bindconfirm属性绑定的事件,此时页面上会显示请求到的数据中的第一条,如效果二。

wxml

js

// pages/search/search.js
const app = getApp()
Page({

  /**
   * 页面的初始数据
   */
  data: {
    navbarData: {
      showCapsule: 1, //是否显示左上角图标   1表示显示    0表示不显示
      title: '马蜂窝旅游', //导航栏 中间的标题
      backgroundColor: '#ffffff', //'#354a98'
      city: '',
      opacity: 1,
      showMain: 0
    },
    height: app.globalData.height * 2 + 20,
    result: [],
    searchparams: '',
    show: true,
    searchHistory: [],
    showResult: false,
    showconfirm: false,
    placedata: []
  },
  
  clear() {
    this.setData({
      searchHistory: []
    })
    wx.removeStorageSync('searchHistory')
  },
  
  confirm(e) {
    if (e.detail.value != '') {
      let searchHistory = wx.getStorageSync('searchHistory') || []
      if (searchHistory.filter(a => a === e.detail.value).length === 0) {
        searchHistory.push(e.detail.value)
        wx.setStorageSync('searchHistory', searchHistory)
      }
      if (this.data.result.length > 0) {
        let currentCity = this.data.result[0].name;
        this.getCityDataByName(currentCity);
      }
      this.setData({
        show: false,
        showResult: false,
        showconfirm: true
      })
    }
  },
  
  gotomain(e) {
    wx.setStorageSync('currentCity', e.currentTarget.dataset.name)
    wx.switchTab({
      url: '/pages/main/index',
    })
  },
  
  gosearch(e) {
    let that = this
    wx.request({
      url: `https://www.easy-mock.com/mock/5ca457f04767c3737055c868/example/mafengwo/search?name=${e.currentTarget.dataset.name}`,
      success: (res) => {
        if (res.data.data.length > 0) {
          that.getCityDataByName(res.data.data[0].name)
        } else {
          this.setData({
            show: false,
            showResult: false,
            showconfirm: true
          })
        }
      }
    })

  },
  // 通过城市名字 获取城市数据
  getCityDataByName(cityname) {
    let that = this
    wx.request({
      url: 'https://www.easy-mock.com/mock/5ca457f04767c3737055c868/example/mafengwo/china',
      success: (res) => {
        let placedata = [];
        placedata.push(...res.data.data.china.filter(e => e.chName === cityname))
        that.setData({
          placedata,
          show: false,
          showResult: false,
          showconfirm: true
        })
      }
    })
  },
  
  search(e) {
    let that = this
    wx.request({
      url: `https://www.easy-mock.com/mock/5ca457f04767c3737055c868/example/mafengwo/search?name=${e.detail.value}`,
      success: (res) => {
        if (res.data.data.length > 0) {
          that.changecolor(res.data.data, e.detail.value)
        } else {
          that.setData({
            result: [],
            searchparams: '',
            showResult: false
          })
        }
      }
    })
  },
  
  changecolor(result, searchparams) {
    for (let j = 0; j < result.length; j++) {
      let i = result[j].name.search(searchparams);
      let left = result[j].name.slice(0, i),
        mid = result[j].name.slice(i, i + searchparams.length),
        right = result[j].name.slice(i + searchparams.length);
      result[j].left = left;
      result[j].mid = mid;
      result[j].right = right;
    }
    this.setData({
      result,
      searchparams,
      show: false,
      showResult: true,
      showconfirm: false
    })
  },
  _navback() {
    wx.navigateBack({
      delta: 1
    })
  },
  /**
   * 生命周期函数--监听页面加载
   */
  onLoad: function() {
    
    let searchHistory = wx.getStorageSync('searchHistory') || []
    this.setData({
      searchHistory
    })
  }

这个API接口是我用 Easy Mock写的

Easy Mock地址链接

Easy Mock 代码

{
  "data": function({
    _req
  }) {
    let i = 0,
    
      _data = [
        {
            name: '亚洲',
            type: '目的地'
          },
          {
            name: '欧洲',
            type: '目的地'
          },
          {
            name: '大洋洲',
            type: '目的地'
          },
          {
            name: '非洲',
            type: '目的地'
          },
          {
            name: '北美洲',
            type: '目的地'
          },
          {
            name: '南美洲',
            type: '目的地'
          },
          {
            name: '南极洲',
            type: '目的地'
          }
      ],
      
      name = _req.query.name;
    if (name != '') {
    
      let result = [];
      let data = []
      for (let j = 0; j < result.length; j++) {
      
        if (eval('/' + name + '/').test(result[j].name)) {
          data.push(result[j])
        }
        
        if (data.length > 8) break;
      }
      return data
    } else {
    
      return []
    }
  }
}

热门城市动画

因为动画只有6个元素,所以就没有必要写成数组遍历创建了,直接写6个盒子,给他们的样式初始化,让他们到自己的初始位置去。微信小程序提供了创建动画实例的API wx.createAnimation

wxml


  
    {{placeData.vlog[0].title}}
  
  
    
  


  
    {{placeData.vlog[1].title}}
  
  
    
  


  
    {{placeData.vlog[2].title}}
  
  
    
  


  
    {{placeData.vlog[3].title}}
  
  
    
  


  
    {{placeData.vlog[4].title}}
  
  
    
  


  
    {{placeData.vlog[5].title}}
  
  
    
  

wxss

.a{
  opacity: 0.9;
}
.b{
  transform: translate(170rpx,-110rpx) scale(0.8);
  opacity: 0.8;
}
.c{
  transform: translate(210rpx,-250rpx) scale(0.7);
  opacity: 0.7;
}
.d{
  transform: translate(10rpx,-350rpx) scale(0.6);
  opacity: 0.6;
}
.e{
  transform: translate(-250rpx,-290rpx) scale(0.8);
  opacity: 0.5;
}
.f{
  transform: translate(-300rpx,-130rpx) scale(0.9);
  opacity: 0.8;
}

js

// 动画的运行路线
  translate: function(i) {
    // 获取屏幕宽度来实现自适应
    let windowwidth = this.data.windowWidth;
    //动画的运行状态status[x轴偏移量,y轴偏移量,scale缩放倍数,opacity透明度],也是动画的运行路线
    let status = [
      [170, -110, 0.8, 0.7],
      [210, -250, 0.7, 0.6],
      [10, -350, 0.6, 0.5],
      [-250, -300, 0.8, 0.7],
      [-300, -130, 0.9, 0.8],
      [0, 0, 1, 0.9]
    ];
    let x = 0,
      y = 0,
      scale = 0,
      opacity = 0;
    for (let j = 0; j < 6; j++) {
      let animationName = 'animation' + (j + 1);
      x = status[(i + j) % 6][0] / 750 * windowwidth;
      y = status[(i + j) % 6][1] / 750 * windowwidth;
      scale = status[(i + j) % 6][2];
      opacity = status[(i + j) % 6][3];
      this.animation.translate(x, y).scale(scale).opacity(opacity).step()
      this.setData({
        [animationName]: this.animation.export()//导出动画数据传递给组件的 animation 属性
      })
    }
  },
  hotCityAnimation() {
    let i = 0;
    
    this.animation = wx.createAnimation({
      duration: 2000,
      timingFunction: 'ease',
    })
    let that = this
    let anicontrol = this.data.anicontrol
    anicontrol = setInterval(function() {
      that.translate(i)
      if (i == 5) {
        i = -1;
      }
      i++;
    }, 3000)
    this.setData({
      anicontrol
    })
  }

这里要注意的是,因为这是写在tabbar页面的动画,而且用了setinterval定时器,会按照指定的周期(以毫秒计)来执行注册的回调函数,意思就是即使你跳转到别的页面,动画依然在运行,当你回到主页时,动画就会运行出错,出现鬼畜,所以要在主页的onHide周期函数,监听页面隐藏时就把定时器给清除了,并且把动画实例也清除。

onHide: function() {
    let anicontrol = this.data.anicontrol;
    clearInterval(anicontrol)
    this.setData({
      animation1: '',
      animation2: '',
      animation3: '',
      animation4: '',
      animation5: '',
      animation6: ''
    })
  }

关于css

写这个小程序我没有用到任何UI框架,这有坏处,也有好处,坏处就是代码进度贼慢,好处就是自己增加了很多对css的理解。有想用UI框架的可以使用 WeUI 。链接里有详细的使用方法。


结语

因为时间和精力的缘故,小程序只写了几个页面和小部分功能,在写项目的过程中也发现了自己的很多不足,因此吃到了不少苦头,但是也学到了不少,可以说痛并快乐着。希望这篇文章能够对打算写小程序的你有一点帮助。GitHub源码在这里,需要自取。

你可能感兴趣的:(前端,微信小程序)