学习(入坑)前端想来已有三个月了,学习老是枯燥乏味的,写代码更是如此。可是,曾记否?高中时候的你能够由于解出了一道困扰了你几日的数学题而高兴一成天。在程序员的世界里,我想,没有比作出一个项目更开心的了。javascript
是的,此前微信小程序学习了近一个月后,仿着APP-运动世界校园写了一个微信小程序奔跑吧(取名为这个是由于看到跑步第一时间就想到了奔跑吧,兄弟)。其实我以为本身写的这个项目着实不能称得上是一个项目,由于它只实现了原APP的一小部分功能,写得实在是简陋,要想完美,果真仍是得一个团队写。好了,话很少说,咱们言归正传。css
"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逻辑就能够轻松实现。html
我先从最简单的开始提及吧,个人页面几乎没有写任何js(除了获取已跑里程数据),都只是在切页面。(注:因为代码量太大,我就不一一贴出来阐述了,就讲讲关键点,如下叙述皆如此,想看所有代码的能够去上面贴出来的github网址上看)前端
<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.java
.right .arrow{
width: 30rpx;
height: 30rpx;
border-top: 1px solid #fff;
border-right: 1px solid #fff;
transform-origin: 0 0;
transform: rotate(45deg);
}
复制代码
接着咱们来关注一下挨着头部下面的那部分,这里使用了弹性布局和涉及到1px问题。git
<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单元格组件。github
{
"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',
})
},
复制代码
这里实现页面的跳转也是至关的简单。
<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能够预览图片。具体每一个参数是什么意思,你们能够去官方文档上看。
// 云函数入口文件
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吧,看成是对做者学习的确定。)