這是一個完整的已經線上運行的天氣應用小程序,點擊可查看源碼,可隨意 star。也能夠掃描下方的小程序碼直接體驗。javascript
效果圖:css
鳴謝:pure 天氣 APP:首頁樣式借鑑了 pure天氣 APP。如侵刪。html
地理編碼、天氣數據均來自百度地圖開放平臺。我的開發徹底免費,有對應的小程序 sdk,加入便可,可是返回的天氣數據較少。java
無git
由於只是一個我的版DEMO(完整版),開發前就決定選擇免費的天氣數據(我的開發免費),懶得去尋找其餘的天氣數據,懶得去註冊帳號,就直接選擇了百度地圖開放平臺的天氣數據,正好也提供了小程序對應的 sdk,可是可能相比於其餘的天氣 API,百度返回的數據偏少:當天 pm2.五、當天和將來三天數據、當天生活指數,其餘的就沒有了。可是對於一款簡單的天氣應用小程序來講也夠了。github
獲取天氣數據默認返回當前城市的天氣數據,若是要獲取其餘的城市的天氣數據,須要傳入經緯度。爲了獲取其餘城市的經緯度,這裏使用的地圖的地理編碼接口,輸入城市名,輸出經緯度,而後調用獲取天氣數據 API 便可。json
該應用只有五個個頁面:首頁、城市選擇頁、設置頁、關於頁、系統信息頁(展現頁)。以下:小程序
首頁最終的顯示效果是這個樣子:微信小程序
從上到下依次是:其餘城市天氣搜索、當前城市數據展現、當天和將來三每天氣數據展現、當天生活指數展現、footer。下拉刷新會刷新當前地區的天氣數據。其中,頂部城市天氣搜索和生活指數能夠在設置中隱藏。屏幕右下角是一個能夠移動的懸浮球(片??)菜單,點擊後會彈出城市選擇、設置、關於頁面的入口。背景色默認是 #40a7e7
純色,可在設置中更換背景圖,將來三每天氣預報和生活指數分別添加了透明的黑色背景。設計稿?沒有的,純肉眼調試,直到本身看着舒服。api
先定義一個方法獲取當前地區的天氣數據:
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高於1五、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
,會出現從頂部下當心心的動畫,以下:
這裏實現比較簡單。
建立了一個 heartbeat
的組件。wxml
結構是遍歷數組,建立多個大小、位置隨機的圖片:
<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
裏面異步獲取了先。
動畫代碼以下:
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
中。一個一個來講。
自定義背景是將選取的圖片(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)系統信息
系統信息會跳轉到新頁面。
1)首頁懸浮球復位
首頁懸浮球的位置信息是保存本地的變量 pos
,復位位置,清除 pos
便可。
2)恢復初始化設置
設置信息是保存本地的變量 setting
,復位位置,清除 setting
便可。
3)清除全部本地數據
wx.clearStorage
便可。
Tip: 恢復初始化設置、清除全部本地數據並無刪除設置的背景圖(若是有設置的話),這個後續能夠加上。
其餘代碼細節,再也不贅述,具體可查看源碼。
更新日誌:
2018.07.04
2018.07.05
openSetting API
廢棄兼容處理(SDKVersion >= 2.0.7
使用 button
,引導用戶主動打開小程序設置頁面),以下: