兩天擼一個天氣應用微信小程式

myvin發表於2018-07-02

更新說明:

I、氣象資料由百度地圖開放平臺修改為了和風天氣,需要註冊賬號獲取 key

II、d0e51c8 版本之後為小程式雲開發版本,若未開通雲開發功能,為不影響小程式正常執行,可以將版本號回退到 git reset d0e51c8 --hard,或,將雲開發相關程式碼註釋掉。具體可檢視這裡

簡介

這是一個完整的已經線上執行的天氣應用小程式,點選可檢視原始碼,可隨意 star。也可以掃描下方的小程式碼直接體驗。順便推薦另一個開源小程式 掘金第三方版原始碼在這裡

兩天擼一個天氣應用微信小程式

新版首頁(可選擇內建背景)

兩天擼一個天氣應用微信小程式

效果圖:

兩天擼一個天氣應用微信小程式
兩天擼一個天氣應用微信小程式
兩天擼一個天氣應用微信小程式
兩天擼一個天氣應用微信小程式
兩天擼一個天氣應用微信小程式

資料來源

地理編碼、天氣資料均來自百度地圖開放平臺。個人開發完全免費,有對應的小程式 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;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 中。一個一個來說。

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{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)系統資訊

系統資訊會跳轉到新頁面。

兩天擼一個天氣應用微信小程式

  • 清除資料

1)首頁懸浮球復位

首頁懸浮球的位置資訊是儲存本地的變數 pos,復位位置,清除 pos 即可。

2)恢復初始化設定

設定資訊是儲存本地的變數 setting,復位位置,清除 setting 即可。

3)清除所有本地資料

wx.clearStorage 即可。

Tip: 恢復初始化設定、清除所有本地資料並沒有刪除設定的背景圖(如果有設定的話),這個後續可以加上。

其他

其他程式碼細節,不再贅述,具體可檢視原始碼

另一個開源小程式推薦

掘金小程式:文章在這裡:兩週擼一個掘金微信小程式原始碼在這裡

相關文章