zoukankan      html  css  js  c++  java
  • 两天撸一个天气应用微信小程序

    更新说明:

    I、气象数据由百度地图开放平台修改为了和风天气,需要注册账号获取 key

    II、d0e51c8 版本之后为小程序云开发版本,若未开通云开发功能,为不影响小程序正常运行,可以将版本号回退到 git reset d0e51c8 --hard,或,将云开发相关代码注释掉。具体可查看这里

    简介

    这是一个完整的已经线上运行的天气应用小程序,点击可查看源码,可随意 star。也可以扫描下方的小程序码直接体验。顺便推荐另一个开源小程序 掘金第三方版源码在这里

    新版首页(可选择内置背景)

    效果图:

    说明

    鸣谢:pure 天气 APP:首页样式借鉴了 pure天气 APP。如侵删。

    数据来源

    地理编码、天气数据均来自百度地图开放平台。个人开发完全免费,有对应的小程序 sdk,加入即可,但是返回的天气数据较少。

    运行前准备

    利益相关

    天气数据获取

    因为只是一个个人版DEMO(完整版),开发前就决定选择免费的天气数据(个人开发免费),懒得去寻找其他的天气数据,懒得去注册账号,就直接选择了百度地图开放平台的天气数据,正好也提供了小程序对应的 sdk,但是可能相比于其他的天气 API,百度返回的数据偏少:当天 pm2.5、当天和未来三天数据、当天生活指数,其他的就没有了。但是对于一款简单的天气应用小程序来说也够了。

    地理编码

    获取天气数据默认返回当前城市的天气数据,如果要获取其他的城市的天气数据,需要传入经纬度。为了获取其他城市的经纬度,这里使用的地图的地理编码接口,输入城市名,输出经纬度,然后调用获取天气数据 API 即可。

    具体实现

    该应用只有五个个页面:首页、城市选择页、设置页、关于页、系统信息页(展示页)。如下:

    首页

    首页最终的显示效果是这个样子:

    从上到下依次是:其他城市天气搜索、当前城市数据展示、当天和未来三天天气数据展示、当天生活指数展示、footer。下拉刷新会刷新当前地区的天气数据。其中,顶部城市天气搜索和生活指数可以在设置中隐藏。屏幕右下角是一个可以移动的悬浮球(片??)菜单,点击后会弹出城市选择、设置、关于页面的入口。背景色默认是 #40a7e7 纯色,可在设置中更换背景图,未来三天天气预报和生活指数分别添加了透明的黑色背景。设计稿?没有的,纯肉眼调试,直到自己看着舒服。

    主页面

    先定义一个方法获取当前地区的天气数据:

    init(params) {
      let that = this
      let BMap = new bmap.BMapWX({
        ak: globalData.ak,
      })
      BMap.weather({
        location: params.location,
        fail: that.fail,
        success: that.success,
      })
    },
    

    ak 请替换为自己的 ak,因为需要获取用户的地理位置,所以在 fail 的回调中需要处理用户拒绝获取地理位置的逻辑,这里处理为:提示打开地理位置授权,3000mswx.openSetting() 跳转到小程序设置页,如下:

    fail (res) {
      wx.stopPullDownRefresh()
      let errMsg = res.errMsg || ''
      // 拒绝授权地理位置权限
      if (errMsg.indexOf('deny') !== -1 || errMsg.indexOf('denied') !== -1) {
        wx.showToast({
          title: '需要开启地理位置权限',
          icon: 'none',
          duration: 3000,
          success (res) {
            let timer = setTimeout(() => {
              clearTimeout(timer)
              wx.openSetting({})
            }, 3000)
          },
        })
      } else {
        wx.showToast({
          title: '网络不给力,请稍后再试',
          icon: 'none',
        })
      }
    },
    

    获取到用户的地理位置后,执行 success

    success (data) {
      wx.stopPullDownRefresh()
      let now = new Date()
      // 存下来源数据
      data.updateTime = now.getTime()
      data.updateTimeFormat = utils.formatDate(now, "MM-dd hh:mm")
      let results = data.originalData.results[0] || {}
      data.pm = this.calcPM(results['pm25'])
      // 当天实时温度
      data.temperature = `${results.weather_data[0].date.match(/d+/g)[2]}`
      wx.setStorage({
        key: 'cityDatas',
        data: data,
      })
      this.setData({
        cityDatas: data,
      })
    },
    

    看一下返回的天气数据格式:

    {
        "error": 0, 
        "status": "success", 
        "date": "2018-06-29", 
        "results": [
            {
                "currentCity": "北京市", 
                "pm25": "55", 
                "index": [
                    {
                        "des": "天气炎热,建议着短衫、短裙、短裤、薄型T恤衫等清凉夏季服装。", 
                        "zs": "炎热", 
                        "tipt": "穿衣指数", 
                        "title": "穿衣"
                    }, 
                    {
                        "des": "较适宜洗车,未来一天无雨,风力较小,擦洗一新的汽车至少能保持一天。", 
                        "zs": "较适宜", 
                        "tipt": "洗车指数", 
                        "title": "洗车"
                    }, 
                    {
                        "des": "各项气象条件适宜,发生感冒机率较低。但请避免长期处于空调房间中,以防感冒。", 
                        "zs": "少发", 
                        "tipt": "感冒指数", 
                        "title": "感冒"
                    }, 
                    {
                        "des": "天气较好,无雨水困扰,但考虑气温很高,请注意适当减少运动时间并降低运动强度,运动后及时补充水分。", 
                        "zs": "较不宜", 
                        "tipt": "运动指数", 
                        "title": "运动"
                    }, 
                    {
                        "des": "属中等强度紫外线辐射天气,外出时建议涂擦SPF高于15、PA+的防晒护肤品,戴帽子、太阳镜。", 
                        "zs": "中等", 
                        "tipt": "紫外线强度指数", 
                        "title": "紫外线强度"
                    }
                ], 
                "weather_data": [
                    {
                        "date": "周五 06月29日 (实时:34℃)", 
                        "dayPictureUrl": "http://api.map.baidu.com/images/weather/day/duoyun.png", 
                        "nightPictureUrl": "http://api.map.baidu.com/images/weather/night/qing.png", 
                        "weather": "多云转晴", 
                        "wind": "东南风微风", 
                        "temperature": "38 ~ 25℃"
                    }, 
                    {
                        "date": "周六", 
                        "dayPictureUrl": "http://api.map.baidu.com/images/weather/day/duoyun.png", 
                        "nightPictureUrl": "http://api.map.baidu.com/images/weather/night/duoyun.png", 
                        "weather": "多云", 
                        "wind": "东南风微风", 
                        "temperature": "36 ~ 23℃"
                    }, 
                    {
                        "date": "周日", 
                        "dayPictureUrl": "http://api.map.baidu.com/images/weather/day/qing.png", 
                        "nightPictureUrl": "http://api.map.baidu.com/images/weather/night/qing.png", 
                        "weather": "晴", 
                        "wind": "东南风微风", 
                        "temperature": "35 ~ 23℃"
                    }, 
                    {
                        "date": "周一", 
                        "dayPictureUrl": "http://api.map.baidu.com/images/weather/day/qing.png", 
                        "nightPictureUrl": "http://api.map.baidu.com/images/weather/night/duoyun.png", 
                        "weather": "晴转多云", 
                        "wind": "南风微风", 
                        "temperature": "35 ~ 25℃"
                    }
                ]
            }
        ]
    }
    

    success 里缓存了最新一次获取的天气数据+更新的时间 cityDatas,小程序的模板里无法使用方法,所以数据需要在 js 里面先格式化。calcPM 用来计算当前 pm2.5 的质量,返回“优良差”类似字样,范围标准可自行搜索。当天的实时温度并没有给出独立的字段,而是混在了 wearther_data[0]data 字段里:"date": "周五 06月29日 (实时:34℃)",需要自行提取。返回的天气 icon 和色调不搭,就没有使用。其他的数据按照按照我们要显示的格式直接填充即可。

    城市天气搜索

    获取天气数据传参为经纬度,所以搜索城市天气时,需先将城市转换为对应的经纬度,然后调用获取天气数据 API 即可。获取经纬度的 API 为:

    https://api.map.baidu.com/geocoder/v2/?address=${address}&output=json&ak=${yourak}

    返回的数据格式为:

    {
        "status":0,
        "result":{
            "location":{
                "lng":117.21081309155257,
                "lat":39.143929903310074
            },
            "precise":0,
            "confidence":12,
            "level":"城市"
        }
    }
    

    然后直接调用获取天气 API 即可。具体代码如下:

    geocoder (address, success) {
      let that = this
      wx.request({
        url: getApp().setGeocoderUrl(address),
        success (res) {
          let data = res.data || {}
          if (!data.status) {
            let location = (data.result || {}).location || {}
            // location = {lng, lat}
            success && success(location)
          } else {
            wx.showToast({
              title: data.msg || '网络不给力,请稍后再试',
              icon: 'none',
            })
          }
        },
        fail (res) {
          wx.showToast({
            title: res.errMsg || '网络不给力,请稍后再试',
            icon: 'none',
          })
        },
        complete () {
          that.setData({
            searchText: '',
          })
        },
      })
    },
    search (val) {
      // 动画
      if (val === '520' || val === '521') {
        this.setData({
          searchText: '',
        })
        this.dance()
        return
      }
      wx.pageScrollTo({
        scrollTop: 0,
        duration: 300,
      })
      if (val) {
        let that = this
        this.geocoder(val, (loc) => {
          that.init({
            location: `${loc.lng},${loc.lat}`
          })
        })
      }
    },
    

    搜索动画彩蛋

    在搜索框里搜索 520521,会出现从顶部下小心心的动画,如下:

    这里实现比较简单。

    创建了一个 heartbeat 的组件。wxml 结构是遍历数组,创建多个大小、位置随机的图片:

    <image wx:for='{{arr}}' wx:key='{{index}}' animation='{{animations[index]}}' class='heart' style='left:{{lefts[index]}}px;top:{{tops[index]}}px;{{widths[index]}}rpx;height:{{widths[index]}}rpx;' src='/img/heartbeat.png'></image>
    

    然后使用的是小程序提供的 wx.createAnimation,动画的使用比较简单,创建动画,然后赋予 animation 属性即可,比较简单,但是也有局限性,比如,没有直接的动画结束后的回调,但是可以使用 setTimeout 来实现等。这里会用到可用窗口宽高,因为多处用到了该参数,所以在 app.js 里面异步获取了先。

    动画代码如下:

    dance (callback) {
        let windowWidth = this.data.windowWidth
        let windowHeight = this.data.windowHeight
        let duration = this.data.duration
        let animations = []
        let lefts = []
        let tops = []
        let widths = []
        let obj = {}
        for (let i = 0; i < this.data.arr.length; i++) {
          lefts.push(Math.random() * windowWidth)
          tops.push(-140)
          widths.push(Math.random() * 50 + 40)
          let animation = wx.createAnimation({
            duration: Math.random() * (duration - 1000) + 1000
          })
          animation.top(windowHeight).left(Math.random() * windowWidth).rotate(Math.random() * 960).step()
          animations.push(animation.export())
        }
        this.setData({
          lefts,
          tops,
          widths,
        })
        let that = this
        let timer = setTimeout(() => {
          that.setData({
            animations,
          })
          clearTimeout(timer)
        }, 200)
        let end = setTimeout(() => {
          callback && callback()
          clearTimeout(end)
        }, duration)
      },
    },
    

    首页搜索特定关键词后,调用组件 dance 方法即触发小心心动画。

    悬浮球菜单

    屏幕右下角的悬浮球提供了三个页面的入口:城市选择页、设置页、关于页。菜单弹出、收回会有动画。

    这里的动画分为弹出和收起,两者写起来基本上一样的,只是动画的参数不一样。这里贴出弹出的动画:

    // wxml
    <!-- 悬浮菜单 -->
    <view class='menus'>
      <image src="/img/location.png" animation="{{animationOne}}" class="menu" bindtap="menuOne"  style='top:{{pos.top}}px;left:{{pos.left}}px;'></image>
      <image src="/img/setting.png" animation="{{animationTwo}}" class="menu" bindtap="menuTwo"  style='top:{{pos.top}}px;left:{{pos.left}}px;'></image>
      <image src="/img/info.png" animation="{{animationThree}}" class="menu" bindtap="menuThree"  style='top:{{pos.top}}px;left:{{pos.left}}px;'></image>
      <image src="/img/menu.png" animation="{{animationMain}}" class="menu main" bindtap="menuMain" catchtouchmove='menuMainMove' style='top:{{pos.top}}px;left:{{pos.left}}px;'></image>
    </view>
    
    // js
    popp() {
      let animationMain = wx.createAnimation({
        duration: 200,
        timingFunction: 'ease-out'
      })
      let animationOne = wx.createAnimation({
        duration: 200,
        timingFunction: 'ease-out'
      })
      let animationTwo = wx.createAnimation({
        duration: 200,
        timingFunction: 'ease-out'
      })
      let animationThree = wx.createAnimation({
        duration: 200,
        timingFunction: 'ease-out'
      })
      animationMain.rotateZ(180).step()
      animationOne.translate(-50, -60).rotateZ(360).opacity(1).step()
      animationTwo.translate(-90, 0).rotateZ(360).opacity(1).step()
      animationThree.translate(-50, 60).rotateZ(360).opacity(1).step()
      this.setData({
        animationMain: animationMain.export(),
        animationOne: animationOne.export(),
        animationTwo: animationTwo.export(),
        animationThree: animationThree.export(),
      })
    },
    

    悬浮菜单是可以在屏幕上随意滑动的,方法也很简单,监听 touchmove 事件即可,因为菜单展开方向是在左边,所以悬浮菜单能往左边移动的最远距离要有一段间隔,否则展开的菜单就进入左边屏幕了,移动到上方同样逻辑(后期可以改成菜单展开方向随移动而改变,而不是一味在左边展开)。

    代码如下:

    menuMainMove (e) {
      // 如果已经弹出来了,需要先收回去,否则会受 top、left 会影响
      if (this.data.hasPopped) {
        this.takeback()
        this.setData({
          hasPopped: false,
        })
      }
      let windowWidth = SYSTEMINFO.windowWidth
      let windowHeight = SYSTEMINFO.windowHeight
      let touches = e.touches[0]
      let clientX  = touches.clientX
      let clientY = touches.clientY
      // 边界判断
      if (clientX > windowWidth - 40) {
        clientX = windowWidth - 40
      }
      if (clientX <= 90) {
        clientX = 90
      }
      if (clientY > windowHeight - 40 - 60) {
        clientY = windowHeight - 40 - 60
      }
      if (clientY <= 60) {
        clientY = 60
      }
      let pos = {
        left: clientX,
        top: clientY,
      }
      this.setData({
        pos,
      })
    },
    

    至于一些样式、逻辑上的细节,这里不再赘述,具体可查看源码

    城市选择页

    城市选择页面就是一个城市列表,如下:

    点击相应的城市,跳转到首页获取所选城市的天气数据。这里的城市数据是这样的格式无序的列表:

    { "letter": "B", "name": "北京市" }

    因为需要按照字母排列进行排序,所以需要先排序再遍历(城市数据是之前用过的数据,没有排序就直接粘过来了)。代码如下:

    // 按照字母顺序生成需要的数据格式
    getSortedAreaObj(areas) {
      // let areas = staticData.areas
      areas = areas.sort((a, b) => {
        if (a.letter > b.letter) {
          return 1
        }
        if (a.letter < b.letter) {
          return -1
        }
        return 0
      })
      let obj = {}
      for (let i = 0, len = areas.length; i < len; i++) {
        let item = areas[i]
        delete item.districts
        let letter = item.letter
        if (!obj[letter]) {
          obj[letter] = []
        }
        obj[letter].push(item)
      }
      // 返回一个对象,直接用 wx:for 来遍历对象,index 为 key,item 为 value,item 是一个数组
      return obj
    },
    

    点击城市后,需要通知首页“我已经切换城市了,麻烦获取下这个城市的数据谢谢”,这里使用的是使用 getCurrentPages 获取页面堆栈,修改首页数据的方式。代码如下:

    choose(e) {
      let item = e.currentTarget.dataset.item
      let name = item.name
      let pages = getCurrentPages()
      let len = pages.length
      let indexPage = pages[len - 2]
      indexPage.setData({
        // 是否切换了城市
        cityChanged: true,
        // 需要查询的城市
        searchCity: name,
      })
      wx.navigateBack({})
    },
    

    关于页

    关于页是一个展示页,没有多少交互,使用到的 API 只有复制到剪切板 wx.setClipboardData。“微信快速联系”使用的是小程序提供的联系客服的方式<button open-type="contact" class='btn'></button>,将 button 绝对定位隐藏到点击区域的下方即可。有精力的话,可以自己搭建服务,将小程序的消息 push 到自己的服务上去。

    设置页

    设置页的功能看着有点多,其实并不多,只是一堆 API 的调用。这个页面分了自定义、检查更新、小工具、清除数据三个部分。各个设置参数保存在 storage 中。一个一个来说。

    1. 自定义

    • 自定义首页背景

    自定义背景是将选取的图片(wx.chooseImage)保存(wx.saveFile)到本地,然后首页获取(wx.getSavedFileList)保存的图片,在首页展示出来即可。长按删除,则是获取(wx.getSavedFileList)保存的图片,然后 wx.removeSavedFile 掉即可。现在设置的是本地只保存一张图片,所以重新设置其他背景时,会删除上一张背景图,然后重新保存新背景图。

    实现如下:

    defaultBcg () {
      this.removeBcg(() => {
        wx.showToast({
          title: '恢复默认背景',
          duration: 1500,
        })
      })
    },
    removeBcg (callback) {
      wx.getSavedFileList({
        success: function (res) {
          let fileList = res.fileList
          let len = fileList.length
          if (len > 0) {
            for (let i = 0; i < len; i++)
            (function (path) {
              wx.removeSavedFile({
                filePath: path,
                complete: function (res) {
                  if (i === len - 1) {
                    callback && callback()
                  }
                }
              })
            })(fileList[i].filePath)
          } else {
            callback && callback()
          }
        },
        fail: function () {
          wx.showToast({
            title: '出错了,请稍后再试',
            icon: 'none',
          })
        },
      })
    },
    customBcg () {
      let that = this
      wx.chooseImage({
        success: function (res) {
          that.removeBcg(() => {
            wx.saveFile({
              tempFilePath: res.tempFilePaths[0],
              success: function (res) {
                wx.navigateBack({})
              },
            })
          })
        },
        fail: function (res) {
          let errMsg = res.errMsg
          // 如果是取消操作,不提示
          if (errMsg.indexOf('cancel') === -1) {
            wx.showToast({
              title: '发生错误,请稍后再试',
              icon: 'none',
            })
          }
        },
      })
    },
    
    • 打开顶部城市天气快捷搜索

    该操作只是将首页的顶部搜索 wx:if 掉而已。switch 组件的样式可以通过修改默认的类来修改,调一个自己满意的即可:

    .wx-switch-input{84rpx !important;height:43rpx !important;}
    .wx-switch-input::before{82rpx !important;height: 38rpx !important;}
    .wx-switch-input::after{ 38rpx !important;height: 38rpx !important;}
    
    • 显示生活指数信息

    同样 wx:if 掉。

    • 检查更新

    检查更新默认关闭。小程序的更新是在冷启动时去检查,如果有新版本会异步下载,再次冷启动时会加载新版本。这里使用 wx.getUpdateManager,因为该 API 基础库支持最低版本是 1.9.90,基础库版本低的会提示不支持,显示的文案也会相应修改。

    • 小工具

    1)NFC

    使用 wx.getHCEState

    2)屏幕亮度

    获取屏幕亮度、设置屏幕亮度、保持常亮使用的 API 分别是 wx.getScreenBrightnesswx.setScreenBrightnesswx.setKeepScreenOn。完整实现可查看源码

    3)系统信息

    系统信息会跳转到新页面。

    • 清除数据

    1)首页悬浮球复位

    首页悬浮球的位置信息是保存本地的变量 pos,复位位置,清除 pos 即可。

    2)恢复初始化设置

    设置信息是保存本地的变量 setting,复位位置,清除 setting 即可。

    3)清除所有本地数据

    wx.clearStorage 即可。

    Tip: 恢复初始化设置、清除所有本地数据并没有删除设置的背景图(如果有设置的话),这个后续可以加上。

    其他

    其他代码细节,不再赘述,具体可查看源码


    更新日志:

    2018.07.04

    • 城市选择页面添加城市列表搜索过滤功能

    2018.07.05

    • openSetting API 废弃兼容处理(SDKVersion >= 2.0.7 使用 button,引导用户主动打开小程序设置页面),如下:

    其他开源小程序

    感兴趣的可以看下另一个开源小程序噢:掘金小程序第三方版源码地址在这里,欢迎交流学习~~~~。


  • 相关阅读:
    [PHP] class_exists类不存在时会调用__autoload函数
    [Redis] Redis的消息机制- 发布订阅
    [发电] 现在正式入驻爱发电平台
    [MySQL] PHP IP登录限制的实现
    [Redis] 哈希表的Rehash机制
    [Redis] redis的hash类型底层结构哈希表
    [Linux] ls命令的几个常用参数实现按时间/文件大小排序
    [Go] 在gin框架gorm下查询一对多的数据
    [Redis] list底层的数据结构
    [GO]go redis实现滑动窗口限流-redis版
  • 原文地址:https://www.cnblogs.com/myvin/p/9243416.html
Copyright © 2011-2022 走看看