preface

Learning has been going on for three months, and learning is always boring, especially when it comes to writing code. But, ever remember? When you were in high school, you could be happy that you solved a math problem that had been bothering you for days. In the world of programmers, I think, there is no greater joy than making a project.

Yes, after learning wechat mini program for nearly a month, I wrote a wechat mini program named Running Bar in imitation of APP-Sports World campus (it was named because I thought of running when I saw running, brother). In fact, I think the project I wrote can’t really be called a project, because it only realized a small part of the functions of the original APP. The writing is really simple. If you want to be perfect, you really need a team to write it. Well, without further ado, let’s get to the point.

Here is a github address:Run!It has all the source code.

Projects show

Use tools and prepare for development

  • Wechat developer tools +VScode
  • Vant-weapp+Tencent/weui-wxss
  • Iconfont + Online photo resource
  • Small program cloud development of database, storage and cloud functions
  • Wechat applet official documents +Vant official documents + Weui official documents
  • Netease Cloud Music NodeJS API

The project structure

General structure

Page path, page window representation, bottom TAB

"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": "Run!"."navigationBarTextStyle": "white"
  },
  "tabBar": {
    "color": "#B5B5B5"."selectedColor": "#1296DB"."list": [{"text": "Movement"."pagePath": "pages/run/run"."iconPath": "images/run.png"."selectedIconPath": "images/run-active.png"
      },
      {
        "text": "Dynamic"."pagePath": "pages/chat/chat"."iconPath": "images/chat.png"."selectedIconPath": "images/chat-active.png"
      },
      {
        "text": "I"."pagePath": "pages/mine/mine"."iconPath": "images/mine.png"."selectedIconPath": "images/mine-active.png"}},Copy the code

Have to say wechat small program tabBar is really good, do not have to write a JS logic can be easily implemented.

my

I’ll start with the simplest. My page hardly writes any JS (except to get mileage data) and just slices the page. (Note: Due to the large amount of code, I will not post it to explain the key points, as described below, if you want to see the whole code, please go to the github website posted above.)

In the head, I directly use the open data open-data label provided by wechat mini program to show the wechat avatar and nickname, which is much simpler than using Button.

<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">School of Software, Nanchang Guanglan Campus, East China Institute of Technology</view>
        </view>
        <view class="right">
            <view class="arrow"></view>
        </view>
    </view>
Copy the code

The arrow on the right, needless to say, is simple CSS.

.right .arrow{
    width: 30rpx;
    height: 30rpx;
    border-top: 1px solid #fff;
    border-right: 1px solid #fff;
    transform-origin: 0 0;
    transform: rotate(45deg);
}
Copy the code

Next, let’s focus on the section next to the header, which uses the elastic layout and involves the 1px problem.

<view class="hd-footer"> <view class="ft-left"> <view class="num">120.00</view> <view class=" STR "> < / view > < / view > < the view class = "ft - mid" > < the view class = "num" > {{num}} < / view > < the view class = "STR" > < text > has run mileage < / text > < / view > </view> <view class="ft-right"> <view class="num">0.00</view> <view class=" STR "> </view> </view> </view>Copy the code
.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; The 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; The transform: scale (0.5); transform-origin: 0 0; border-right: 1px solid #aaa; } .ft-right{ flex: 1; }Copy the code

Hd-footer set display: flex; Set all three of its children to Flex: 1; Make each child one third of the parent. We know that border can be set to a minimum of 1px, but we can achieve 0.5px by adding a pseudo-element, which is a CSS trick.

The body section below also uses an elastic layout, which I won’t repeat here. The.footer section at the bottom is a Cell component that uses the Vant component.

{ "usingComponents": { "van-cell": ".. /vant-weapp/dist/cell/index", "van-cell-group": ".. /vant-weapp/dist/cell-group/index" } }Copy the code
<view class="footer"> <van-cell group> <van-cell title=" contact customer service "icon="setting-o" IS-link url=""link-type="navigateTo"/> <van-cell title=" set "icon="service-o" IS-link border="true" URL ="" link-type="navigateTo"/> </van-cell group> </view>Copy the code

{{num}} uses the MVVM(data-driven page) idea. After completing a run, sum is set in this interface, and num is assigned to the page, and finally rendered to the page. When it comes to MVVM, I can’t help but praise it. It’s a great creation that lets us get rid of the tedious DOM manipulation.

Enclosing globalData = {sum: '0.00', baseUrl: 'http://neteasecloudmusicapi.zhaoboy.com'}Copy the code
    this.setData({
      num: app.globalData.sum
    })
Copy the code

dynamic

This interface involves more JS logic, I will focus on JS.The header still uses an elastic layout, just to mention the ellipsis for text beyond.

.hobby-title{
    font-size: 14px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
Copy the code

The body parts seem to have a lot of the same structure, so HERE I write a Speak component. There is nothing to say about the structure and style of the component, but how the three data on the component (time, text content, image) come from. And that’s what we’re going to talk about through position: fixed; To locate the button, click can jump to share page.

<view class="add" bindtap='share'> <image class="add-btn" src=".. /.. /images/add.png"></image> </view>Copy the code
share() { wx.navigateTo({ url: '.. /share/share', }) },Copy the code

It is also fairly simple to implement a page jump.A Button, an input box and a Weui Uploader upload component.

<view class="write"> <button type='primary' size='mini' class=' BTN 'bindtap='send'> </button> <input type='text' Placeholder = 'share... ' 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>Copy the code

Introduce weUI styles in app.wxSS.

@import './weui.wxss';
Copy the code
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(' upload successfully ',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); }})},Copy the code

Here we use several API of wechat small program. Set the data in data (image path) with the success callback of wx.chooseImage. With wx. Cloud. UploadFile upload picture resources to the development of the cloud storage, because every time can only upload one, so here USES a for loop. Preview images with wx.previewImage. You can go to the official documentation to see what each parameter means.

Get the input from the input box with bindConfirm =’complete’ on the input, and call the createDynamic cloud function with the send method bound to the Button.

// cloud = require('wx-server-sdk') const env = 'lvwei666-pv2y1' cloud.init() const db = Cloud.database ({env}) // cloudfunction 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() } }) }Copy the code

The createDynamic cloud function takes the content and image path from the call and adds the time data to the dynamic database. (Here really want to make fun of the cloud function debugging, once the error, every debugging should upload a cloud function, very time-consuming and troublesome.)

Once the data is created, let’s go get the data.

const db = wx.cloud.database()
const dynamic = db.collection('dynamic')
Copy the code
onShow: function () { let self = this; Wx.showloading ({title: 'loading'}); 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(); }})},Copy the code

In fact, I put the same code in the onPullDownRefresh page related event handler — Listen to the user pull down action, and get data when you pull down refresh.

Once I got the array, I did two things. One was to reverse the array, because the newly added record (dynamic) would be stored at the end of the dynamic database. The second is to do a string concatenation of the time data to get what you want.

Let’s move on to the body part of the dynamic page, the Speak component. Start by introducing the Speak component on this page.

    <view class="body">
      <block wx:for="{{every}}" wx:key="index">
        <speak createTime="{{item.createTime}}" content="{{item.content}}" images="{{item.images}}"></speak>
      </block>
    </view>
Copy the code
"usingComponents": { "speak": ".. /.. /components/speak/speak" },Copy the code

The for loop is also used here to render the Speak component because multiple records can be added to the Dynamic collection, rendering one record for each Speak component. How does the component get the data on the page? Take a look at the following code.

  properties: {
    createTime: {
      type: String,
      value: ''
    },
    content: {
      type: String,
      value: ''
    },
    images: {
      type: Array,
      value: []
    }
  },
Copy the code

Properties Component’s property list – This allows the component to retrieve data from the page. When you have the data, just dig a hole in the HTML ({{}}) and put the data in it.

<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>Copy the code
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 }) } }Copy the code

Use the “like” method to control the true or false values of the data to “like” and “unlike”. Wx. previewImage is also used to preview the image.

movement

Here is not the texture, the effect we can according to what I said to the above project to show. The first thing you can see on this page is a sliding screen using Vant’s Van-Tab component. The title and content of each screen are stored in the navData database. Clicking on the term goal brings up a pull-up menu using Vant’s Van-Action-Sheet component. References to components and data operations to get to the database were covered earlier, but not here.

Let’s talk about the CSS animation of the start button in the middle. Compared to the effect in the original APP, my animation is really much worse. The original APP looks like water droplets.

.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%); Opacity: 0.2; Translate the transform: scale (1.7) (50%, 50%); } 100% { opacity: 0; Translate the transform: scale (1.7) (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); }}Copy the code

Anim changes its opacity as it zooms in, while.start shrinks it a bit and then immediately restores it.

Next, click the music button on the page to enter the music page.

wx.request({ url: 'http://neteasecloudmusicapi.zhaoboy.com/top/list', data: { idx: 1 }, success: Res => {console.log(' hot songs ', res); const songLists = res.data.playlist.tracks; this.setData({ songLists }); wx.hideLoading(); }})Copy the code

Here, wx.request is used to initiate a request to obtain the hot song information of netease Cloud music hot song List interface and render the data to the page through the for loop. Here, what is IDx: 1 in data? Call this interface, pass in the number IDX, can get different charts) this is the official description, 0 for new songs, 1 for hot songs, etc.

    <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>
Copy the code
toPlayAudio(e) { const id = e.currentTarget.dataset.id; wx.navigateTo({ url: `.. /play/play? id=${id}` }) },Copy the code

Note that each.item has data-id=”{{item.id}}”, so that you know what song is playing when you jump to the music page.

Play page interface and style are very simple, let’s see how to implement the music.

onLoad: function (options) { console.log(options); Wx.setnavigationbarcolor ({frontColor: '# FFFFFF ', backgroundColor: {frontColor: '# FFFFFF ', backgroundColor: '#3daed9', }) const id = options.id wx.request({ url: app.globalData.baseUrl + '/song/url', data: { id: id }, success: Res => {console.log(' song details ', 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(' song info ', 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 }) }) },Copy the code

Through the options to jump when the page is incoming id, reoccupy wx. Request to obtain the Url of the songs and calls wx. GetBackgroundAudioManager background audio playback management play music, and with wx. Request for details on the song.

<view> <button type='primary' bindtap='togglePlayStatus'> {{isPlay ? </button> </view>Copy the code
  togglePlayStatus() {
    const bgAu = wx.getBackgroundAudioManager();
    if (this.data.isPlay) {
      bgAu.pause();
      this.setData({
        isPlay: false
      })
    } else {
      bgAu.play();
      this.setData({
        isPlay: true
      })
    }
  },
Copy the code

TogglePlayStatus method to control the music play and pause.

Finally, there is the running part, in which the logic of calculating the distance is the most difficult part to solve in the whole project. I modified it for many times, so that it could calculate the number of kilometers run rough. I also shed a lot of sweat for this.

First, Tencent Map was introduced.

<map id='myMap' scale='{{scale}}' latitude='{{latitude}}' longitude='{{longitude}}' polyline="{{polyline}}" show-location markers='{{markers}}'></map>
Copy the code

The map’s scale level, latitude and longitude, polyline route, show-location shows the current entry point with its orientation, and other attributes that can be used to track the map.

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: 'get location failed ', icon:' None '})}})}, onReady() {this.mapctx = wx.createmapContext ('myMap'); this.start(); },Copy the code

When the page loads, we get the current geographic location via wx.getLocation and add a starting point as a marker. We call the start function when the page is rendered for the first time. Let’s see what start does.

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 }) },Copy the code

We see that there are two timers in the start function. The first one is used to continuously get latitude and longitude and draw lines, and the second one is used to continuously calculate the distance. There are really a lot of functions involved, getLocation, drawLine, getDistance, format, countTime, and see how they are implemented.

// Get longitude () {var latitude1, longitude ude1; 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}]})}, // return d * math.pi / 180.0; }, // Calculate the distance with the latitude and longitude of the first point respectively; 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))); // The radius of the earth is 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) },Copy the code

The rad and getDistance functions are used to convert latitude and longitude distances to kilometers, which of course has to be found online, don’t ask me why. The countTime function is used to count the time. Convert sum(km) and time(time) to whatever format you want using the format and format1 functions.

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(); },Copy the code

The end function is used to pause the run, and the stop function is used to end the run and add an endpoint marker.

The last thing I want to mention is that to display any other DOM structure on the map you have to use a cover-view tag, and you can only have a cover-view nested inside of it, so you can’t use a component, which is the worst thing, so I hand-wrote a component that looked like a pull-up menu, If you look at the presentation of the project and you see that it’s very rough so I don’t want to post the code.

conclusion

Write seems a little too much, will not say what.

Post the Github address again: Run (if you like, give a Star, as a recognition of the author’s learning.)