Skip to main content

Campus Social Mini Program Development Practice

Overview

This article introduces how to leverage cloud development capabilities to rapidly build a campus social mini program. It demonstrates and explains the features of the Draw a Match and My Notes pages.

Note

The source code materials involved in this example tutorial have all been properly authorized.

Preparations

  1. Register Tencent Cloud.
  2. Enabled CloudBase for the mini program. For details, see Quick Start for Mini Program Side.

Operation Procedure

The specific operation procedure can be divided into the following 6 steps. For more details, refer to the sample.

Database Table Design

To decouple user login from the note data they place, create a users_school table to store WeChat Mini Program users. For all notes placed by users, create a body_girl_yun table. To allow users to manage drawn notes, create a body_girl_yun_put table to store all notes drawn by users.

Open WeChat Developer Tools, click Cloud Development to enter the Cloud Development Console > Database page. Click the New button to create three collections: users_school, body_girl_yun, and body_girl_yun_put, as shown in the figure below:

Step 1: Implement the Login Function and Homepage Code

This article primarily focuses on the index.wxml and index.wxss of the homepage. For more details on the index frontend page code, refer to index frontend page and index page styles.

Mini Program Configuration and Page Creation

  1. In app.json, configure the paths for two pages:
    "pages": [
    "pages/index/index",
    "pages/history/history"
    ]
    After configuration, refresh the developer tools, and it will generate index and history folders under the pages folder, along with corresponding .js, .wxml, .wxss, and .json files.
  2. In app.json, set the tabbar position at the top of the page and configure its content.
    "tabBar": {
    "color": "black",
    "selectedColor": "#D00000",
    "position": "top",
    "list": [{
    "pagePath": "pages/index/index",
    "text": "Draw a Date"
    },
    {
    "pagePath": "pages/history/history",
    "text": "My Notes"
    }
    ]
    },
3. In app.js, add global data to store the current user's openId:
```js
this.globalData = {
openId: ''
}

Implementing User Login Function and Recording openId

  1. Right-click the current environment folder, click New Node.js Cloud Function, and name the file login_yun.
  2. Enter the index.js file in the login_yun directory and configure the environment within the cloud.init() function.
    cloud.init({
    // env parameter description:
    // The env parameter determines which cloud environment's resources subsequent cloud development calls (wx.cloud.xxx) initiated by the Mini Program will be directed to by default.
    // Please enter the environment ID here. You can view the environment ID in the cloud console.
    // If not filled, the default environment (the first created environment) will be used
    // env: 'my-env-id',
    env: "cloud1234567890XXXXXXX",
    });
  3. Perform openId operations in the cloud function entry function
exports.main = async (event, context) => {
const wxContext = cloud.getWXContext();

const openId = md5(wxContext.OPENID);

// Check if there is any data under this openid
let user_data = await db
.collection("users_school")
.where({
openId: openId,
})
.get();

// If the user does not exist, add the openId to the database
if (user_data.data.length === 0) {
try {
let data_add = await db.collection("users_school").add({
data: {
openId: openId,
},
});
return {
data_add,
openId,
status: 202, // Newly added data
};
} catch (e) {
console.error(e);
}
// If present, return directly.
} else {
return {
user_data,
status: 200, // User already exists
};
}
};
  1. Then right-click the login_yun folder and click Upload and Deploy: Cloud-based Dependency Installation to complete the cloud function deployment.
  2. The cloud function for login functionality is invoked when the index page loads index.js, specifically within the onload lifecycle hook.
const that = this;
if (wx.getUserProfile) {
that.setData({
canIUseGetUserProfile: false,
});
}
wx.cloud
.callFunction({
name: "login_yun", // Invoke cloud function
})
.then((res) => {
console.log(res);
if (res.result.status === 200) {
// Store in the global globalData for later use
app.globalData.openId = res.result.user_data.data[0].openId;
} else {
app.globalData.openId = res.result.openId;
}
});

Developing the index Front-end Page

  1. The top carousel can be directly implemented using the swiper and swiper-item tags provided by the Mini Program.
    <view class="swiper_view">
    <swiper
    autoplay="true"
    interval="3000"
    duration="500"
    circular="true"
    class="swiper"
    >
    <swiper-item>
    <image mode="widthFix" src="../../images/_2.jpg"></image>
    </swiper-item>
    <swiper-item>
    <image mode="widthFix" src="../../images/_1.png"></image>
    </swiper-item>
    </swiper>
    <!-- scaleToFill -->
    </view>
2. The middle box section can be created using basic tags, images, and CSS styles. Clicking "Put In" or "Take Out" will trigger an information collection pop-up. Use bindtap to bind click events and handle them in index.js.
```html
<view class="body_bottom">
<view class="body_bottom_put" bindtap="putBody">Place a boy's note</view>
<view class="body_bottom_out" bindtap="outBody">Take out a boy's note</view>
</view>
  1. The black overlay layer is implemented using a view tag combined with CSS styles. Clicking the overlay layer triggers cancelHide to hide it.
    <view
    class="hide"
    wx:if="{{putBodyMask || outBodyMask || putGirlMask || outGirlMask || xuzhiMask || xieyiMask}}"
    bindtap="cancelHide"
    ></view>
    CSS covers the entire screen; simply add color and opacity.
    /_ Mask layer _/ .hide {
    width: 100vw;
    height: 100vh;
    background-color: black;
    opacity: 0.5;
    position: fixed;
    top: 0vw;
    left: 0vh;
    }
4. Popup for putting in and taking out boys' and girls' notes, with a picker for selecting different schools by category.
```html
<picker bindchange="bindSchoolChangePut" value="{{indexBody4}}" range="{{arrayBody4}}" class="out_body_content_2_picker">
<view> - {{arrayBody4[indexBody4]}} -
</view>
</picker>
  1. After refining the index front-end page and styles, the final result is as shown in the figure below:

Step 2: Homepage Logic Processing

This article primarily focuses on the index.js of the homepage. For more details, refer to Homepage Logic Code.

  1. In the index.js page, within the onload lifecycle, call the login_yun cloud function to implement the login operation.
wx.cloud
.callFunction({
name: "login_yun",
})
.then((res) => {
console.log(res);
if (res.result.status === 200) {
app.globalData.openId = res.result.user_data.data[0].openId;
} else {
app.globalData.openId = res.result.openId;
}
});
  1. In the index.js page, add data to store the required data.
  data: {
// Mask layer flag
putBodyMask: false,
putGirlMask: false,
xuzhiMask: true,
// Extracted mask layer
outBodyMask: false,
outGirlMask: false,
// Dating Declaration
textareaBody: '',
textareaGirl: '',
// qq WeChat ID
numberBody: '',
numberGirl: '',
// src for uploaded image preview
srcListBody: [],
srcListGirl: [],

// Placed School
arrayBody4: ["Henan Polytechnic University", "Jiaozuo University", "Jiaozuo Teachers College"],
indexBody4: 0,
// Note Life
arrayBody2: ["Destroyed when drawn once", "Destroyed when drawn twice", "Destroyed when drawn three times"],
indexBody2: 0,
arrayBody3: ["Henan Polytechnic University", "Jiaozuo University", "Jiaozuo Teachers College"],
indexBody3: 0,

// Placed School
arrayGirl4: ["Henan Polytechnic University", "Jiaozuo University", "Jiaozuo Teachers College"],
indexGirl4: 0
// Note Life
arrayGirl2: ["Destroyed when drawn once", "Destroyed when drawn twice", "Destroyed when drawn three times"],
indexGirl2: 0,
arrayGirl3: ["Henan Polytechnic University", "Jiaozuo University", "Jiaozuo Teachers College"],
indexGirl3: 0,
// Add image icon
addMask: true
},
  1. School selection logic.
    1. In the picker, bindchange="bindSchoolChangePut" triggers the school change event to select the corresponding school.
    2. e.detail.value is used to obtain the list index of the school bound in data.
// School selection during placement
bindSchoolChangePut: function(e){
this.setData({
indexBody4: parseInt(e.detail.value)
})
}
  1. When clicking the "Confirm Placement" button, the corresponding surePutBodyBtns event is triggered. Validation is then performed to enforce the length limit of the social declaration.
if (that.data.textareaBody.length < 20) {
return wx.showToast({
title: "Dating Declaration Too Short",
icon: "error",
duration: 2000,
});
}
  1. Regular expression matching for WeChat ID upload
if (
!/^(((13[0-9]{1})|(15[0-9]{1})|(18[0-9]{1})|(17[0-9]{1}))+\d{8})$/.test(
that.data.numberBody
) &&
!/^[a-zA-Z]([-_a-zA-Z0-9]{6,20})$/.test(that.data.numberBody)
) {
return wx.showToast({
title: "WeChat ID Format Error",
icon: "error",
duration: 2000,
});
}
Note

WeChat officially defined ID rules:

  • Can contain 6-20 letters, digits, underscores, and hyphens.
  • Must start with a letter (case-insensitive).
  • Does not support setting Chinese.
  1. Implement local image selection with a limit of one image per upload, impose image size restrictions, and randomly configure the cloudPath as the storage path in cloud storage.
// Select local image
chooseImgGirl: function(){
const that = this
wx.chooseImage({
count: 5,
sizeType: ['original', 'compressed'],
sourceType: ['album', 'camera'],
success (res) {
// tempFilePath can be used as the src attribute of an img tag to display the image
if(res.tempFiles[0].size > 1200000){
return wx.showToast({
title: 'Image Too Large',
icon: 'error',
duration: 2000
})
}

const filePath = res.tempFilePaths[0]
let timeStamp = Date.parse(new Date());
const cloudPath = "image/" + timeStamp + filePath.match(/\.[^.]+?$/)[0]
that.pageData.filePath = filePath
that.pageData.cloudPath = cloudPath
that.setData({
srcListGirl: res.tempFilePaths
})
}
})
}
  1. Click the life card, and the selected value will correspond to the life field in the database.

  1. After clicking Confirm to place, the corresponding logic is to first upload the image, then upload information such as location and weChatId to the body_girl_yun table. Upon successful upload, it will jump to the history page.
// api for uploading images: first upload the images to cloud storage
wx.cloud.uploadFile({
cloudPath,
filePath,
success: function(res){
wx.hideLoading()
wx.showLoading({
title: 'Information Uploading',
mask: true
})
// Upload data after uploading images
db.collection("body_girl_yun").add({
data: {
location: that.data.indexGirl4,
weChatId: that.data.numberGirl,
picture: res.fileID,
life: parseInt(that.data.indexGirl2) + 1,
des: that.data.textareaGirl,
sex: 2,
openId: app.globalData.openId,
time: new Date().getTime()
}
}).then(result => {
wx.hideLoading()

that.pageData = {
filePath: '',
cloudPath: ''
}
that.setData({
putGirlMask: false,
srcListGirl: [],
textareaGirl: '',
numberGirl: ''
})
// After the upload completes, go to the history page
wx.switchTab({
url: '/pages/history/history',
success: res=>{
wx.showToast({
title: 'Note Successfully Placed',
icon: 'success'
})
},
fail: err => {
wx.showToast({
title: err,
icon: 'error'
})
}
})
}
})
  1. At the bottom of the page, there are two click events: Contact Customer Service and uu Notice. Contact Customer Service is primarily implemented in index.wxml via the mini program's open-type="contact".
<!-- Bottom Notice -->
<view class="bottom_view">
<button bindtap="xuzhi">UU Notice</button>
<!-- <text class="heng">|</text> -->
<button bindcontact="contact" open-type="contact">Having issues? Contact Customer Service</button>
</view>

Step 3: Implementing the Note Drawing Function

This article continues to focus on explaining the note drawing function in the homepage's index.js. For more details, refer to Homepage Logic Code.

  1. Random draw can be implemented using Developer Resources provided by WeChat Open Documentation to perform random query extraction. The extraction condition is "gender, school, and HP must be greater than 0".
// Random draw
db.collection("body_girl_yun")
.aggregate()
.match({
sex: 1,
location: parseInt(that.data.indexBody3),
life: _.gt(0), // HP must be greater than 0
})
.sample({ size: 1 })
.end();
  1. If drawn, process it; if not, prompt the user and reduce HP by 1 during processing.
// If the database has no notes from boys in this school,
if (res.list.length == 0) {
return wx.showToast({
title: "No Notes Available in This School",
icon: "error",
mask: true,
});
}
// If a note is drawn, perform the operation
// Decrease HP by 1. Notes with 0 HP will not be drawn.
db.collection("body_girl_yun")
.where({
_id: res.list[0]._id,
})
.update({
data: {
life: parseInt(res.list[0].life) - 1,
},
})
.then((resultUpdate) => {
wx.showToast({
title: "Draw Successful",
icon: "success",
mask: true,
});
})
.catch((err) => {
wx.showToast({
title: err,
icon: "error",
});
});
  1. Write the data to the body_girl_yun_put table.
// and add the data to body_girl_yun_put
db.collection("body_girl_yun_put")
.add({
data: {
picture: res.list[0].picture,
des: res.list[0].des,
location: res.list[0].location,
sex: res.list[0].sex,
weChatId: res.list[0].weChatId,
openId: app.globalData.openId,
time: new Date().getTime(),
},
})
.then((resultAdd) => {})
.catch((err) => {
wx.showToast({
title: err,
icon: "error",
});
});
  1. If the data is successfully stored, navigate to the history page.
wx.switchTab({
url: "/pages/history/history",
success: (res) => {
wx.showToast({
title: "Note Draw Successful",
icon: "success",
});
},
fail: (err) => {},
});
that.setData({
outBodyMask: false,
});
  1. Add toast and loading effects with wx.showLoading and wx.hideLoading(), along with basic error handling.
// Confirm to take out a boy's note
sureOutBodyBtn: function(){
const that = this

wx.showLoading({
title: 'Random Draw in Progress',
mask: true
})
// Random draw
db.collection('body_girl_yun').aggregate().match({
sex: 1,
location: parseInt(that.data.indexBody3),
life: _.gt(0) // HP must be greater than 0
}).sample({ size: 1}).end()
.then(res => {
wx.hideLoading()
// If the database has no notes from boys in this school,
if(res.list.length == 0){
return wx.showToast({
title: 'No Notes Available in This School',
icon: 'error',
mask: true
})
}

console.log(res)
// Decrease HP by one. Notes with 0 HP will not be drawn.
db.collection('body_girl_yun').where({
_id: res.list[0]._id
}).update({
data: {
life: parseInt(res.list[0].life) - 1
}
}).then( resultUpdate => {
wx.showToast({
title: 'Draw Successful',
icon: 'success',
mask: true
})
}).catch(err=>{
wx.showToast({
title: err,
icon: 'error'
})
})


// and add the data to body_girl_yun_put
db.collection('body_girl_yun_put').add({
data: {
picture: res.list[0].picture,
des: res.list[0].des,
location: res.list[0].location,
sex: res.list[0].sex,
weChatId: res.list[0].weChatId,
openId: app.globalData.openId,
time: new Date().getTime()
}
}).then( resultAdd => {

wx.switchTab({
url: '/pages/history/history',
success: res=>{
wx.showToast({
title: 'Note Draw Successful',
icon: 'success'
})
},
fail: err => {
wx.showToast({
title: err,
icon: 'error'
})
}
})

// console.log("Data add", resultAdd)
that.setData({
outBodyMask: false
})
}).catch(err=>{
wx.showToast({
title: err,
icon: 'error'
})
})
})
}

Step 4: My Notes Page Design

This article primarily focuses on the history.wxml frontend page for my notes. For more details, refer to My Notes Frontend Code.

My Placed Notes Page Design

  1. At the top, "My Placed Notes" and "My Drawn Notes" switch classes by changing the active value.
<view class="top_title">
<text class="{{active === true ? 'on': ''}}" bindtap="inBtn"
>My Placed Notes</text
>
<text class="{{active === true ? '': 'on'}}" bindtap="outBtn"
>My Drawn Notes</text
>
</view>
  1. The frontend uses a for loop to display each data item. dataList is the data retrieved from the database, with wx:key specified.
<view class="put" wx:for="{{dataListPut}}" wx:key="_id">
<view class="putTop">
<view class="putTopImg">
<image src="{{item.picture}}"></image>
</view>
<view class="putTopDes"> <text>Dating Declaration:</text>{{item.des}} </view>
</view>
</view>

When deleting, the _id of the data is required, so parameters need to be passed via JS: data-id="{{item._id}}".

<view class="putBottom">
<text>{{schoolList[item.location]}}</text>
<text>{{item.sex == 1? 'Male': 'Female'}}</text>
<text class="putBottomDelete" bindtap="deletePut" data-id="{{item._id}}"
>Delete!</text
>
</view>

The passed parameter is obtained via e.target.dataset.id.

deletePut: function (e) {
const that = this
const _id = e.target.dataset.id
}
  1. Display the dating declaration in three lines; overflow content is shown with an ellipsis.
.outTopDes_1 {
height: 15vh;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
word-break: break-all;
}
  1. The frontend checks if data exists; if no data is present, it displays "Empty".
<view
class="kong"
wx:if="{{active === true? true: false}}"
hidden="{{dataListPut.length == 0 ? false: true}}"
>Empty</view
>
<view class="kong" wx:else hidden="{{dataListOut.length == 0 ? false: true}}"
>Empty</view
>

My Drarawn Notes Page Design

  1. Basically the same as the "My Placed Notes" page. Adds a display area for the WeChat ID and reduces the dating declaration to two lines. The WeChat ID is center-aligned:
.outTopDes_2 {
color: rgb(62, 88, 238);
text-align: center;
font-size: 35rpx;
}
  1. User-uploaded images can be obtained on the Cloud Development Console > Storage > Storage Management page.

Step 5: My Notes Page Logic Processing

This article primarily focuses on the history.js file for my notes. For more details, refer to My Notes Logic Code.

  1. data design.
{
"active": true, // Used to change the style of the currently selected item
"dataListPut": [], // Used to store data for the put page
"dataListOut": [],
"schoolList": ["Henan Polytechnic University", "Henan Normal University", "Jiaozuo Teachers College"]
}
  1. onload lifecycle design.
  • Since this page does not have the function to obtain the user's openId, it first needs to check whether the user's openId has been obtained. If not, it directly redirects to the index page to obtain the openId and displays an appropriate error message.
if (app.globalData.openId == "") {
wx.switchTab({
url: "/pages/index/index",
success: (res) => {},
fail: (err) => {
wx.showToast({
title: err,
icon: "error",
});
},
});
}
  • Call the data function for requesting "My Placed Notes"
that.getPutData();
  1. Query via the Mini Program official documentation using the user's openId to retrieve data, then update the dataListPut for page rendering through that.setData.
    Query MethodPurpose
    limit To limit the number of data entries retrieved
    orderBy Data Sorting
// Get data put
getPutData: function(e){
const that = this
db.collection('body_girl_yun').where({
openId: app.globalData.openId
}).limit(10).orderBy('time', 'desc').get().then(res => {
console.log(res)
if(res.data.length == 0){
that.setData({
dataListPut: res.data
})
return wx.showToast({
title: 'No data',
icon: 'none'
})
}
that.setData({
dataListPut: res.data
})
}).catch(err=>{
wx.showToast({
title: 'Data load failed',
icon: 'error'
})
})
},
  1. Logic implementation for deleting notes.
  • Implement delete confirmation.
wx.showModal({
title: 'Prompt',
// content: 'Confirm deletion of the note?',
content: 'After deletion, the Friends Hall will become invisible. Confirm?',
success (res) {

})
  • If the user confirms deletion, use the remove operation with the _id passed from the frontend (which corresponds to a unique data entry) to perform deletion, including basic error handling. Otherwise, prompt that the deletion was canceled.
 if (res.confirm) {
db.collection("body_girl_yun").where({
_id: _id
}).remove().then(res => {
wx.hideLoading()
if(res.errMsg == 'collection.remove:ok'){
that.getPutData()
}else{
wx.showToast({
title: 'Deletion Failed',
icon: 'error',
mask: true
})
}
}).catch(
console.error
)
} else if (res.cancel) {
wx.showToast({
title: 'Deletion Canceled',
icon: 'error',
mask: true
})
}
}
  1. Implement pagination queries combined with loading data on scroll-to-bottom.
  • Trigger the event via onReachBottom.
/**
* Handler for the page pull-to-bottom event
*/
onReachBottom: function () {
if(this.data.active==true){
this.getPutDataBottom()
}else{
this.getOutDataBottom()
}
},
  • Using the pagePut parameter in skip(pagePut * 10).limit(10) to track the page number.
  • Using concat to concatenate old and new data, then updating the page.
// Pull-to-bottom event
getPutDataBottom: function(){
const that = this
let pagePut = that.data.pagePut + 1

db.collection('body_girl_yun').where({
openId: app.globalData.openId
}).skip(pagePut * 10).limit(10).orderBy('time', 'desc').get().then(res => {
console.log(res)
wx.hideLoading()
// If there is still data
if(res.data.length > 0){
// Using concat to concatenate data
let all_data = that.data.dataListPut.concat(res.data)
that.setData({
dataListPut: all_data,
pagePut: pagePut
})
}else{
wx.hideLoading()
wx.showToast({
title: 'No more data',
icon: 'none'
})
}

})
},
  1. Implement pull-to-refresh.
  • First, configure enablePullDownRefresh in the app.json file to enable the pull-to-refresh feature.
"window": {
"backgroundColor": "#FFCCFF",
"backgroundTextStyle": "light",
"navigationBarBackgroundColor": "#FFCCFF",
"navigationBarTitleText": "Campus Transport U",
"navigationBarTextStyle": "black",
"enablePullDownRefresh": true
},
  • The onPullDownRefresh function, triggered by the user's pull-down action, requests and re-fetches data.
/**
* Page event handler--Listens for user pull-down actions
*/
onPullDownRefresh: function () {
if(this.data.active == true){
this.getPutData("pull")
}else{
this.getOutData("pull")
}
},

Step 6: Detail Supplementation

  1. Add the showLoading effect for note deletion.
wx.showLoading({
title: "Deleting",
mask: true,
});

Also, fetching put data, showLoading, etc.

  1. Add a showModal prompt.
wx.showModal({
title: 'Prompt',
content: 'Confirm deletion of the note?',
success (res) {
})
})

At this point, all features of the mini-program have been fully implemented. For more details, refer to the sample code.