學習(入坑)前端想來已有三個月了,學習老是枯燥乏味的,寫代碼更是如此。可是,曾記否?高中時候的你能夠由於解出了一道困擾了你幾日的數學題而高興一成天。在程序員的世界裏,我想,沒有比作出一個項目更開心的了。javascript
是的,此前微信小程序學習了近一個月後,仿着APP-運動世界校園寫了一個微信小程序奔跑吧(取名爲這個是由於看到跑步第一時間就想到了奔跑吧,兄弟)。其實我以爲本身寫的這個項目着實不能稱得上是一個項目,由於它只實現了原APP的一小部分功能,寫得實在是簡陋,要想完美,果真仍是得一個團隊寫。好了,話很少說,咱們言歸正傳。css
頁面路徑、頁面的窗口表現、底部 tabhtml
"pages": [ "pages/run/run", "pages/map/map", "pages/chat/chat", "pages/mine/mine", "pages/music/music", "pages/share/share", "pages/play/play" ], "window": { "backgroundColor": "#F6F6F6", "backgroundTextStyle": "light", "navigationBarBackgroundColor": "#00B26A", "navigationBarTitleText": "奔跑吧", "navigationBarTextStyle": "white" }, "tabBar": { "color": "#B5B5B5", "selectedColor": "#1296DB", "list": [ { "text": "運動", "pagePath": "pages/run/run", "iconPath": "images/run.png", "selectedIconPath": "images/run-active.png" }, { "text": "動態", "pagePath": "pages/chat/chat", "iconPath": "images/chat.png", "selectedIconPath": "images/chat-active.png" }, { "text": "個人", "pagePath": "pages/mine/mine", "iconPath": "images/mine.png", "selectedIconPath": "images/mine-active.png" } ] },
不得不說微信小程序的tabBar是真的好用,不用寫一句js邏輯就能夠輕鬆實現。前端
我先從最簡單的開始提及吧,個人頁面幾乎沒有寫任何js(除了獲取已跑里程數據),都只是在切頁面。(注:因爲代碼量太大,我就不一一貼出來闡述了,就講講關鍵點,如下敘述皆如此,想看所有代碼的能夠去上面貼出來的github網址上看)java
頭部這裏我是直接用微信小程序提供的開放數據 open-data標籤直接展現了微信頭像和暱稱,比起用Button來是否是簡單多了。git
<view class="header"> <view class="left"> <open-data type="userAvatarUrl" class="left-ava"></open-data> </view> <view class="mid"> <view class="mid-top"> <open-data type="userNickName"></open-data> </view> <view class="mid-bottom">東華理工大學南昌廣蘭校區 軟件學院</view> </view> <view class="right"> <view class="arrow"></view> </view> </view>
右邊的那個箭頭也不用多說,就是簡單的css.程序員
.right .arrow{ width: 30rpx; height: 30rpx; border-top: 1px solid #fff; border-right: 1px solid #fff; transform-origin: 0 0; transform: rotate(45deg); }
接着咱們來關注一下挨着頭部下面的那部分,這裏使用了彈性佈局和涉及到1px問題。github
<view class="hd-footer"> <view class="ft-left"> <view class="num">120.00</view> <view class="str"> <text>學期目標</text> </view> </view> <view class="ft-mid"> <view class="num">{{num}}</view> <view class="str"> <text>已跑里程</text> </view> </view> <view class="ft-right"> <view class="num">0.00</view> <view class="str"> <text>計入成績</text> </view> </view> </view>
.hd-footer{ display: flex; padding: 30rpx; background-color: #fff; text-align: center; font-size:14px; } .ft-left{ flex: 1; position: relative; } .ft-left:after{ content:""; position: absolute; top: 0;left: 0; width: 200%; height: 200%; box-sizing: border-box; transform: scale(0.5); transform-origin: 0 0; border-right: 1px solid #aaa; } .ft-mid{ flex: 1; position: relative; } .ft-mid:after{ content: ''; position: absolute; top: 0; left: 0; width: 200%; height: 200%; box-sizing: border-box; transform: scale(0.5); transform-origin: 0 0; border-right: 1px solid #aaa; } .ft-right{ flex: 1; }
給父容器.hd-footer設置display: flex;給其三個子容器都設置爲flex: 1;使每一個子容器都佔父容器的三分之一。咱們知道border最小隻能設置成1px,可是咱們能夠經過添加一個僞元素來實現0.5px,這是css的一個小技巧。數據庫
下面的body部分一樣是使用了彈性佈局,這裏就再也不贅述。底部的.footer部分是使用了Vant組件的Cell單元格組件。小程序
{ "usingComponents": { "van-cell": "../vant-weapp/dist/cell/index", "van-cell-group": "../vant-weapp/dist/cell-group/index" } }
<view class="footer"> <van-cell-group> <van-cell title="聯繫客服" icon="setting-o" is-link url=""link-type="navigateTo"/> <van-cell title="設置" icon="service-o" is-link border="true"url="" link-type="navigateTo"/> </van-cell-group> </view>
最後再講講這裏惟一的js部分吧,{{num}}使用了MVVM(數據驅動頁面)思想,
經過完成一次跑步後設置全局數據sum的值,再在此界面獲取全局數據sum的值並賦予此頁面數據num,最後渲染到頁面上。提到MVVM就不得不誇讚它,真是一個偉大的創造,讓咱們能夠拋去繁瑣的DOM操做。
this.globalData = { sum: '0.00', baseUrl: 'http://neteasecloudmusicapi.zhaoboy.com' }
this.setData({ num: app.globalData.sum })
這個界面涉及的js邏輯較多,我會主講js方面。
頭部使用的依舊是彈性佈局,這裏就提一下文本超出則打省略號吧。
.hobby-title{ font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
身體部分一看有不少相同的結構,因而這裏我就寫了一個speak組件,組件的結構和樣式沒什麼好說的,就講講組件上的三個數據(時間、文本內容、圖片)怎麼來的。這就要說說這個經過position: fixed;來定位的按鈕了,點擊後能夠跳轉到分享頁面。
<view class="add" bindtap='share'> <image class="add-btn" src="../../images/add.png"></image> </view>
share() { wx.navigateTo({ url: '../share/share', }) },
這裏實現頁面的跳轉也是至關的簡單。
一個Button,一個input框和weui的Uploader上傳組件。
<view class="write"> <button type='primary' size='mini' class='btn' bindtap='send'>發佈</button> <input type='text' placeholder='分享...' class='towrite' bindconfirm='complete'></input> <view class="page__bd"> <view class="weui-cells"> <view class="weui-cell"> <view class="weui-cell__bd"> <view class="weui-uploader"> <view class="weui-uploader__hd"> <view class="weui-uploader__title">圖片上傳</view> <view class="weui-uploader__info">{{files.length}}/2</view> </view> <view class="weui-uploader__bd"> <view class="weui-uploader__files" id="uploaderFiles"> <block wx:for="{{files}}" wx:key="*this"> <view class="weui-uploader__file" bindtap="previewImage" id="{{item}}"> <image class="weui-uploader__img" src="{{item}}" mode="aspectFill" /> </view> </block> </view> <view class="weui-uploader__input-box"> <view class="weui-uploader__input" bindtap="chooseImage"></view> </view> </view> </view> </view> </view> </view> </view> </view>
在app.wxss裏引入weui的樣式。
@import './weui.wxss';
data: { files: [], fileID: [], content: '' }, chooseImage() { let that = this; wx.chooseImage({ sizeType: ['original','compressed'], sourceType: ['album','camera'], success(res) { console.log(res); that.setData({ files: that.data.files.concat(res.tempFilePaths) }) // ------ for(let i = 0; i < res.tempFilePaths.length; i++) { const filePath = res.tempFilePaths[i]; let randString = Math.floor(Math.random() * 1000000).toString() + filePath.match(/\.[^.]+?$/); wx.cloud.uploadFile({ cloudPath:randString, filePath, success: res => { console.log('上傳成功',res); that.data.fileID.push(res.fileID); }, fail: err => { console.log(err); } }) } } }) }, previewImage(e) { console.log(e); wx.previewImage({ current: e.currentTarget.id, urls: this.data.files }) }, complete(e) { this.setData({ content: e.detail.value }) }, send() { wx.cloud.callFunction({ name: 'createDynamic', data: { content: this.data.content, imagePath: this.data.fileID }, success(res) { console.log(res.result); wx.navigateBack(); }, fail(error) { console.log(error); } }) },
這裏用到了微信小程序的幾個API。經過wx.chooseImage的success回調函數來設置data裏的數據(圖片的路徑)。用wx.cloud.uploadFile將圖片資源上傳到雲開發的存儲裏,因爲每次只能上傳一張,因此這裏用了for循環。經過wx.previewImage能夠預覽圖片。具體每一個參數是什麼意思,你們能夠去官方文檔上看。
經過input上的bindconfirm='complete'來獲取input框的輸入內容,最後經過Button按鈕綁定的send方法來調用createDynamic雲函數。
// 雲函數入口文件 const cloud = require('wx-server-sdk') const env = 'lvwei666-pv2y1' cloud.init() const db = cloud.database({env}) // 雲函數入口函數 exports.main = async (event, context) => { const userInfo = event.userInfo; return await db.collection('dynamic').add({ data: { content: event.content, images: event.imagePath, createBy: userInfo.openId, createTime: new Date() } }) }
createDynamic雲函數拿到調用時傳來的內容和圖片路徑並添加了時間這一條數據,把這三條數據存儲到了dynamic這個數據庫裏。(這裏真的要吐槽一下雲函數的調試,一旦報錯,每次調試都要上傳一次雲函數,很是的耗時、麻煩。)
數據建立好了,咱們再去取數據。
const db = wx.cloud.database() const dynamic = db.collection('dynamic')
onShow: function () { let self = this; wx.showLoading({ title: '正在加載中' }); dynamic.get({ success(res) { let every = res.data.reverse() for (let n of every) { n.createTime = n.createTime.getFullYear() + '-' + (+n.createTime.getMonth() + 1) + '-' + n.createTime.getDate() + ' ' + n.createTime.getHours() + ':' + n.createTime.getMinutes() + ':' + n.createTime.getSeconds(); } self.setData({ every }) }, fail(error) { console.log(error); }, complete() { wx.hideLoading(); } }) },
其實在onPullDownRefresh頁面相關事件處理函數--監聽用戶下拉動做裏我也放了一樣的代碼,在下拉刷新的時候也能夠獲取數據。
拿到數據數組後我進行了兩個操做,一是使數組reverse一下,由於新添加的記錄(動態)會存在dynamic數據庫的最後;二是把時間這條數據進行了一頓字符串拼接操做來獲得你想要的樣子。
然後咱們就接着講動態頁面的body部分吧,也就是speak組件。先在此頁面引入speak組件。
<view class="body"> <block wx:for="{{every}}" wx:key="index"> <speak createTime="{{item.createTime}}" content="{{item.content}}" images="{{item.images}}"></speak> </block> </view>
"usingComponents": { "speak": "../../components/speak/speak" },
這裏也使用了for循環去渲染speak組件,由於dynamic集合裏能夠添加多條記錄,一個speak組件就渲染一條記錄。組件又是如何拿到頁面上的數據的呢?咱們看看下面的代碼就知道了。
properties: { createTime: { type: String, value: '' }, content: { type: String, value: '' }, images: { type: Array, value: [] } },
properties組件的屬性列表-這個可讓組件從頁面那裏拿到數據。有了數據就直接在html裏挖坑({{}}),把數據放上去就好了。
<view class="item"> <view class="header"> <view class="left"> <open-data type="userAvatarUrl"></open-data> </view> <view class="right"> <open-data class="right-top" type="userNickName"></open-data> <view class="right-bottom">{{createTime}}</view> </view> </view> <view class="body"> <view class="content">{{content}}</view> <view class="img" wx:for="{{images}}" wx:key="index" bindtap="previewImage" id='{{item}}'> <image src="{{item}}" mode="aspectFill" alt="" /> </view> </view> <view class="footer"> <view class="click"> <view class="click-left"> <image class="comment" src='../../images/comment.png'></image> <text class="comment-num">0</text> </view> <view class="click-right"> <image class="support" src="{{like ? '../../images/support.png' : '../../images/support-active.png'}}" bindtap='like'></image> <text class="support-num">{{num}}</text> </view> </view> </view> </view>
data: { like: true, num : 0 }, methods: { like() { if (this.data.like) { this.setData({ num: this.data.num + 1 }) }else { this.setData({ num: this.data.num - 1 }) } this.setData({ like: !this.data.like }) }, previewImage(e) { // console.log(e); wx.previewImage({ current: e.currentTarget.id, urls: this.properties.images }) } }
經過like方法來控制數據like的true或false來達到點贊和取消點讚的效果,這裏也用了wx.previewImage來預覽圖片。
這裏就不貼圖了,效果你們能夠根據我說的去上面的項目展現看。這個頁面首先能夠看到的是一個滑屏效果,用的是Vant的van-tab組件,每屏的標題和內容我存在了navData數據庫裏。點擊學期目標後彈出一個相似上拉菜單的用的是Vant的van-action-sheet組件。對於組件的引用和獲取數據庫的數據操做在前面都講過了,這裏就不講了。
說一下中間這個開始按鈕的css動畫吧,比起原APP裏的效果,我這個真的是差了許多,原APP裏的看起來就像水滴同樣流暢。
.anim{ width: 250rpx; height: 250rpx; background-color: white; opacity: 0.3; border-radius: 50%; position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); transform-origin: 0 0; animation: expend 2.5s ease-in-out both infinite; } @keyframes expend{ 0% { opacity: 0; transform: scale(1) translate(-50%,-50%); } 40% { opacity: 0.2; transform: scale(1.7) translate(-50%,-50%); } 100% { opacity: 0; transform: scale(1.7) translate(-50%,-50%); } } .start{ width: 250rpx; height: 250rpx; background-color: white; border-radius: 50%; position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); text-align: center; line-height: 250rpx; color: #7AD5C7; transform-origin: 0 0; animation: dd 2.5s linear both infinite; } @keyframes dd { 0%,8%,100%{ transform: translate(-50%,-50%) scale(1); } 5% { transform: translate(-50%,-50%) scale(.98); } }
.anim是一邊放大一邊改變其透明度,而.start就先縮小一點,而後立馬恢復原樣。
接着講音樂部分,點擊頁面上的音樂按鈕進入music頁面。
wx.request({ url: 'http://neteasecloudmusicapi.zhaoboy.com/top/list', data: { idx: 1 }, success: res => { console.log('熱歌', res); const songLists = res.data.playlist.tracks; this.setData({ songLists }); wx.hideLoading(); } })
這裏用wx.request發起請求來獲取網易雲音樂熱歌榜接口的熱歌信息並經過for循環將數據渲染到頁面上,這裏講一下data裏的idx: 1是什麼,(說明 : 調用此接口 , 傳入數字 idx, 可獲取不一樣排行榜)這是官方說明,0表明新歌榜,1表明熱歌榜等等。
<view class='songlist'> <block wx:for="{{songLists}}" wx:key="index"> <view class='item' data-id="{{item.id}}" bindtap='toPlayAudio'> <view class='index'>{{index + 1}}</view> <view class='rightView'> <view class='songTitle'>{{item.name}}</view> </view> </view> </block> </view>
toPlayAudio(e) { const id = e.currentTarget.dataset.id; wx.navigateTo({ url: `../play/play?id=${id}` }) },
注意每一個.item都有data-id="{{item.id}}",這是爲了等下跳去播放音樂頁面時知道播放的是哪首歌。
play頁面的界面和樣式都很簡單,咱們就看怎麼實現播放音樂的吧。
onLoad: function (options) { console.log(options); wx.setNavigationBarTitle({ title: '雲音樂', }) wx.setNavigationBarColor({ frontColor: '#ffffff', backgroundColor: '#3daed9', }) const id = options.id wx.request({ url: app.globalData.baseUrl + '/song/url', data: { id: id }, success: res => { console.log('歌曲詳情', res); if (res.data.code === 200) { this.createBackgroundAudio(res.data.data[0] || {}); } } }) wx.request({ url: app.globalData.baseUrl + '/song/detail', data: { ids: id }, success: (res) => { console.log('歌曲信息', res); this.setData({ song: res.data.songs[0] }) } }) }, createBackgroundAudio(songInfo) { const bgAudio = wx.getBackgroundAudioManager(); bgAudio.title = "title"; bgAudio.epname = "epname"; bgAudio.singer = "chris wu"; bgAudio.coverImgUrl = ""; bgAudio.src = songInfo.url; bgAudio.onPlay(res => { this.setData({ isPlay: true }) }) },
經過options能夠拿到跳轉頁面時傳進來的id,再用wx.request獲取歌曲的Url並調用wx.getBackgroundAudioManager背景音頻播放管理進行播放音樂,還用wx.request獲取了歌曲詳情。
<view> <button type='primary' bindtap='togglePlayStatus'> {{isPlay ? '暫停' : '播放'}} </button> </view>
togglePlayStatus() { const bgAu = wx.getBackgroundAudioManager(); if (this.data.isPlay) { bgAu.pause(); this.setData({ isPlay: false }) } else { bgAu.play(); this.setData({ isPlay: true }) } },
用togglePlayStatus方法來控制音樂的播放與暫停。
最後要講的是跑步部分,裏面的計算運動距離邏輯能夠說是整個項目裏最難解決的地方。修改了不少次,才讓它能夠比較粗糙的計算出跑了多少千米,爲此我也是流下了許多的汗水。
先是引入騰訊地圖。
<map id='myMap' scale='{{scale}}' latitude='{{latitude}}' longitude='{{longitude}}' polyline="{{polyline}}" show-location markers='{{markers}}'></map>
地圖的scale縮放級別、經緯度、polyline路線、show-location顯示帶有方向的當前定位點、markers標記點這些屬性就不詳細解釋了,感興趣的能夠去官方文檔看看。
onLoad: function(options) { let markers = []; let marker = { iconPath: "../../images/baseline.png", id: 0, width: 40, height: 40 }; wx.getLocation({ type: 'gcj02', success: (res) => { console.log(res) marker.latitude = res.latitude; marker.longitude = res.longitude; markers.push(marker) this.setData({ latitude: res.latitude, longitude: res.longitude, markers }) }, fail: (error) => { console.log(error); wx.showToast({ title: '獲取地理位置失敗', icon: 'none' }) } }) }, onReady() { this.mapCtx = wx.createMapContext('myMap'); this.start(); },
頁面加載時就經過wx.getLocation來獲取當前地理位置並添加一個起點做爲標記點,在頁面初次渲染完成時調用start函數,咱們再看看start作了什麼。
start() { let that = this; this.timer = setInterval(repeat, 1000); function repeat() { console.log('re'); that.getLocation(); that.drawLine(); } cal = setInterval(() => { let dis, sum = 0; for (let i = 0; i < point.length - 1; i++) { dis = that.getDistance(point[i].latitude, point[i].longitude, point[i + 1].latitude, point[i + 1].longitude); sum += (+dis); } that.setData({ sum: that.format(sum.toFixed(2)) }) console.log(sum); }, 3000) that.countTime(); that.setData({ switch: !this.data.switch }) },
咱們看到start函數裏有兩個計時器,第一個用來持續獲取經緯度和畫線,第二個用來持續計算距離。涉及的函數是真的多,getLocation、drawLine、getDistance、format、countTime,再看看它們是怎麼實現的。
// 獲取經緯度 getLocation() { var latitude1, longitude1; wx.getLocation({ type: 'gcj02', success: res => { latitude1 = res.latitude; longitude1 = res.longitude; this.setData({ latitude: latitude1, longitude: longitude1 }) point.push({ latitude: latitude1, longitude: longitude1 }); console.log(point); } }) }, // 畫線 drawLine() { this.setData({ polyline: [{ points: point, color: "#1298db", width: 4 }] }) }, // 進行經緯度轉換爲距離的計算 rad(d) { // 經緯度轉換成三角函數中度分表形式 return d * Math.PI / 180.0; }, // 計算距離,參數分別爲第一點的緯度,經度;第二點的緯度,經度 getDistance(lat1, lng1, lat2, lng2) { let that = this; var radLat1 = that.rad(lat1); var radLat2 = that.rad(lat2); var a = radLat1 - radLat2; var b = that.rad(lng1) - that.rad(lng2); var s = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2) + Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2))); // 地球半徑 s = s * 6378.137; // 輸出爲千米 s = Math.round(s * 10000) / 10000; // s = s.toFixed(2); return s; }, format(str) { str = '' + str; return str.length === 1 ? `0.0${str}` : str; }, format1(str) { str = '' + str; return str.length === 1 ? `0${str}` : str; }, countTime() { this.tim = setInterval(() => { cur++; time.setMinutes(cur / 60); time.setSeconds(cur % 60); this.setData({ time: '00:' + this.format1(time.getMinutes()) + ':' + this.format1(time.getSeconds()) }) }, 1000) },
rad和getDistance函數是用來將經緯度距離換算成千米的,這固然得在網上找,不要問我爲何。countTime函數是用來計算時間的,經過format和format1函數把sum(千米)和time(時間)轉化爲你想要的格式。
end() { console.log("clear"); clearInterval(this.timer); clearInterval(cal); clearInterval(this.tim); this.setData({ switch: !this.data.switch }) }, stop() { let markers1 = []; let marker1 = { iconPath: "../../images/terminal.png", id: 1, width: 40, height: 40 }; clearInterval(this.timer); clearInterval(cal); clearInterval(this.tim); marker1.latitude = point[point.length - 1].latitude; marker1.longitude = point[point.length - 1].longitude; markers1.push(marker1); this.setData({ markers: this.data.markers.concat(markers1) }) app.globalData.sum = this.data.sum; // console.log(app.globalData.sum) point = []; cur = 0; // wx.navigateBack(); },
end函數用來暫停跑步,而stop函數則用來結束跑步並添加終點標記點。
最後要提一下的是想要在地圖上面顯示其餘的dom結構就必須得用cover-view標籤,並且裏面只能嵌套cover-view,因此用不了組件,這是最坑的,因而我便手寫了一個相似於上拉菜單的組件,你們看到項目展現裏的效果就知道寫的很是的粗糙就不貼代碼了。
寫的好像有點多了,就不說啥了。
再貼一次github地址吧:奔跑吧 (喜歡的就給個Star吧,看成是對做者學習的確定。)