目录
前言
组件功能示例
一、数据库
二、后端接口定义
三、前端准备
3.1 定义连接接口
3.2 Vant Weapp UI 组件库
3.3 授权登录与相关工具
四、小程序编写
4.1 投票组件
WXML
WXSS
JSON
WXJS
效果展示讲解:
4.2 发布投票组件
WXML
WXSS
JSON
WXJS
效果展示讲解:
尾
前言
本次主要讲解的是在会议系统中完整投票功能,首先建立思维流程:
- 先必须要有会议数据,而会议要由发布、审核、待开等,到开启会议后才能进行发布投票功能。
- 发布的投票必须要用户进行登录才可投票
- 而用户能够查询所有投票以及已投票或结束的投票
总结非常简单,如果要我们去实现,这时候就有许多问题:
- 需要编写复杂的sql和查询
- 需要多样化的组件并实现数据交互
- 需要分析流程与执行顺序
如果这些都需要自己独立完成,很多东西还要自己去研究测试,不仅要走许多弯路还得花很多时间和心思。嗯...我就是这样过来的,这篇也是爆肝凌晨几点完成的。话不多说,现在就开始进入正题。
组件功能示例
一、数据库
投票功能是建立在会议和用户之上的,所以先介绍一下这两个表:
t_oa_meeting_info:
wx.user:
主要关注会议状态(state字段)
1、存储发布的投票表:
2. 用户投票信息表:
sql预览:
#查询进行中的投票 select * from t_oa_meeting_option where meetingId = (select id from t_oa_meeting_info where state = 5)#查询结束会议的投票 select * from t_oa_meeting_option where meetingId = (select id from t_oa_meeting_info where state = 6)#查询票数 select count(*) from t_oa_meeting_vote where optionId = 2#查询已投的的票 select * from t_oa_meeting_option where id = (select optionId from t_oa_meeting_vote where personId = 5)
二、后端接口定义
方法功能:注释
WxInfoController :会议数据
package com.ycxw.ssm.wxcontroller;import com.ycxw.ssm.mapper.InfoMapper;
import com.ycxw.ssm.model.Info;
import com.ycxw.ssm.util.ResponseUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.HashMap;
import java.util.List;
import java.util.Map;/*** @author 云村小威* @create 2023-10-26 22:28*/
@RestController
@RequestMapping("/wx/info")
public class WxInfoController {@Autowiredprivate InfoMapper infoMapper;@RequestMapping("/list")public Object list (Info info){List<Info> list = infoMapper.list(info);Map<Object, Object> data = new HashMap<Object, Object>();data.put("infoList",list);return ResponseUtil.ok(data);}//查询所有进行中的会议@RequestMapping("/listState")public Object listState (Info info){info.setState(4);List<Info> list = infoMapper.list(info);Map<Object, Object> data = new HashMap<Object, Object>();data.put("listState",list);return ResponseUtil.ok(data);}//修改会议状态@RequestMapping("/state")public Object updateState (Info info){infoMapper.updateByPrimaryKeySelective(info);return ResponseUtil.ok();}
}
WXOptionController:投票数据
package com.ycxw.ssm.wxcontroller;import com.ycxw.ssm.mapper.OptionMapper;
import com.ycxw.ssm.model.Option;
import com.ycxw.ssm.util.ResponseUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.HashMap;
import java.util.List;
import java.util.Map;/*** @author 云村小威* @create 2023-10-26 22:28*/
@RestController
@RequestMapping("/wx/option")
public class WXOptionController {@Autowiredprivate OptionMapper optionMapper; //正在投票信息//查询所有投票@RequestMapping("/list")public Object list() {List<Option> list = optionMapper.list();Map<Object, Object> data = new HashMap<Object, Object>();data.put("voteList", list);return ResponseUtil.ok(data);}/*发布投票*/@RequestMapping("/add")public Object save(Option option) {//发起投票optionMapper.insertSelective(option);return ResponseUtil.ok();}//查询所有结束的投票@RequestMapping("/over")public Object selectOverVote() {List<Option> list = optionMapper.selectOverVote();Map<Object, Object> data = new HashMap<Object, Object>();data.put("overList", list);return ResponseUtil.ok(data);}//查询所有已投票的信息@RequestMapping("/already")public Object selectByAlready(Long personId) {List<Option> list = optionMapper.selectByAlready(personId);Map<Object, Object> data = new HashMap<Object, Object>();data.put("alreadyList", list);return ResponseUtil.ok(data);}//模糊查询投票信息@RequestMapping("/search")public Object SearchVote(Option option) {List<Option> list = optionMapper.SearchVote(option);Map<Object, Object> data = new HashMap<Object, Object>();data.put("searchList", list);return ResponseUtil.ok(data);}
}
WXVoteController:用户投票数据
package com.ycxw.ssm.wxcontroller;import com.ycxw.ssm.mapper.VoteMapper;
import com.ycxw.ssm.model.Vote;
import com.ycxw.ssm.util.ResponseUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.HashMap;
import java.util.Map;/*** @author 云村小威* @create 2023-10-26 22:28*/
@RestController
@RequestMapping("/wx/vote")
public class WXVoteController {@Autowiredprivate VoteMapper voteMapper;//查询选项正在投票的会议的票数@RequestMapping("/ticket")public Object selectByOptionId(Long optionId) {Long i = voteMapper.selectByOptionId(optionId);Map<Object, Object> data = new HashMap<Object, Object>();data.put("ticket", i);return ResponseUtil.ok(data);}//投票@RequestMapping("/add")public Object insertSelective(Vote record) {int i = voteMapper.insertSelective(record);Map<Object, Object> data = new HashMap<Object, Object>();data.put("ok", i);return ResponseUtil.ok(data);}}
三、前端准备
3.1 定义连接接口
这里只列出本次需要实现功能的接口
// 以下是业务服务器API地址
// 本机开发API地址
var WxApiRoot = 'http://localhost:8080/oapro/wx/';module.exports = {AuthLoginByWeixin: WxApiRoot + 'auth/login_by_weixin', //微信登录AuthLogout: WxApiRoot + 'auth/logout', //账号登出MettingInfoState: WxApiRoot + 'info/listState', //所有进行中的会议VoteInfos: WxApiRoot + 'option/list', //所有发布投票的会议SearchVote: WxApiRoot + 'option/search', //搜索投票信息UpdateState: WxApiRoot + 'info/state', //修改会议状态MeetingAddVote: WxApiRoot + 'option/add', //发布投票OverVote: WxApiRoot + 'option/over',//所有结束的会议AlreadyVote: WxApiRoot + 'option/already',//所有已投的会议SelectTicket: WxApiRoot + 'vote/ticket',//查询票数AddTicket: WxApiRoot + 'vote/add',//投票
};
3.2 Vant Weapp UI 组件库
本次主要使用 Vant Weapp UI 组件搭建的,学习这个很简单,根据文档安装操作即可:进入 Vant Weapp UI 文档
3.3 授权登录与相关工具
进入 【微信小程序】授权登录流程解析
主要为了了解微信授权登录的流程和原理,方便更好理解后面的知识。
四、小程序编写
4.1 投票组件
WXML
这里是利用 vant weapp 组件布局的,里面包括搜索框、tab列表、弹窗、以及一个定位的图片按钮,用于进入发布投票页面。
<!--pages/vote/list/list.wxml-->
<view style="height: 15rpx;"></view>
<!-- 搜索框 -->
<van-search value="{{ search }}" placeholder="请输入搜索关键词" show-action bind:change="Search" bind:search="onSearch" bind:cancel="onCancel" />
<view style="height: 20rpx;"></view>
<!-- tabs列表 -->
<van-tabs type="card" color="#1989fa"><!-- VoteALL --><van-tab title="进行中"><van-divider dashed contentPosition="center" customStyle="color: #1989fa; border-color: #1989fa;">投一票</van-divider><view class="oaFlex"><block wx:for-items="{{VoteAll}}" wx:for-item="item" wx:key="item.id"><view class="list" data-id="{{item.id}}" bindtap="open_vote" data-text="{{item.title}}"><view class="list-detail"><view class="list-title"><text>{{item.title}}</text></view><!-- <view class="list-tag"><view class="join al-center"><text class="list-num">0</text> 人已投</view></view> --></view><view class="list-img al-center"><image class="video-img" mode="scaleToFill" src="{{item.picture}}"></image></view></view></block></view></van-tab><!-- publishVote --><van-tab title="已结束"><view style="height: 50rpx;"></view><view class="oaFlex"><block wx:for-items="{{OverVote}}" wx:for-item="item" wx:key="item.id"><view class="list" data-id="{{item.id}}" bindtap="open_vote" data-text="{{item.title}}"><view class="list-detail"><view class="list-title"><text>{{item.title}}</text></view><!-- <view class="list-tag"><view class="join al-center"><text class="list-num">0</text> 人已投</view></view> --></view><view class="list-img al-center"><image class="video-img" mode="scaleToFill" src="{{item.picture}}"></image></view></view></block></view></van-tab><!-- AlreadyVote --><van-tab title="已投票"><view style="height: 50rpx;"></view><view class="oaFlex"><block wx:for-items="{{AlreadyVote}}" wx:for-item="item" wx:key="item.id"><view class="list" data-id="{{item.id}}" bindtap="open_vote" data-text="{{item.title}}"><view class="list-detail"><view class="list-title"><text>{{item.title}}</text></view><!-- <view class="list-tag"><view class="join al-center"><text class="list-num">0</text> 人已投</view></view> --></view><view class="list-img al-center"><image class="video-img" mode="scaleToFill" src="{{item.picture}}"></image></view></view></block></view></van-tab>
</van-tabs>
<!-- 投票弹出框 -->
<van-dialog use-slot title="投一票" show="{{ showVote }}" show-cancel-button confirm-button-open-type="getUserInfo" bind:close="onClose" bind:getuserinfo="getUserInfo"><view style="height: 25rpx;"></view><van-notice-bar scrollable="{{true}}" color="#1989fa" background="#ecf9ff" left-icon="volume-o" text="{{text}}" /><view style="height: 15rpx;"></view><van-cell-group inset><van-cell title="当前票数"><van-stepper disable-input="{{true}}" value="{{ ticket }}" bind:change="onChange" max="{{ticket+1}}" min="{{ticket}}" /></van-cell></van-cell-group>
</van-dialog>
<!-- 发布投票图片按钮 -->
<view style="position:fixed; bottom:10px;width: 150rpx;right: 10px;"><van-image bindtap="open_publishVote" round width="5rem" height="5rem" src="/static/images/add.png" />
</view>
WXSS
/* pages/vote/list/list.wxss */
.oaFlex {display: flex;flex-direction: column;align-items: center;
}.list {width: 360px;display: flex;flex-direction: column;align-items: center;background-color: rgb(228, 240, 253);border-bottom: 2px solid #d9dbe2;margin-bottom: 25px;
}.video-img {width: 360px;height: 160px;
}.list-detail {display: flex;flex-direction: column;align-items: center;
}.list-title {margin-top: 10px;height: 30px;font-size: 13pt;color: #333;font-weight: bold;
}.list-info {color: #aaa;
}.list-num {color: red;/* font-weight: 700; */
}.join {padding: 0px 0px 0px 10px;color: #aaa;
}.state {margin: 0px 6px 0px 6px;border: 1px solid #4083ff;color: #4083ff;padding: 3px 5px 3px 5px;
}.list-tag {padding: 10px 0px 10px 0px;display: flex;align-items: center;
}
JSON
{"navigationBarTitleText": "投票","usingComponents": {"van-row": "@vant/weapp/row/index","van-col": "@vant/weapp/col/index","van-search": "@vant/weapp/search/index","van-switch": "@vant/weapp/switch/index","van-dialog": "@vant/weapp/dialog/index","van-tab": "@vant/weapp/tab/index","van-tabs": "@vant/weapp/tabs/index","van-divider": "@vant/weapp/divider/index","van-image": "@vant/weapp/image/index","van-stepper": "@vant/weapp/stepper/index","van-cell": "@vant/weapp/cell/index","van-cell-group": "@vant/weapp/cell-group/index","van-notice-bar": "@vant/weapp/notice-bar/index"}
}
WXJS
// pages/vote/list.js
var app = getApp();
const api = require('../../../config/api');
const util = require('../../../utils/util.js');Page({/*** 页面的初始数据*/data: {VoteAll: [], //进行中的投票OverVote: [], //已结束AlreadyVote: [], //已投票checked: false, //禁止投票开关showVote: false, //投票弹出框开关text: '', //投票标题ticket: 0, //票数ticket2: 0, //投票后的票数search: '', //搜索值optionId: 0, //投票idpersonId: 0, //用户id},/*监听搜索输入框的值*/Search(event) {this.setData({search: event.detail})},/*输入框搜索商品*/onSearch() {var that = this;//调用查询接口util.request(api.SearchVote, {title: that.data.search}).then(res => {//如果搜索字段为空,就刷新界面if (that.data.search == '') {this.onShow();} else {this.setData({VoteAll: res.data.searchList});}}).catch(res => {console.log('服器没有开启,使用模拟数据!')})},//获取全部投票信息loadVoteInfos() {util.request(api.VoteInfos).then(res => {this.setData({VoteAll: res.data.voteList});}).catch(res => {console.log('服器没有开启,使用模拟数据!')})},//获取全部已结束投票信息loadOverVote() {util.request(api.OverVote).then(res => {this.setData({OverVote: res.data.overList});}).catch(res => {console.log('服器没有开启,使用模拟数据!')})},//获取全部已投票的信息loadAlreadyVote() {util.request(api.AlreadyVote, {personId: this.data.personId}).then(res => {this.setData({AlreadyVote: res.data.alreadyList});}).catch(res => {console.log('服器没有开启,使用模拟数据!')})},//发布投票点击事件open_publishVote: function () {wx.navigateTo({url: '/pages/meeting/add/add',})},//投票弹窗点击事件open_vote(event) {var itemId = event.currentTarget.dataset.id; //投票idvar itemText = event.currentTarget.dataset.text; //投票主题util.request(api.SelectTicket, {optionId: itemId}).then(res => {this.setData({text: itemText,showVote: true,ticket: res.data.ticket,optionId: itemId});}).catch(res => {console.log('服器没有开启,使用模拟数据!')})},//投票弹窗关闭事件onClose() {this.setData({showVote: false});},//票数绑定onChange(event) {// 用户投的票this.setData({ticket2: event.detail});console.log(this.data.ticket); //原票数console.log(this.data.ticket2); //新票数},// 投票确认事件getUserInfo(event) {let ticket = this.data.ticket2;let userInfo = wx.getStorageSync('userInfo');;// 构造发布投票请求参数const putVote = {optionId: this.data.optionId,personId: userInfo.userId};//对比如果没增长就代表没投票,并且为登录状态if (this.data.ticket == ticket || userInfo.userId == 0) {wx.showToast({title: '暂未投票哦',icon: 'error',duration: 2000,mask: true //显示透明蒙层,防止触摸穿透});} else {util.request(api.AddTicket, putVote).then(res => {console.log(res);wx.showToast({title: '投票成功',icon: 'sucess',duration: 2000,mask: true //显示透明蒙层,防止触摸穿透});this.onShow();}).catch(res => {console.log('服器没有开启,使用模拟数据!')})}},/*** 生命周期函数--监听页面加载*/onLoad(options) {},/*** 生命周期函数--监听页面显示*/onShow() {// 从本地缓冲拿取用户idlet userInfo = wx.getStorageSync('userInfo');this.setData({personId: userInfo.userId});this.loadVoteInfos();this.loadOverVote();this.loadAlreadyVote();},/*** 生命周期函数--监听页面隐藏*/onHide() {},/*** 生命周期函数--监听页面卸载*/onUnload() {},/*** 页面相关事件处理函数--监听用户下拉动作*/onPullDownRefresh() {},/*** 页面上拉触底事件的处理函数*/onReachBottom() {},/*** 用户点击右上角分享*/onShareAppMessage() {}
})
效果展示讲解:
1、查询当前登录的用户id,获取已投票和未投票信息,分别存储不同数组进行遍历显示
2、通过数据投票主题关键字进行模糊查询
3、点击投票列表进入弹窗,显示投票主题和该票数,利用进步器选择加减投票
4、必须加一票和登录后才能投票成功,并刷新界面
🌟更多解释请研究js代码
4.2 发布投票组件
WXML
这里用到了图片上传和下拉会议列表
<!--pages/meeting/add/add.wxml-->
<view class="img"><image class="upload_img" src="{{imageUrl=='' ? '/static/images/uploadimg.png':imageUrl }}" mode="aspectFit" bindtap="handleUploadImage"></image><input hidden="true" type="text" name="images" value="{{imageUrl}}" />
</view>
<view style="height: 10px;"></view>
<van-cell-group><van-field model:value="{{ title }}" placeholder="请输入投票主题" label="主题" border="{{ true }}" />
</van-cell-group>
<view style="height: 10px;"></view>
<!-- 会议列表 -->
<van-dropdown-menu active-color="#1989fa"><van-dropdown-item value="{{ value }}" options="{{ option }}" bind:change="onDropdownMenuChange" />
</van-dropdown-menu>
<view style="height: 300px;"></view>
<van-row><van-col offset="8" span="8"><van-button plain hairline icon="add-square" type="info" style="margin-top: 25px;" bind:tap="handleVote" size="large">发起投票</van-button></van-col>
</van-row>
WXSS
/* pages/meeting/add/add.wxss */
.img {margin-top: 25px;height: 480rpx;
}.upload_img {height: 450rpx;width: 735rpx;box-shadow: 5px 8px rgb(218, 221, 221);border-radius: 5rpx;
}
JSON
{"navigationBarTitleText": "发起投票","usingComponents": {"van-field": "@vant/weapp/field/index","van-uploader": "@vant/weapp/uploader/index","van-row": "@vant/weapp/row/index","van-col": "@vant/weapp/col/index","van-calendar": "@vant/weapp/calendar/index","van-cell": "@vant/weapp/cell/index","van-cell-group": "@vant/weapp/cell-group/index","van-button": "@vant/weapp/button/index","van-picker": "@vant/weapp/picker/index","van-dropdown-menu": "@vant/weapp/dropdown-menu/index","van-dropdown-item": "@vant/weapp/dropdown-item/index"}
}
WXJS
// pages/meeting/add/add.js
var app = getApp();
const api = require('../../../config/api');
const util = require('../../../utils/util.js');Page({/*** 页面的初始数据*/data: {imageUrl: '', //发布投票图片option: [{text: '选择会议',value: 0}], //会议选项(该是默认值)value: 0, //会议选项idoptiontext: null, //会议选项标题title: '', //投票主题},//图片选择器handleUploadImage() {wx.chooseImage({count: 1,success: (res) => {const imagePath = res.tempFilePaths[0];// 处理选择的图片路径console.log('选择的图片路径:', imagePath);this.setData({imageUrl: imagePath // 更新图片路径,触发重新渲染});}});},//获取所有正在进行的会议loadMeetingInfos() {util.request(api.MettingInfoState).then(res => {//定义空数组存储数据库传来的json数据值let options = [];for (var index in res.data.listState) {//获取需要的字段值let id = res.data.listState[index].id;let title = res.data.listState[index].title;//给每个值设置对应的字段存储到数组options.push({text: title,value: id});}this.setData({//在option数组的内容后添加新的内容option: this.data.option.concat(options)});}).catch(res => {console.log('服器没有开启,使用模拟数据!')})},// 监听菜单选择事件onDropdownMenuChange(event) {//获取选择的选项值let value = event.detail;//获取option数组的value与选择的value值相同的数据const selectedOption = this.data.option.find(option => option.value === value);this.setData({//将数据赋值到变量中进行存储optiontext: selectedOption.text,value: selectedOption.value});},//发布投票handleVote() {// 获取页面中的数据const picture = this.data.imageUrl;const meetingId = this.data.value;const title = this.data.title;//判断只要有内容为空就不进行发布if (picture == '' || meetingId == 0 || title == '') {wx.showToast({title: '请完善投票内容',icon: 'error',duration: 2000,mask: true //显示透明蒙层,防止触摸穿透});//阻止运行后面的代码return}// 构造发布投票请求参数const requestData = {meetingid: meetingId,title: title,picture: picture};// 构造修改会议状态请求参数const updateData = {id: meetingId,state:5};// 发起网络请求,将数据传递给后端util.request(api.MeetingAddVote, requestData).then(res => {//修改会议状态为开启投票接口util.request(api.UpdateState,updateData).then(res => {console.log(res)});//获取页面栈返回上一个界面const pages = getCurrentPages() //获取页面列表const perpage = pages[pages.length - 1] //当前页 //刷新页面wx.navigateBack({delta: 1,//返回成功调用函数success: function () {wx.showToast({title: '发布成功',icon: 'success',duration: 2000,mask: true //显示透明蒙层,防止触摸穿透});}})//加载界面(用于刷新)perpage.onLoad()// 可以在这里进行页面跳转或其他操作}).catch(res => {// 请求失败的处理逻辑console.error('数据保存失败', err);})},/*** 生命周期函数--监听页面加载*/onLoad(options) {this.loadMeetingInfos();},/*** 生命周期函数--监听页面初次渲染完成*/onReady() {},/*** 生命周期函数--监听页面显示*/onShow() {},/*** 生命周期函数--监听页面隐藏*/onHide() {},/*** 生命周期函数--监听页面卸载*/onUnload() {},/*** 页面相关事件处理函数--监听用户下拉动作*/onPullDownRefresh() {},/*** 页面上拉触底事件的处理函数*/onReachBottom() {},/*** 用户点击右上角分享*/onShareAppMessage() {}
})
效果展示讲解:
1、首先是一个图片点击事件跳转到发布投票界面
2、进入页面就开始渲染会议列表内容,获取所有会议状态为开启会议的json数据,通过遍历拿取id和标题,按照指定下拉列表组件指定属性进行复制。
3、通过微信提供的图片上传函数,将图片解析成网络路径进行访问和存储
4、最后准备发布投票:
- 监听下拉列表数据,通过选择的值对原数组的值进行判断从而确定拿取的数据
- 发布成功后通过获取页面栈返回上级界面,并利用生命周期同时刷新投票界面
🌟更多解释请研究js代码
尾
如你们所见,其实其中许多不足,比如用户投完票后,该选择没有在界面上消除。这里还需要做连表查询投票信息如果存在该发布的投票列表就不会再获取到;还有每当点击投票弹窗确定时,不管是否投了票,是否登录了,它都会调用一次微信登录方法,这个目前还在排查中,不知道是否是某些方法牵引到了登录功能,各位大佬有什么见解欢迎评论区留言🥰