微信小程序天气预报(附源码)

简介

这是一个完整的已经线上运行的天气应用小程序。

新版首页(可根据不同的天气改变背景)

其他效果图:

数据来源

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

运行前准备

  • 注册微信小程序,获取 appid
  • 气象数据和风天气,需要注册账号获取 key;免费版只能获取三天的天气数据,若要获取七天的气象数据,可以申请个人开发者认证;
  • 在 app.js 中替换 globalData 中的 key 为自己的 key
  • Run

天气数据获取

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

地理编码

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

具体实现

该应用只有五个个页面:首页、设置页、关于页。如下:

首页

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

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

主页面

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

csharp 复制代码
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()跳转到小程序设置页,如下:

less 复制代码
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

kotlin 复制代码
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,
  })
},

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

json 复制代码
{
    "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}

返回的数据格式为:

json 复制代码
{
    "status":0,
    "result":{
        "location":{
            "lng":117.21081309155257,
            "lat":39.143929903310074
        },
        "precise":0,
        "confidence":12,
        "level":"城市"
    }
}

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

kotlin 复制代码
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结构是遍历数组,创建多个大小、位置随机的图片:

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

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

动画代码如下:

scss 复制代码
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方法即触发小心心动画。

悬浮球菜单

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

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

css 复制代码
// 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事件即可,因为菜单展开方向是在左边,所以悬浮菜单能往左边移动的最远距离要有一段间隔,否则展开的菜单就进入左边屏幕了,移动到上方同样逻辑(后期可以改成菜单展开方向随移动而改变,而不是一味在左边展开)。

代码如下:

ini 复制代码
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,
  })
},

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

关于页

关于页是一个展示页,没有多少交互,使用到的 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掉即可。现在设置的是本地只保存一张图片,所以重新设置其他背景时,会删除上一张背景图,然后重新保存新背景图。

实现如下:

javascript 复制代码
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组件的样式可以通过修改默认的类来修改,调一个自己满意的即可:

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

同样wx:if掉。

  • 检查更新

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

  • 小工具

1)NFC

使用wx.getHCEState

2)屏幕亮度

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

3)系统信息

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

​编辑

其他

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

作者:jack.zhang

▲长按二维码进行关注▲

回复「天气」,获取项目源码**▲**

相关推荐
秋子aria几秒前
模块的原理及使用
前端·javascript
菜市口的跳脚长颌几秒前
一个 Vite 打包配置,引发的问题—— global: 'globalThis'
前端·vue.js·vite
胖虎2651 分钟前
实现无缝滚动无滚动条的 Element UI 表格(附完整代码)
前端·vue.js
小左OvO2 分钟前
基于百度地图JSAPI Three的城市公交客流可视化(一)——线路客流
前端
星链引擎4 分钟前
企业级智能聊天机器人 核心实现与场景落地
前端
GalaxyPokemon5 分钟前
PlayerFeedback 插件开发日志
java·服务器·前端
爱加班的猫6 分钟前
深入理解防抖与节流
前端·javascript
自由日记20 分钟前
学习中小牢骚1
前端·javascript·css
泽泽爱旅行24 分钟前
业务场景-opener.focus() 不聚焦解决
前端
VOLUN29 分钟前
Vue3 选择弹窗工厂函数:高效构建可复用数据选择组件
前端·javascript·vue.js