您好,登錄后才能下訂單哦!
這是一個(gè)完整的已經(jīng)線上運(yùn)行的天氣應(yīng)用小程序,點(diǎn)擊可查看源碼,可隨意 star。也可以掃描下方的小程序碼直接體驗(yàn)。
cdn.xitu.io/2018/7/2/16459858549fee06?w=344&h=344&f=png&s=63516">
效果圖:
鳴謝:pure 天氣 APP:首頁(yè)樣式借鑒了 pure天氣 APP。如侵刪。
地理編碼、天氣數(shù)據(jù)均來(lái)自百度地圖開(kāi)放平臺(tái)。個(gè)人開(kāi)發(fā)完全免費(fèi),有對(duì)應(yīng)的小程序 sdk,加入即可,但是返回的天氣數(shù)據(jù)較少。
- 注冊(cè)微信小程序,獲取 appid
- 注冊(cè)百度地圖開(kāi)放平臺(tái)開(kāi)發(fā)者,創(chuàng)建應(yīng)用,獲取 ak(其他配置自行查看)
- 在 app.js 中替換 globalData 中的 ak 為自己的 ak
- Run
無(wú)
因?yàn)橹皇且粋€(gè)個(gè)人版DEMO(完整版),開(kāi)發(fā)前就決定選擇免費(fèi)的天氣數(shù)據(jù)(個(gè)人開(kāi)發(fā)免費(fèi)),懶得去尋找其他的天氣數(shù)據(jù),懶得去注冊(cè)賬號(hào),就直接選擇了百度地圖開(kāi)放平臺(tái)的天氣數(shù)據(jù),正好也提供了小程序?qū)?yīng)的 sdk,但是可能相比于其他的天氣 API,百度返回的數(shù)據(jù)偏少:當(dāng)天 pm2.5、當(dāng)天和未來(lái)三天數(shù)據(jù)、當(dāng)天生活指數(shù),其他的就沒(méi)有了。但是對(duì)于一款簡(jiǎn)單的天氣應(yīng)用小程序來(lái)說(shuō)也夠了。
獲取天氣數(shù)據(jù)默認(rèn)返回當(dāng)前城市的天氣數(shù)據(jù),如果要獲取其他的城市的天氣數(shù)據(jù),需要傳入經(jīng)緯度。為了獲取其他城市的經(jīng)緯度,這里使用的地圖的地理編碼接口,輸入城市名,輸出經(jīng)緯度,然后調(diào)用獲取天氣數(shù)據(jù) API 即可。
該應(yīng)用只有五個(gè)個(gè)頁(yè)面:首頁(yè)、城市選擇頁(yè)、設(shè)置頁(yè)、關(guān)于頁(yè)、系統(tǒng)信息頁(yè)(展示頁(yè))。如下:
首頁(yè)最終的顯示效果是這個(gè)樣子:
從上到下依次是:其他城市天氣搜索、當(dāng)前城市數(shù)據(jù)展示、當(dāng)天和未來(lái)三天天氣數(shù)據(jù)展示、當(dāng)天生活指數(shù)展示、footer。下拉刷新會(huì)刷新當(dāng)前地區(qū)的天氣數(shù)據(jù)。其中,頂部城市天氣搜索和生活指數(shù)可以在設(shè)置中隱藏。屏幕右下角是一個(gè)可以移動(dòng)的懸浮球(片??)菜單,點(diǎn)擊后會(huì)彈出城市選擇、設(shè)置、關(guān)于頁(yè)面的入口。背景色默認(rèn)是 #40a7e7
純色,可在設(shè)置中更換背景圖,未來(lái)三天天氣預(yù)報(bào)和生活指數(shù)分別添加了透明的黑色背景。設(shè)計(jì)稿?沒(méi)有的,純?nèi)庋壅{(diào)試,直到自己看著舒服。
先定義一個(gè)方法獲取當(dāng)前地區(qū)的天氣數(shù)據(jù):
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
請(qǐng)?zhí)鎿Q為自己的 ak
,因?yàn)樾枰@取用戶的地理位置,所以在 fail
的回調(diào)中需要處理用戶拒絕獲取地理位置的邏輯,這里處理為:提示打開(kāi)地理位置授權(quán),3000ms
后 wx.openSetting()
跳轉(zhuǎn)到小程序設(shè)置頁(yè),如下:
fail (res) {
wx.stopPullDownRefresh()
let errMsg = res.errMsg || ''
// 拒絕授權(quán)地理位置權(quán)限
if (errMsg.indexOf('deny') !== -1 || errMsg.indexOf('denied') !== -1) {
wx.showToast({
title: '需要開(kāi)啟地理位置權(quán)限',
icon: 'none',
duration: 3000,
success (res) {
let timer = setTimeout(() => {
clearTimeout(timer)
wx.openSetting({})
}, 3000)
},
})
} else {
wx.showToast({
title: '網(wǎng)絡(luò)不給力,請(qǐng)稍后再試',
icon: 'none',
})
}
},
獲取到用戶的地理位置后,執(zhí)行 success
:
success (data) {
wx.stopPullDownRefresh()
let now = new Date()
// 存下來(lái)源數(shù)據(jù)
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'])
// 當(dāng)天實(shí)時(shí)溫度
data.temperature = `${results.weather_data[0].date.match(/\d+/g)[2]}`
wx.setStorage({
key: 'cityDatas',
data: data,
})
this.setData({
cityDatas: data,
})
},
看一下返回的天氣數(shù)據(jù)格式:
{
"error": 0,
"status": "success",
"date": "2018-06-29",
"results": [
{
"currentCity": "北京市",
"pm25": "55",
"index": [
{
"des": "天氣炎熱,建議著短衫、短裙、短褲、薄型T恤衫等清涼夏季服裝。",
"zs": "炎熱",
"tipt": "穿衣指數(shù)",
"title": "穿衣"
},
{
"des": "較適宜洗車,未來(lái)一天無(wú)雨,風(fēng)力較小,擦洗一新的汽車至少能保持一天。",
"zs": "較適宜",
"tipt": "洗車指數(shù)",
"title": "洗車"
},
{
"des": "各項(xiàng)氣象條件適宜,發(fā)生感冒機(jī)率較低。但請(qǐng)避免長(zhǎng)期處于空調(diào)房間中,以防感冒。",
"zs": "少發(fā)",
"tipt": "感冒指數(shù)",
"title": "感冒"
},
{
"des": "天氣較好,無(wú)雨水困擾,但考慮氣溫很高,請(qǐng)注意適當(dāng)減少運(yùn)動(dòng)時(shí)間并降低運(yùn)動(dòng)強(qiáng)度,運(yùn)動(dòng)后及時(shí)補(bǔ)充水分。",
"zs": "較不宜",
"tipt": "運(yùn)動(dòng)指數(shù)",
"title": "運(yùn)動(dòng)"
},
{
"des": "屬中等強(qiáng)度紫外線輻射天氣,外出時(shí)建議涂擦SPF高于15、PA+的防曬護(hù)膚品,戴帽子、太陽(yáng)鏡。",
"zs": "中等",
"tipt": "紫外線強(qiáng)度指數(shù)",
"title": "紫外線強(qiáng)度"
}
],
"weather_data": [
{
"date": "周五 06月29日 (實(shí)時(shí):34℃)",
"dayPictureUrl": "http://api.map.baidu.com/images/weather/day/duoyun.png",
"nightPictureUrl": "http://api.map.baidu.com/images/weather/night/qing.png",
"weather": "多云轉(zhuǎn)晴",
"wind": "東南風(fēng)微風(fēng)",
"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": "東南風(fēng)微風(fēng)",
"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": "東南風(fēng)微風(fēng)",
"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": "晴轉(zhuǎn)多云",
"wind": "南風(fēng)微風(fēng)",
"temperature": "35 ~ 25℃"
}
]
}
]
}
success
里緩存了最新一次獲取的天氣數(shù)據(jù)+更新的時(shí)間 cityDatas
,小程序的模板里無(wú)法使用方法,所以數(shù)據(jù)需要在 js
里面先格式化。calcPM
用來(lái)計(jì)算當(dāng)前 pm2.5 的質(zhì)量,返回“優(yōu)良差”類似字樣,范圍標(biāo)準(zhǔn)可自行搜索。當(dāng)天的實(shí)時(shí)溫度并沒(méi)有給出獨(dú)立的字段,而是混在了 wearther_data[0]
的 data
字段里:"date": "周五 06月29日 (實(shí)時(shí):34℃)"
,需要自行提取。返回的天氣 icon 和色調(diào)不搭,就沒(méi)有使用。其他的數(shù)據(jù)按照按照我們要顯示的格式直接填充即可。
獲取天氣數(shù)據(jù)傳參為經(jīng)緯度,所以搜索城市天氣時(shí),需先將城市轉(zhuǎn)換為對(duì)應(yīng)的經(jīng)緯度,然后調(diào)用獲取天氣數(shù)據(jù) API 即可。獲取經(jīng)緯度的 API 為:
https://api.map.baidu.com/geocoder/v2/?address=${address}&output=json&ak=${yourak}
返回的數(shù)據(jù)格式為:
{
"status":0,
"result":{
"location":{
"lng":117.21081309155257,
"lat":39.143929903310074
},
"precise":0,
"confidence":12,
"level":"城市"
}
}
然后直接調(diào)用獲取天氣 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 || '網(wǎng)絡(luò)不給力,請(qǐng)稍后再試',
icon: 'none',
})
}
},
fail (res) {
wx.showToast({
title: res.errMsg || '網(wǎng)絡(luò)不給力,請(qǐng)稍后再試',
icon: 'none',
})
},
complete () {
that.setData({
searchText: '',
})
},
})
},
search (val) {
// 動(dòng)畫
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
,會(huì)出現(xiàn)從頂部下小心心的動(dòng)畫,如下:
這里實(shí)現(xiàn)比較簡(jiǎn)單。
創(chuàng)建了一個(gè) heartbeat
的組件。wxml
結(jié)構(gòu)是遍歷數(shù)組,創(chuàng)建多個(gè)大小、位置隨機(jī)的圖片:
<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
,動(dòng)畫的使用比較簡(jiǎn)單,創(chuàng)建動(dòng)畫,然后賦予 animation
屬性即可,比較簡(jiǎn)單,但是也有局限性,比如,沒(méi)有直接的動(dòng)畫結(jié)束后的回調(diào),但是可以使用 setTimeout
來(lái)實(shí)現(xiàn)等。這里會(huì)用到可用窗口寬高,因?yàn)槎嗵幱玫搅嗽搮?shù),所以在 app.js
里面異步獲取了先。
動(dòng)畫代碼如下:
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)
},
},
首頁(yè)搜索特定關(guān)鍵詞后,調(diào)用組件 dance
方法即觸發(fā)小心心動(dòng)畫。
屏幕右下角的懸浮球提供了三個(gè)頁(yè)面的入口:城市選擇頁(yè)、設(shè)置頁(yè)、關(guān)于頁(yè)。菜單彈出、收回會(huì)有動(dòng)畫。
這里的動(dòng)畫分為彈出和收起,兩者寫起來(lái)基本上一樣的,只是動(dòng)畫的參數(shù)不一樣。這里貼出彈出的動(dòng)畫:
// 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(),
})
},
懸浮菜單是可以在屏幕上隨意滑動(dòng)的,方法也很簡(jiǎn)單,監(jiān)聽(tīng) touchmove
事件即可,因?yàn)椴藛握归_(kāi)方向是在左邊,所以懸浮菜單能往左邊移動(dòng)的最遠(yuǎn)距離要有一段間隔,否則展開(kāi)的菜單就進(jìn)入左邊屏幕了,移動(dòng)到上方同樣邏輯(后期可以改成菜單展開(kāi)方向隨移動(dòng)而改變,而不是一味在左邊展開(kāi))。
代碼如下:
menuMainMove (e) {
// 如果已經(jīng)彈出來(lái)了,需要先收回去,否則會(huì)受 top、left 會(huì)影響
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,
})
},
至于一些樣式、邏輯上的細(xì)節(jié),這里不再贅述,具體可查看源碼。
城市選擇頁(yè)面就是一個(gè)城市列表,如下:
點(diǎn)擊相應(yīng)的城市,跳轉(zhuǎn)到首頁(yè)獲取所選城市的天氣數(shù)據(jù)。這里的城市數(shù)據(jù)是這樣的格式無(wú)序的列表:
{ "letter": "B", "name": "北京市" }
因?yàn)樾枰凑兆帜概帕羞M(jìn)行排序,所以需要先排序再遍歷(城市數(shù)據(jù)是之前用過(guò)的數(shù)據(jù),沒(méi)有排序就直接粘過(guò)來(lái)了)。代碼如下:
// 按照字母順序生成需要的數(shù)據(jù)格式
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)
}
// 返回一個(gè)對(duì)象,直接用 wx:for 來(lái)遍歷對(duì)象,index 為 key,item 為 value,item 是一個(gè)數(shù)組
return obj
},
點(diǎn)擊城市后,需要通知首頁(yè)“我已經(jīng)切換城市了,麻煩獲取下這個(gè)城市的數(shù)據(jù)謝謝”,這里使用的是使用 getCurrentPages
獲取頁(yè)面堆棧,修改首頁(yè)數(shù)據(jù)的方式。代碼如下:
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({})
},
關(guān)于頁(yè)是一個(gè)展示頁(yè),沒(méi)有多少交互,使用到的 API 只有復(fù)制到剪切板 wx.setClipboardData
?!拔⑿趴焖俾?lián)系”使用的是小程序提供的聯(lián)系客服的方式<button open-type="contact" class='btn'></button>
,將 button
絕對(duì)定位隱藏到點(diǎn)擊區(qū)域的下方即可。有精力的話,可以自己搭建服務(wù),將小程序的消息 push 到自己的服務(wù)上去。
設(shè)置頁(yè)的功能看著有點(diǎn)多,其實(shí)并不多,只是一堆 API 的調(diào)用。這個(gè)頁(yè)面分了自定義、檢查更新、小工具、清除數(shù)據(jù)三個(gè)部分。各個(gè)設(shè)置參數(shù)保存在 storage
中。一個(gè)一個(gè)來(lái)說(shuō)。
自定義背景是將選取的圖片(wx.chooseImage
)保存(wx.saveFile
)到本地,然后首頁(yè)獲?。?code>wx.getSavedFileList)保存的圖片,在首頁(yè)展示出來(lái)即可。長(zhǎng)按刪除,則是獲?。?code>wx.getSavedFileList)保存的圖片,然后 wx.removeSavedFile
掉即可?,F(xiàn)在設(shè)置的是本地只保存一張圖片,所以重新設(shè)置其他背景時(shí),會(huì)刪除上一張背景圖,然后重新保存新背景圖。
實(shí)現(xiàn)如下:
defaultBcg () {
this.removeBcg(() => {
wx.showToast({
title: '恢復(fù)默認(rèn)背景',
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: '出錯(cuò)了,請(qǐng)稍后再試',
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: '發(fā)生錯(cuò)誤,請(qǐng)稍后再試',
icon: 'none',
})
}
},
})
},
該操作只是將首頁(yè)的頂部搜索 wx:if
掉而已。switch
組件的樣式可以通過(guò)修改默認(rèn)的類來(lái)修改,調(diào)一個(gè)自己滿意的即可:
.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
掉。
檢查更新默認(rèn)關(guān)閉。小程序的更新是在冷啟動(dòng)時(shí)去檢查,如果有新版本會(huì)異步下載,再次冷啟動(dòng)時(shí)會(huì)加載新版本。這里使用 wx.getUpdateManager
,因?yàn)樵?API 基礎(chǔ)庫(kù)支持最低版本是 1.9.90,基礎(chǔ)庫(kù)版本低的會(huì)提示不支持,顯示的文案也會(huì)相應(yīng)修改。
1)NFC
使用 wx.getHCEState
。
2)屏幕亮度
獲取屏幕亮度、設(shè)置屏幕亮度、保持常亮使用的 API 分別是 wx.getScreenBrightness
、wx.setScreenBrightness
、wx.setKeepScreenOn
。完整實(shí)現(xiàn)可查看源碼。
3)系統(tǒng)信息
系統(tǒng)信息會(huì)跳轉(zhuǎn)到新頁(yè)面。
1)首頁(yè)懸浮球復(fù)位
首頁(yè)懸浮球的位置信息是保存本地的變量 pos
,復(fù)位位置,清除 pos
即可。
2)恢復(fù)初始化設(shè)置
設(shè)置信息是保存本地的變量 setting
,復(fù)位位置,清除 setting
即可。
3)清除所有本地?cái)?shù)據(jù)
wx.clearStorage
即可。
Tip: 恢復(fù)初始化設(shè)置、清除所有本地?cái)?shù)據(jù)并沒(méi)有刪除設(shè)置的背景圖(如果有設(shè)置的話),這個(gè)后續(xù)可以加上。
其他代碼細(xì)節(jié),不再贅述,具體可查看源碼。
更新日志:
2018.07.04
2018.07.05
openSetting API
廢棄兼容處理(SDKVersion >= 2.0.7
使用 button
,引導(dǎo)用戶主動(dòng)打開(kāi)小程序設(shè)置頁(yè)面),如下:免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。