这是一个完整的已经线上运行的天气应用小程序,点击可查看源码,可随意 star。也可以扫描下方的小程序码直接体验。
image
新版首页(可根据不同的天气改变背景)
image
其他效果图:
image
image
地理编码、天气数据均来自百度地图开放平台。个人开发完全免费,有对应的小程序 sdk,加入即可,但是返回的天气数据较少。
- 注册微信小程序,获取 appid
- 气象数据和风天气,需要注册账号获取
key
;免费版只能获取三天的天气数据,若要获取七天的气象数据,可以申请个人开发者认证;- 在 app.js 中替换 globalData 中的
key
为自己的key
- Run
因为只是一个个人版DEMO(完整版),开发前就决定选择免费的天气数据(个人开发免费),懒得去寻找其他的天气数据,懒得去注册账号,就直接选择了百度地图开放平台的天气数据,正好也提供了小程序对应的 sdk,但是可能相比于其他的天气 API,百度返回的数据偏少:当天 pm2.5、当天和未来三天数据、当天生活指数,其他的就没有了。但是对于一款简单的天气应用小程序来说也够了。
获取天气数据默认返回当前城市的天气数据,如果要获取其他的城市的天气数据,需要传入经纬度。为了获取其他城市的经纬度,这里使用的地图的地理编码接口,输入城市名,输出经纬度,然后调用获取天气数据 API 即可。
该应用只有五个个页面:首页、设置页、关于页。如下:
首页最终的显示效果是这个样子:
image
从上到下依次是:其他城市天气搜索、当前城市数据展示、当天和未来三天天气数据展示、当天生活指数展示、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
的回调中需要处理用户拒绝获取地理位置的逻辑,这里处理为:提示打开地理位置授权,3000ms
后wx.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}`
})
})
}
},
在搜索框里搜索520
或521
,会出现从顶部下小心心的动画,如下:
image
这里实现比较简单。
创建了一个heartbeat
的组件。wxml
结构是遍历数组,创建多个大小、位置随机的图片:
然后使用的是小程序提供的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
// 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,
})
},
至于一些样式、逻辑上的细节,这里不再赘述,具体可查看源码。
关于页是一个展示页,没有多少交互,使用到的 API 只有复制到剪切板wx.setClipboardData
。“微信快速联系”使用的是小程序提供的联系客服的方式,将
button
绝对定位隐藏到点击区域的下方即可。有精力的话,可以自己搭建服务,将小程序的消息 push 到自己的服务上去。
设置页的功能看着有点多,其实并不多,只是一堆 API 的调用。这个页面分了自定义、检查更新、小工具、清除数据三个部分。各个设置参数保存在storage
中。一个一个来说。
自定义背景是将选取的图片(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{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.getScreenBrightness
、wx.setScreenBrightness
、wx.setKeepScreenOn
。完整实现可查看源码。
3)系统信息
系统信息会跳转到新页面。
其他代码细节,不再赘述,具体可查看源码。
作者:jack.zhang
GitHub:GitHub - zhangliwen1101/WeatherWidget: 小程序天气预报
智能垃圾分类,源码,扫码直接体验
定制属于你自己的婚礼邀请函,源码,扫描直接体验