跳到主要内容

校园交友小程序开发实践

概述

本文介绍如何用云开发相关能力,快速完成一个校园交友小程序的开发。并围绕抽个对象我的纸条两个页面进行功能展示和讲解。

说明

本实例教程所涉及到的相关源码材料,均已得到相应授权。

准备工作

  1. 注册腾讯云
  2. 开通了云开发的小程序,详情请参见 小程序端快速入门

操作流程

具体操作流程可分为以下 6 步。更多详情可参见 示例

数据库表的设计

将用户的登录与用户对应放入的纸条数据分离开,就要建个users_school表来存储微信小程序用户,所有用户放入的纸条再建一个body_girl_yun表,为了便于让用户管理抽出的纸条就再建一个body_girl_yun_put表用来存储所有用户抽出的纸条。

打开微信开发者工具,单击云开发进入云开发控制台 > 数据库页面。单击新建按钮创建 users_schoolbody_girl_yunbody_girl_yun_put 三个集合,如下图所示:

步骤 1:编写登录功能及主页代码

本文主要围绕主页的 index.wxml 和 index.wxss 进行讲解,更多 index 前端页面代码细节可参见 index 前端页面index 页面样式

小程序配置及页面创建

  1. app.json 中,配置两个页面的路径:
    "pages": [
    "pages/index/index",
    "pages/history/history"
    ]
    配置好后刷新开发者工具,就会在页面 pages 文件夹下生成 index,history 文件夹以及对应的 .js.wxml.wxss.json 文件。
  2. 在 app.json 中设置 tabbar 的位置,在页面顶部以及设置里面的内容:
    "tabBar": {
    "color": "black",
    "selectedColor": "#D00000",
    "position": "top",
    "list": [{
    "pagePath": "pages/index/index",
    "text": "抽个对象"
    },
    {
    "pagePath": "pages/history/history",
    "text": "我的纸条"
    }
    ]
    },
3. 在 app.js 里新增全局的数据,用来存放当前用户的 openId:
```js
this.globalData = {
openId: ''
}

实现用户登录功能及 openId 的记录

  1. 右击当前环境文件夹,单击新建 Node.js 云函数,并将文件命名为 login_yun。
  2. 进入 login_yun 目录下的 index.js 文件,在 cloud.init() 函数内配置环境。
    cloud.init({
    // env 参数说明:
    // env 参数决定接下来小程序发起的云开发调用(wx.cloud.xxx)会默认请求到哪个云环境的资源
    // 此处请填入环境 ID, 环境 ID 可打开云控制台查看
    // 如不填则使用默认环境(第一个创建的环境)
    // env: 'my-env-id',
    env: "cloud1234567890XXXXXXX",
    });
  3. 在云函数入口函数里进行 openId 的操作
exports.main = async (event, context) => {
const wxContext = cloud.getWXContext();

const openId = md5(wxContext.OPENID);

// 查询这个openid下有无数据
let user_data = await db
.collection("users_school")
.where({
openId: openId,
})
.get();

// 没有此用户则将openId添加到数据库
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, // 新添加的数据
};
} catch (e) {
console.error(e);
}
// 有了则直接返回
} else {
return {
user_data,
status: 200, // 已经有了用户
};
}
};
  1. 然后右击 login_yun 文件夹,单击上传并部署:云端安装依赖,即完成了云函数的编写。
  2. 登录功能的云函数的调用,是在 index 页面加载 index.js 时进行调用,且在 onload 的生命周期中。
const that = this;
if (wx.getUserProfile) {
that.setData({
canIUseGetUserProfile: false,
});
}
wx.cloud
.callFunction({
name: "login_yun", // 调用云函数
})
.then((res) => {
console.log(res);
if (res.result.status === 200) {
// 存到全局的globalData里,便于后续使用
app.globalData.openId = res.result.user_data.data[0].openId;
} else {
app.globalData.openId = res.result.openId;
}
});

编写 index 前端页面

  1. 顶部的轮播图,直接可以用小程序提供的 swiper 加上 swiper-item 标签来实现。
    <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. 中间的盒子部分用基本的标签、图片和 CSS 样式来编写。单击放入与抽取都会跳出一个收集信息的框,使用 bindtap 来绑定点击的事件,并在 index.js 里处理对应的事件。
```html
<view class="body_bottom">
<view class="body_bottom_put" bindtap="putBody">放入一张男生纸条</view>
<view class="body_bottom_out" bindtap="outBody">抽取一张男生纸条</view>
</view>
  1. 黑遮罩层用一个 view 标签,结合 CSS 的样式来完成。点击遮罩层对应 cancelHide 来隐藏遮罩层。
    <view
    class="hide"
    wx:if="{{putBodyMask || outBodyMask || putGirlMask || outGirlMask || xuzhiMask || xieyiMask}}"
    bindtap="cancelHide"
    ></view>
    CSS 占满整个屏幕,加上颜色与透明度即可。
    /_ 遮罩层 _/ .hide {
    width: 100vw;
    height: 100vh;
    background-color: black;
    opacity: 0.5;
    position: fixed;
    top: 0vw;
    left: 0vh;
    }
4. 男女生放入与抽取的弹出框,picker 用来选择分类的不同的学校。
```html
<picker bindchange="bindSchoolChangePut" value="{{indexBody4}}" range="{{arrayBody4}}" class="out_body_content_2_picker">
<view> - {{arrayBody4[indexBody4]}} -
</view>
</picker>
  1. 将 index 前端页面和样式细化后,最后效果如下图所示:

步骤 2:主页逻辑处理

本文主要围绕主页的 index.js 进行讲解,更多详情请参见 主页逻辑代码

  1. 进入 index.js 页面,在 onload 的生命周期里,调用 login_yun 云函数实现登录的操作。
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. 在 index.js 页面,添加 data 存放所需要的数据。
  data: {
// 遮罩层标志
putBodyMask: false,
putGirlMask: false,
xuzhiMask: true,
// 抽出的遮罩层
outBodyMask: false,
outGirlMask: false,
// 交友宣言
textareaBody: '',
textareaGirl: '',
// qq微信号
numberBody: '',
numberGirl: '',
// 上传图片预览的src
srcListBody: [],
srcListGirl: [],

// 放入的学校
arrayBody4: ["河南理工大学", "焦作大学", "焦作师范"],
indexBody4: 0,
// 纸条的生命
arrayBody2: ["被抽中一次销毁", "被抽中两次销毁", "被抽中三次销毁"],
indexBody2: 0,
arrayBody3: ["河南理工大学", "焦作大学", "焦作师范"],
indexBody3: 0,

// 放入的学校
arrayGirl4: ["河南理工大学", "焦作大学", "焦作师范"],
indexGirl4: 0
// 纸条的生命
arrayGirl2: ["被抽中一次销毁", "被抽中两次销毁", "被抽中三次销毁"],
indexGirl2: 0,
arrayGirl3: ["河南理工大学", "焦作大学", "焦作师范"],
indexGirl3: 0,
// 添加图片的加号
addMask: true
},
  1. 学校选择的逻辑。
    1. picker 中 bindchange="bindSchoolChangePut",通过 bindSchoolChangePut 来触发学校改变的事件,选择对应的学校。
    2. e.detail.value 来获取在 data 中绑定的学校的列表索引。
// 放入时,学校的选择
bindSchoolChangePut: function(e){
this.setData({
indexBody4: parseInt(e.detail.value)
})
}
  1. 在点击确认放入,触发对应的 surePutBodyBtns 事件,再进行判断,进而实现限制交友宣言长度。
if (that.data.textareaBody.length < 20) {
return wx.showToast({
title: "交友宣言太短",
icon: "error",
duration: 2000,
});
}
  1. 上传微信号的正则匹配。
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: "微信号格式错误",
icon: "error",
duration: 2000,
});
}
提示

微信官方定义的微信号规则:

  • 可使用 6-20 个字母、数字、下划线和减号。
  • 必须以字母开头(字母不区分大小写)。
  • 不支持设置中文。
  1. 实现本地图片的选择,一次上传限制一张,并且进行图片大小的限制,cloudPath 随机配置存储在云存储里的路径。
// 选择本地图片
chooseImgGirl: function(){
const that = this
wx.chooseImage({
count: 5,
sizeType: ['original', 'compressed'],
sourceType: ['album', 'camera'],
success (res) {
// tempFilePath可以作为img标签的src属性显示图片
if(res.tempFiles[0].size > 1200000){
return wx.showToast({
title: '图片过大',
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. 单击纸条的生命,相应值的选择对应数据库 life 字段。

  1. 点击确认放入之后,相应逻辑是先上传图片,再上传 location,weChatId 等信息到 body_girl_yun 表,上传成功要跳转到 history 页面。
// 上传图片的api,先上传图片到云存储
wx.cloud.uploadFile({
cloudPath,
filePath,
success: function(res){
wx.hideLoading()
wx.showLoading({
title: '信息上传中',
mask: true
})
// 上传图片完成后上传数据
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: ''
})
// 上传完成后跳转到history页
wx.switchTab({
url: '/pages/history/history',
success: res=>{
wx.showToast({
title: '放入纸条成功',
icon: 'success'
})
},
fail: err => {
wx.showToast({
title: err,
icon: 'error'
})
}
})
}
})
  1. 页面底部两个点击事件:联系客服与 uu 须知。联系客服主要是在 index.wxml 里通过小程序的 open-type=“concat” 来实现。
<!-- 底部的通知 -->
<view class="bottom_view">
<button bindtap="xuzhi">UU须知</button>
<!-- <text class="heng">|</text> -->
<button bindcontact="contact" open-type="contact">遇到问题?联系客服</button>
</view>

步骤 3:实现抽取纸条功能

本文主要继续围绕主页的 index.js 进行抽取纸条功能的讲解,更多详情请参见 主页逻辑代码

  1. 随机抽取可以用微信开放文档提供的 开发者资源 来查找,进行随机的查询抽取,抽取的条件是“性别,学校和生命值必须大于 0”
// 随机抽取
db.collection("body_girl_yun")
.aggregate()
.match({
sex: 1,
location: parseInt(that.data.indexBody3),
life: _.gt(0), // 生命值要大于0
})
.sample({ size: 1 })
.end();
  1. 抽到则处理,抽不到则提示处理时要把生命值减 1。
// 数据库没有这个学校的男生的纸条就
if (res.list.length == 0) {
return wx.showToast({
title: "此学校暂无纸条",
icon: "error",
mask: true,
});
}
// 抽取到了纸条,则进行操作
// 生命值减一, 生命值为0的不会抽出来
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: "抽取成功",
icon: "success",
mask: true,
});
})
.catch((err) => {
wx.showToast({
title: err,
icon: "error",
});
});
  1. 把数据写到 body_girl_yun_put 表中。
// 并将数据添加到 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. 数据存入成功则跳转页面 history。
wx.switchTab({
url: "/pages/history/history",
success: (res) => {
wx.showToast({
title: "抽出纸条成功",
icon: "success",
});
},
fail: (err) => {},
});
that.setData({
outBodyMask: false,
});
  1. 加上提示框与 loading 效果,wx.showLoading, wx.hideLoading(), 并添加了一些基本的错误处理。
// 确认抽取一张男生纸条
sureOutBodyBtn: function(){
const that = this

wx.showLoading({
title: '随机抽取中',
mask: true
})
// 随机抽取
db.collection('body_girl_yun').aggregate().match({
sex: 1,
location: parseInt(that.data.indexBody3),
life: _.gt(0) // 生命值要大于0
}).sample({ size: 1}).end()
.then(res => {
wx.hideLoading()
// 数据库没有这个学校的男生的纸条就
if(res.list.length == 0){
return wx.showToast({
title: '此学校暂无纸条',
icon: 'error',
mask: true
})
}

console.log(res)
// 生命值减一, 生命值为0的不会抽出来
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: '抽取成功',
icon: 'success',
mask: true
})
}).catch(err=>{
wx.showToast({
title: err,
icon: 'error'
})
})


// 并将数据添加到 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: '抽出纸条成功',
icon: 'success'
})
},
fail: err => {
wx.showToast({
title: err,
icon: 'error'
})
}
})

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

步骤 4:我的纸条页面设计

本文主要围绕我的纸条前端页面 history.wxml 进行讲解,更多详情请参见 我的纸条前端代码

我放入的纸条页面设计

  1. 顶部的“我放入的纸条”与“我抽到的纸条”通过改变 active 的值来切换 class。
<view class="top_title">
<text class="{{active === true ? 'on': ''}}" bindtap="inBtn"
>我放入的纸条</text
>
<text class="{{active === true ? '': 'on'}}" bindtap="outBtn"
>我抽到的纸条</text
>
</view>
  1. 前端 for 循环出每个数据进行展示。dataList 为在数据库查出来的数据,指明 wx:key
<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>交友宣言:</text>{{item.des}} </view>
</view>
</view>

在删除时需要数据的 _id,所以需要配合 JS 进行传递参数:data-id="{{item._id}}"

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

通过 e.target.dataset.id 获取到传递过来的参数。

deletePut: function (e) {
const that = this
const _id = e.target.dataset.id
}
  1. 实现交友宣言的 3 行,超出则用省略号展示。
.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. 前端判断是否有数据,没有数据则显示“空空如也”。
<view
class="kong"
wx:if="{{active === true? true: false}}"
hidden="{{dataListPut.length == 0 ? false: true}}"
>空空如也</view
>
<view class="kong" wx:else hidden="{{dataListOut.length == 0 ? false: true}}"
>空空如也</view
>

我抽到的纸条页面设计

  1. 基本跟“我放入的纸条”页面相同。多出了微信号的展示位置,交友宣言的行数减少一行。微信号的居中显示:
.outTopDes_2 {
color: rgb(62, 88, 238);
text-align: center;
font-size: 35rpx;
}
  1. 用户上传的图片可在云开发控制台 > 存储 > 存储管理页面中获取。

步骤 5:我的纸条页面逻辑处理

本文主要围绕我的纸条 history.js 进行讲解,更多详情请参见 我的纸条逻辑代码

  1. data 数据的设计。
{
"active": true, // 用来改变当前选中的样式
"dataListPut": [], // 用来存put页面的数据
"dataListOut": [],
"schoolList": ["河南理工", "河南师范", "焦作师范"]
}
  1. onload 生命周期的设计。
  • 因为这个页面没有获取用户 openId 的功能,所以需要先判断下是否已经获取到了用户的 openId。如果没有,则直接跳到 index 页面获取 openId,并提示相应错误。
if (app.globalData.openId == "") {
wx.switchTab({
url: "/pages/index/index",
success: (res) => {},
fail: (err) => {
wx.showToast({
title: err,
icon: "error",
});
},
});
}
  • 调用请求“我放入的纸条”的数据函数
that.getPutData();
  1. 通过小程序开放文档提供查询方式,用用户的 openId 来获取到数据,并且把获取到的数据,通过 that.setData 给到 dataListPut 供页面渲染。
    查询方法作用
    limit 来限制获取数据的条数
    orderBy 数据排序
// 获取数据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: '没有数据',
icon: 'none'
})
}
that.setData({
dataListPut: res.data
})
}).catch(err=>{
wx.showToast({
title: '加载数据失败',
icon: 'error'
})
})
},
  1. 删除纸条的逻辑实现。
  • 实现删除提示。
wx.showModal({
title: '提示',
// content: '确认删除纸条?',
content: '删除后友友大厅将不可见,确认?',
success (res) {

})
  • 如果用户确认删除,用 remove 通过前端传过来的 _id,对应唯一一个数据,进行删除,包括基本的错误处理。否则提示取消删除。
 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: '删除失败',
icon: 'error',
mask: true
})
}
}).catch(
console.error
)
} else if (res.cancel) {
wx.showToast({
title: '删除取消',
icon: 'error',
mask: true
})
}
}
  1. 进行分页查询结合上划触底加载数据。
  • 通过 onReachBottom 触发事件。
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom: function () {
if(this.data.active==true){
this.getPutDataBottom()
}else{
this.getOutDataBottom()
}
},
  • 通过 skip(pagePut * 10).limit(10) 中的 pagePut 参数来记录页数。
  • 通过 concat 进行新旧数据的拼接,并更新到页面。
// 上划触底的事件
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(res.data.length > 0){
// 通过concat进行数据的拼接
let all_data = that.data.dataListPut.concat(res.data)
that.setData({
dataListPut: all_data,
pagePut: pagePut
})
}else{
wx.hideLoading()
wx.showToast({
title: '没有更多数据',
icon: 'none'
})
}

})
},
  1. 实现下拉刷新数据。
  • 需要先在 app.json 文件里配置开启下拉刷新功能 enablePullDownRefresh。
"window": {
"backgroundColor": "#FFCCFF",
"backgroundTextStyle": "light",
"navigationBarBackgroundColor": "#FFCCFF",
"navigationBarTitleText": "校园交U",
"navigationBarTextStyle": "black",
"enablePullDownRefresh": true
},
  • 用户 onPullDownRefresh 下拉触发请求数据的函数,重新获取数据。
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh: function () {
if(this.data.active == true){
this.getPutData("pull")
}else{
this.getOutData("pull")
}
},

步骤 6:细节补充

  1. 加上删除纸条的 showLoading 效果。
wx.showLoading({
title: "删除中",
mask: true,
});

还有获取 put 数据的 showLoading 等。

  1. 加上 showModal 提示。
wx.showModal({
title: '提示',
content: '确认删除纸条?',
success (res) {
})
})

至此,该小程序的全部功能已实现完成。更多详情请参见 示例代码