前后端效果图
后端:nodejs 12.8 ; mongoDB 4.0
前端:uniapp
开发工具:HBuilderX 3.99
- 前端首页代码 index.vue
<!-- 源码下载地址 https://pan.baidu.com/s/1AVB71AjEX06wpc4wbcV_tQ?pwd=l9zp --><template> <view class="container"> <view class="content"> <view class="question" v-for="(item,index) in qusetionList" :key='index'> <view class="question_header"> <view class="header_title"> {{item.subjectContent}} <text style="font-weight: 500;">({{item.type==0?'单选':'多选'}})</text> </view> </view> <view class="question_option"> <view :class="{option_item:true,active_option:items.id==items.active}" v-for="(items,indexs) in item.optionList" :key='indexs' @tap.stop="optionItem(items)"> <view class="option_box"> <image src="@/static/hook.png" mode=""></image> </view> <text>{{items.optionContent}}</text> </view> </view> </view> <view style="height: 180rpx;"> <!-- 占位框,避免内容被提交按键遮挡 --> </view> </view> <!-- 底部提交按键,@tap.stop阻止冒泡事件 --> <view class="submit_box" @longpress="goAdmin"> <button class="sub_btn" type="default" @tap.stop="subQuestion">提交</button> </view> </view></template><script> export default { data() { return { baseUrl:'', active: 0, qusetionList: [], } }, onLoad() { // 获取全局变量 baseUrl this.baseUrl = getApp().globalData.baseUrl; // 调用方法 this.getData() }, methods: { //获取用户信息 getUserInfo(param) {}, //获取题目、选项 getData() { uni.request({ url: this.baseUrl + 'query', method: "GET", data: {}, success: (res) => { var arr =res.data.dataArr var dataList = arr.sort(this.compare('sort')) //按对象内的sort字段进行排序数组 // 每个问卷都加上状态字段active for (let i in dataList) { var optionList = [] for (let j in dataList[i].optionList) { dataList[i].optionList[j].active = '' optionList.push(dataList[i].optionList[j]) } dataList[i].optionList = optionList } this.qusetionList = dataList }, fail: () => { uni.showToast({ title: "网络请求失败!", icon: 'none', duration: 2000 }) } }) }, //--- 数组内的对象按某个字段进行排序 ---// compare(property){ return function(a,b){ var value1 = a[property]; var value2 = b[property]; return value1 - value2; //升序, 降序为value2 - value1 } }, // 选择及未选择样式切换 optionItem(param) { // 根据每个字段的id作为唯一状态标识是否选中 this.active = param.id for (var i in this.qusetionList) { // 单项选择 if (this.qusetionList[i].type == 0) { if (this.qusetionList[i].groudId == param.subjectId) { for (var j in this.qusetionList[i].optionList) { if (this.qusetionList[i].optionList[j].id == param.id && this.qusetionList[i].optionList[j].active =='') { this.qusetionList[i].optionList[j].active = param.id } else { this.qusetionList[i].optionList[j].active = '' } } } // 多项选择 } else if (this.qusetionList[i].type == 1) { for (var j in this.qusetionList[i].optionList) { if (this.qusetionList[i].optionList[j].id == param.id) { if (this.qusetionList[i].optionList[j].active == '') { this.qusetionList[i].optionList[j].active = param.id } else if (this.qusetionList[i].optionList[j].active != '') { this.qusetionList[i].optionList[j].active = '' } } } } } }, // 提交问卷 subQuestion() { var subTime = Date.now() var userName = '名字' + subTime.toString ().slice(-3) var activeQuestion = [] //已选择的数据列表 // 循环判断active是否为空,单选和多选因为传参格式需要区分判断 for (var i in this.qusetionList) { // 单选判断循环 if (this.qusetionList[i].type == 0) { for (var j in this.qusetionList[i].optionList) { if (this.qusetionList[i].optionList[j].active != '') { // 把已选择的数据追加到列表 activeQuestion.push({ subTime:subTime, userName: userName, // groudId: this.qusetionList[i].groudId, sort: this.qusetionList[i].sort, subjectContent: this.qusetionList[i].subjectContent, optionContent: this.qusetionList[i].optionList[j].optionContent }) } } } else { // 多选判断循环,选项ID以逗号拼接成字符串 var optionArr = [] for (var j in this.qusetionList[i].optionList) { if (this.qusetionList[i].optionList[j].active != '') { // optionArr.push(this.qusetionList[i].optionList[j].id) optionArr.push(this.qusetionList[i].optionList[j].optionContent) } } // 把已选择的数据追加到列表 if (optionArr != '') { activeQuestion.push({ subTime:subTime, userName: userName, // groudId: this.qusetionList[i].groudId, sort: this.qusetionList[i].sort, subjectContent: this.qusetionList[i].subjectContent, //optionId: optionArr.join() optionContent:optionArr.join() }) } } } //console.log(activeQuestion) if(activeQuestion.length < this.qusetionList.length){ uni.showToast({ title: "问题还没有回答完!", icon: 'none', duration: 2000 }); } else { //提交数据给后端 uni.request({ url: this.baseUrl + 'addAnswer', method: 'POST', header: {'content-type' : "application/x-www-form-urlencoded"}, data: { formData: JSON.stringify(activeQuestion) //转换为JSON格式字符串 }, success: (res) => { // 服务器返回数据,后续业务逻辑处理 console.log(res) // 调用方法,刷新数据 this.getData() uni.showToast({ title: "保存成功", icon : "success", duration:3000 }) }, fail: (err) => { console.log(err) uni.showToast({ title: "服务器响应失败,请稍后再试!", icon : "none", }) }, complete: () => { } }) } }, // 跳转到页面 goAdmin() { uni.navigateTo({ url: '../admin/admin' }) } } }</script><style lang="less" scoped> .question { .question_header { // height: 90rpx;固定高度之后,长内容换行不能自动增加高度 background-color: #f1f1f1; font-size: 34rpx; font-weight: 700; color: #333333; .header_title { width: 95%; margin-left: 37rpx; line-height: 90rpx; } } .question_option { width: 650rpx; margin-top: 7rpx; // background-color: #F0AD4E; display: flex; justify-content: space-between; flex-wrap: wrap; margin: 0 auto; margin-bottom: 40rpx; .option_item { width: 300rpx; margin-top: 34rpx; // background-color: #DD524D; font-size: 30rpx; color: #666666; display: flex; align-items: center; .option_box { width: 35rpx; height: 35rpx; border: 1rpx solid #999999; border-radius: 5px; margin-right: 10rpx; // background-color: #FF852A; display: flex; justify-content: center; align-items: center; image { width: 20rpx; height: 20rpx; } } } } } .active_option { .option_box { background: linear-gradient(-30deg, #ff7029 0%, #faa307 100%); border: 1rpx solid #faa307 !important; } text { color: #ff7029; } } .submit_box { width: 750rpx; height: 160rpx; background-color: #F1F1F1; position: fixed; bottom: 0; } .sub_btn { width: 80%; height: 88rpx; background: linear-gradient(-30deg, #dc4011 0%, #faa307 100%); border-radius: 44rpx; margin: 40rpx auto; font-size: 32rpx; font-weight: 700; color: #ffffff; text-align: center; line-height: 88rpx; } // 按钮原生会存在上下黑线,该属性去除 button::after { border: none; }</style>
- 后台管理部分页面代码 charts.vue
<template> <view> <block v-for="(item,index) in dataList" :key="index"> <view style="margin: 50rpx;">{{item.subjectContent}}</view> <canvas :canvas-id="'id'+index" style="width: 350px; height: 300px;" ></canvas> </block> </view></template><script> // 引入外部 js import canvas from '@/static/canvas.js' export default { data() { return { baseUrl: '', dataList: [] } }, onReady() { // 获取全局变量 baseUrl this.baseUrl = getApp().globalData.baseUrl; // 调用方法 this.getData() }, methods: { //从后端获取数据 getData() { uni.showLoading({ title: '数据加载中...' }) uni.request({ url: this.baseUrl + 'queryByGroup', method: "GET", data: {}, success: (res) => { //console.log(res) let tempArr = res.data this.dataList = tempArr let arr = tempArr.sort(this.compare('sort')) //按对象内的sort字段进行排序数组 // 延迟1秒等待canvas组件渲染完成,再调用方法绘画,否则绘画不成功 setTimeout(function(){ for (let x in arr) { // 调用外部方法并传入参数: canvas-id,数组,总数量 canvas.canvasGraph('id'+x, arr[x].list, arr[x].list[0].total) } },1000) }, fail: (err) => { uni.showToast({ title: "网络请求失败!", icon: 'none', duration: 2000 }) }, complete: () => { setTimeout(function(){ uni.hideLoading() },1000) } }) }, //--- 数组内的对象按某个字段进行排序 ---// compare(property){ return function(a,b){ var value1 = a[property]; var value2 = b[property]; return value1 - value2; //升序, 降序为value2 - value1 } } } }</script><style></style>
- 后端使用 nodejs + mongoDB 搭建服务
- 程序入口文件 app.js
const express = require('express');const cors=require('cors');const bodyParser = require('body-parser');const app = express();//全局变量,数据库地址global.G_url = "mongodb://127.0.0.1:27017";//处理跨域app.use(cors()) //对post请求的请求体进行解析app.use(bodyParser.urlencoded({ extended: false }))app.use(bodyParser.json())//设置share文件夹下的所有文件能通过网址访问,用作静态文件web服务app.use(express.static("./share"))//路由配置const index=require('./routes/index.js')const query=require('./routes/query.js')const add=require('./routes/add.js')const del=require('./routes/del.js')const edit=require('./routes/edit.js')const update=require('./routes/update.js')const addAnswer=require('./routes/addAnswer.js')const queryAnswer=require('./routes/queryAnswer.js')const queryByGroup=require('./routes/queryByGroup.js')const delAll=require('./routes/delAll.js')app.use('/index',index)app.use('/query',query)app.use('/add',add)app.use('/del',del)app.use('/edit',edit)app.use('/update',update)app.use('/addAnswer',addAnswer)app.use('/queryAnswer',queryAnswer)app.use('/queryByGroup',queryByGroup)app.use('/delAll',delAll) //启动服务器app.listen(3000,()=>{ console.log('http://127.0.0.1:3000')})
- 对原始数据按题目名称进行分组,然后追加需要用到的字段,再把处理好的数据发给前端进行渲染。
// queryByGroup.jsconst express = require('express');const router = express.Router();const MongoClient = require("mongodb").MongoClient;const url = G_url; //G_url是全局变量,在app.js定义router.get('/', function(req, res, next) { // 调用方法 dataOperate() /*操作数据库,异步方法*/ async function dataOperate() { var allArr = [] var arr = null var conn = null try { conn = await MongoClient.connect(url) // 定义使用的数据库和表 const dbo = conn.db("mydb").collection("answer") // 查询所有 arr = await dbo.find().toArray() // 调用 byGroup方法对原始数组按指定字段进行分组 let groupBySubjectContent = byGroup(arr, 'subjectContent') // 循环执行 for (var n in groupBySubjectContent) { let subjectContent = groupBySubjectContent[n].subjectContent let nameList = groupBySubjectContent[n].list // 从原数组中过滤字段等于subjectContent ,取最后一个元素 let lastArr = (arr.filter(item => item.subjectContent == subjectContent)).slice(-1) let sort = lastArr[0].sort // 计算数组中某个元素的累计数量 let countedNameObj = nameList.reduce((prev, item) => { if (item in prev) { prev[item]++ } else { prev[item] = 1 } return prev }, {}) // 一个对象分割为多个对象 let list = [] for (var key in countedNameObj) { var temp = {} temp.title = key temp.money = countedNameObj[key] list.push(temp) } // 所有对象 money字段求和 let listSum = list.reduce((prev, item) => { prev += item.money return prev }, 0) // 对象循环追加键值对 for (var k in list) { list[k].total = listSum list[k].value = (list[k].money / listSum).toFixed(4) //计算比例,保留4位小数 list[k].color = randomColor(k) //指定颜色 //list[k].color = '#' + ('00000' + (Math.random() * 0x1000000 << 0).toString(16)).substr(-6) //随机颜色 } // 对象追加到数组 allArr.push({ "sort": sort, "subjectContent": subjectContent, "list": list }) } //给前端返回数据 res.send(allArr) } catch (err) { console.log("错误:" + err.message) } finally { //关闭数据库连接 if (conn != null) conn.close() } } /** * 数据按字段分组处理 * @param arr [Array] 被处理的数组 * @param group_key [String] 分组字段 */ function byGroup(arr, group_key) { let map = {} let res = [] for (let i = 0; i < arr.length; i++) { let ai = arr[i] if (!map[ai[group_key]]) { // map[ai[group_key]] = [ai] //原始代码 //optionContent是要筛选出来的字段 map[ai[group_key]] = ai.optionContent.split(',') } else { // map[ai[group_key]].push(ai) //原始代码 // split()通过指定分隔符对字符串进行分割,生成新的数组; arr = [...arr, ...arr2] 数组合并 map[ai[group_key]] = [...map[ai[group_key]], ...ai.optionContent.split(',')] } } Object.keys(map).forEach(item => { res.push({ [group_key]: item, list: map[item] }) }) return res } /**随机指定颜色**/ function randomColor(index) { let colorList = ["#63b2ee","#76da91","#f8cb7f","#7cd6cf","#f89588","#9192ab","#efa666","#7898e1","#eddd86","#9987ce","#76da91","#63b2ee"] // let index = Math.floor(Math.random() * colorList.length) return colorList[index] }});module.exports = router;