uniapp + node.js 开发问卷调查小程序

前后端效果图
在这里插入图片描述
后端: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.js

const 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;

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>