基于Springboot+Vue的在线答题闯关系统

基于Springboot+Vue的在线答题闯关系统

前言:随着在线教育的快速发展,传统的教育模式逐渐向互联网+教育模式转型。在线答题系统作为其中的一个重要组成部分,能够帮助用户通过互动式的学习方式提升知识掌握度。本文基于Spring Boot和Vue.js框架,设计并实现了一个在线答题闯关系统,旨在为用户提供顺序出题、随机出题、错题本、收藏题目、答题统计等多种功能,以增强用户的学习体验。

目录

前言

本系统采用前后端分离架构,前端使用Vue.js框架实现,后端则通过Spring Boot进行构建,数据存储使用MySQL数据库。前端使用Vue.js进行数据渲染,而后端提供RESTful API接口来实现前后端的有效数据交互。

项目功能及技术

功能模块设计

  • 顺序出题模块:该模块允许用户按顺序答题,系统根据预设的题目顺序逐一展示给用户。用户完成每一道题后可以进入下一题,适合需要系统化学习的用户。

  • 体型练习模块:用户可以根据自己的需求选择特定的练习模式,比如选择某个类别或某个难度的题目进行练习。该模块支持用户自定义练习内容,帮助用户强化薄弱的知识点。

  • 随机出题模块:系统可以随机从题库中抽取题目,进行答题闯关,用户在有限的时间内答题,提升学习的趣味性和挑战性。

  • 错题本模块:该模块记录用户做错的题目,用户可以随时查看并重新进行练习,帮助用户集中攻克自己的薄弱环节,提升记忆与掌握度。

  • 我的收藏模块:用户可以将自己喜欢或难度较高的题目收藏到个人收藏夹,方便以后再次复习或挑战。

  • 答题统计模块:系统自动统计用户的答题情况,包括正确率、答题速度、错题数量等,帮助用户了解自己的学习进度和成效,并能根据数据调整学习策略。

技术:

  • Spring Boot:后端框架,利用Spring Boot的快速开发特性。同时,通过Mybatis简化数据库操作,提高数据访问效率。

  • Vue.js:前端框架,使用Vuex进行全局状态管理,提升数据的一致性与可维护性。

  • MySQL:数据库存储引擎,负责存储题目数据、用户答题记录、错题与收藏等信息。

  • Layui:前端UI组件库,用于搭建美观且响应式的用户界面,提升用户交互体验。

用户端

管理端

API

SpringBoot框架搭建

1.创建maven project,先创建一个名为SpringBootDemo的项目,选择【New Project】

然后在弹出的下图窗口中,选择左侧菜单的【New Project】


在project下创建module,点击右键选择【new】---【Module...】

左侧选择【Spring initializr】,通过idea中集成的Spring initializr工具进行spring boot项目的快速创建。窗口右侧:name可根据自己喜好设置,group和artifact和上面一样的规则,其他选项保持默认值即可,【next】

Developer Tools模块勾选【Spring Boot DevTools】,web模块勾选【Spring Web】,此时,一个Springboot项目已经搭建完成,可开发后续功能

实体映射创建Mapper

创建一个entity实体类文件夹,并在该文件夹下创建项目用到的实体类

java 复制代码
package com.example.demo.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Data
public class User {
    @TableId(type = IdType.AUTO)
    private Long id;

    private String account;

    private String pwd;

    private String userDesc;

    private String userHead;

    private LocalDateTime createTime;

    private Long role;

    private String nickname;

    private String email;

    private String tags;
}

接口封装

由于我们使用mybatis-plus,所以简单的增删改查不用自己写,框架自带了,只需要实现或者继承他的Mapper、Service

创建控制器Controller

整合Swagger

添加依赖

先导入spring boot的web包

<!--swagger依赖-->
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.9.2</version>
</dependency>
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.9.2</version>
</dependency>

配置Swagger

创建一个swagger的配置类,命名为SwaggerConfig.java

/*
     *用于定义API主界面的信息,比如可以声明所有的API的总标题、描述、版本
     */
    private ApiInfo apiDemo() {
        return new ApiInfoBuilder()
                //用来自定义API的标题
                .title("SpringBoot项目SwaggerAPIAPI标题测试")
                //用来描述整体的API
                .description("SpringBoot项目SwaggerAPI描述测试")
                //创建人信息
                .contact(new Contact("测试员张三","http://localhost:8080/springboot/swagger-ui.html","xxxxxxxx@163.com"))
                //用于定义服务的域名
                //.termsOfServiceUrl("")
                .version("1.0") //可以用来定义版本
                .build();
    }

接口测试

运行Spring Boot项目,默认端口8080,通过地址栏访问url

接口组定义

根据不同的业务区分不同的接口组,使用@API来划分

@Api(tags = "用户管理") //  tags:组名称
@RestController
public class RoleController {
}


接口定义

使用@ApiModel来标注实体类,同时在接口中定义入参为实体类作为参数。

  • @ApiModel:用来标类

  • 常用配置项:value:实体类简称;description:实体类说明

  • @ApiModelProperty:用来描述类的字段的含义。

常用字段类型

字段类型 所占字节 存储范围 最大存储值 使用场景
TINYINT 1 -128~127 127 存储小整数
INT 4 -2147483648~2147483647 2147483647 存储大整数
BIGINT 8 -9223372036854775808~9223372036854775807 9223372036854775807 存储极大整数
DECIMAL 可变长度 存储精度要求高的数值
CHAR 固定长度 最多255字节 255个字符 存储长度固定的字符串
VARCHAR 可变长度 最多65535字节 65535个字符 存储长度不固定的字符串
DATETIME 8 '1000-01-01 00:00:00'~'9999-12-31 23:59:59' '9999-12-31 23:59:59' 存储日期和时间

参考代码块

html 复制代码
  <body style="background-color: #f7f7f7;">
		<div class="headerBox">
			<div class="logoBox">
				<img src="img/logo1.png" />
				<div class="logoTitle">在线答题闯关</div>
			</div>
			<div class="menuBox">
				<div class="menuItem activeMenu ">
					<a href="index.html">练习模式</a>
				</div>
				<div class="menuItem blackColor">
					<a href="challengeLevels.html?param=primary">闯关模式</a>
				</div>
				<div class="menuItem blackColor">
					<a href="wrongQuestion.html">我的错题</a>
				</div>
				<div class="menuItem blackColor">
					<a href="myCollection.html">我的收藏</a>
				</div>
				<div class="menuItem blackColor">
					<a href="statistics.html">答题统计</a>
				</div>
				<div class="menuItem blackColor">
					<a href="center.html">个人中心</a>
				</div>
				<div class="menuItem blackColor">
					<a href="./login/login.html">退出登录</a>
				</div>
			</div>
		</div>
		<div class="container-fluid" id="content-page">
			<div class="row">
				<div class="col-md-2">&nbsp;</div>
				<div class="col-md-8">
					<div class="searchBox">
						<div class="leftTitle">
							{{pageTitle}}
						</div>
					</div>
				</div>
				<div class="col-md-2">&nbsp;</div>
			</div>
			<div class="row">
				<div class="col-md-2">&nbsp;</div>
				<div class="col-md-8">
					<div v-for="(item,index) in questionList">
						<div class="radioItemBox" v-if="item.questionType == '单选题'">
							<div class="BtnBox radioItem">
								<div>{{index+1}}.</div>
								<textarea rows="18" cols="90" v-model="item.title" disabled></textarea>
								<img src="img/collecQues.png" class="radioItemTypeImg"
									v-on:click="collection(item.id)" />
							</div>
							<div class="BtnBox radioLineV2" v-if="item.checkA!=''">
								<div class="radioIconBox">
									<input type="radio" class="radioInputCheck" :name="index"
										v-on:change="checkOne(index,'A')" />
								</div>
								<div>A.</div>
								<input v-model="item.checkA" disabled class="radioLineV2Input" />
							</div>
							<div class="BtnBox radioLineV2" v-if="item.checkB!=''">
								<div class="radioIconBox">
									<input type="radio" class="radioInputCheck" :name="index"
										v-on:change="checkOne(index,'B')" />
								</div>
								<div>B.</div>
								<input v-model="item.checkB" disabled class="radioLineV2Input" />
							</div>
							<div class="BtnBox radioLineV2" v-if="item.checkC!=''">
								<div class="radioIconBox">
									<input type="radio" class="radioInputCheck" :name="index"
										v-on:change="checkOne(index,'C')" />
								</div>
								<div>C.</div>
								<input v-model="item.checkC" disabled class="radioLineV2Input" />
							</div>
							<div class="BtnBox radioLineV2" v-if="item.checkD!=''">
								<div class="radioIconBox">
									<input type="radio" class="radioInputCheck" :name="index"
										v-on:change="checkOne(index,'D')" />
								</div>
								<div>D.</div>
								<input v-model="item.checkD" disabled class="radioLineV2Input" />
							</div>
							<div class="BtnBox radioLineV2" v-if="item.checkE!=''">
								<div class="radioIconBox">
									<input type="radio" class="radioInputCheck" :name="index"
										v-on:change="checkOne(index,'E')" />
								</div>
								<div>E.</div>
								<input v-model="item.checkE" disabled class="radioLineV2Input" />
							</div>
						</div>
						<div class="radioItemBox" v-if="item.questionType == '多选题'">
							<div class="BtnBox radioItem">
								<div>{{index+1}}.</div>
								<textarea rows="18" cols="90" v-model="item.title" disabled></textarea>
								<img src="img/collecQues.png" class="radioItemTypeImg"
									v-on:click="collection(item.id)" />
							</div>
							<div class="BtnBox radioLineV2" v-if="item.checkA!=''">
								<div class="radioIconBox">
									<input type="checkbox" class="radioInputCheck" :name="index"
										v-on:change="checkTwo($event,index,'A')" />
								</div>
								<div>A.</div>
								<input v-model="item.checkA" disabled class="radioLineV2Input" />
							</div>

							<div class="BtnBox radioLineV2" v-if="item.checkB!=''">
								<div class="radioIconBox">
									<input type="checkbox" class="radioInputCheck" :name="index"
										v-on:change="checkTwo($event,index,'B')" />
								</div>
								<div>B.</div>
								<input v-model="item.checkB" disabled class="radioLineV2Input" />
							</div>
							<div class="BtnBox radioLineV2" v-if="item.checkC!=''">
								<div class="radioIconBox">
									<input type="checkbox" class="radioInputCheck" :name="index"
										v-on:change="checkTwo($event,index,'C')" />
								</div>
								<div>C.</div>
								<input v-model="item.checkC" disabled class="radioLineV2Input" />
							</div>
							<div class="BtnBox radioLineV2" v-if="item.checkD!=''">
								<div class="radioIconBox">
									<input type="checkbox" class="radioInputCheck" :name="index"
										v-on:change="checkTwo($event,index,'D')" />
								</div>
								<div>D.</div>
								<input v-model="item.checkD" disabled class="radioLineV2Input" />
							</div>
							<div class="BtnBox radioLineV2" v-if="item.checkE!=''">
								<div class="radioIconBox">
									<input type="checkbox" class="radioInputCheck" :name="index"
										v-on:change="checkTwo($event,index,'E')" />
								</div>
								<div>E.</div>
								<input v-model="item.checkE" disabled class="radioLineV2Input" />
							</div>
						</div>
						<div class="radioItemBox" v-if="item.questionType == '判断题'">
							<div class="BtnBox radioItem">
								<div>{{index+1}}.</div>
								<textarea rows="18" cols="90" v-model="item.title" disabled></textarea>
								<img src="img/collecQues.png" class="radioItemTypeImg"
									v-on:click="collection(item.id)" />
							</div>
							<div class="BtnBox radioLineV2" v-if="item.checkA!=''">
								<div class="radioIconBox">
									<input type="radio" class="radioInputCheck" :name="index"
										v-on:change="checkOne(index,'A')" />
								</div>
								<div>A.</div>
								<input v-model="item.checkA" disabled class="radioLineV2Input" />
							</div>
							<div class="BtnBox radioLineV2" v-if="item.checkB!=''">
								<div class="radioIconBox">
									<input type="radio" class="radioInputCheck" :name="index"
										v-on:change="checkOne(index,'B')" />
								</div>
								<div>B.</div>
								<input v-model="item.checkB" disabled class="radioLineV2Input" />
							</div>
						</div>
					</div>

					<div class="BtnBox margin-sm">
						<div class="AddQuesBtnItem" v-on:click="SaveChange">
							<img src="img/submit.png" />
							提交
						</div>
						<div style="height:100px;"></div>
					</div>
				</div>
				<div class="col-md-2">&nbsp;</div>
			</div>
		</div>
		<script type="text/javascript" src="js/jquery.min.js"></script>
		<script type="text/javascript" src="js/vue.js"></script>
		<script type="text/javascript" src="login/layui/layui.js"></script>
		<script>
			//轻量级框架
			var dataInfo = new Vue({
				el: "#content-page",
				//Vue的数据对象
				data: {
					questionList: [],
					pageTitle: ''
				}, //数据对象结束
				//方法
				methods: {
					GetAll: function() {
						let vm = this;
						let param = GetQueryString("param");
						let type = GetQueryString("type");
						let quesType = '';
						if (param == 'practice') {

							if (type == '0') {
								quesType = '单选题';
							} else if (type == '1') {
								quesType = '多选题';
							} else {
								quesType = '判断题';
							}
							vm.pageTitle = '练习模式:' + quesType;
						} else if (param == 'order') {
							vm.pageTitle = '顺序出题';
						} else {
							vm.pageTitle = '随机练习';
						}
						var user = JSON.parse(sessionStorage.getItem('user'));

						$.ajax({
							url: "http://127.0.0.1:8081/common/answer-list?userId=" + user.id + "&param=" +
								param + "&type=" + quesType,
							async: false,
							type: "POST",
							contentType: 'application/json',
							dataType: 'json',
							success: function(json) {

								vm.questionList = json.list
							}

						});

					},
					//单选框选择事件
					checkOne(index, check) {
						let vm = this;
						vm.questionList[index].isChecked = check;
					},
					//多选框选择事件
					checkTwo(event, index, check) {
						let vm = this;
						const checked = event.target.checked;
						let info = vm.questionList[index];
						let checkedArray = info.isChecked.split(',');
						if (checkedArray[0] == '') {
							checkedArray.splice(0, 1);
						}
						if (checked) {
							checkedArray.push(check);
							info.isChecked = checkedArray.join(',');
						} else {
							let ind = checkedArray.indexOf(check);
							if (ind !== -1) {
								checkedArray.splice(ind, 1);
							}
							info.isChecked = checkedArray.join(',');
						}
					},
					//点击提交
					SaveChange() {
						let vm = this;
						//得分
						let number = 0;
						let list = vm.questionList;
						for (let i = 0; i < list.length; i++) {
							if (list[i].isChecked != '') {
								let right = list[i].rightKey.split(',');
								let check = list[i].isChecked.split(',');
								if (right.length === check.length && right.sort().toString() === check.sort()
									.toString()) {
									list[i].correct = 1;
									number++;
								}
							}
						}
						var user = JSON.parse(sessionStorage.getItem('user'));
						var vo = {};
						vo.answerList = list;
						vo.number = number;
						vo.userId = user.id;
						vo.type = '练习';
						$.ajax({
							url: "http://127.0.0.1:8081/common/get-answer",
							async: false,
							type: "POST",
							contentType: 'application/json',
							dataType: 'json',
							data: JSON.stringify(vo),
							success: function(json) {
								layui.use('layer', function() {
									var layer = layui.layer;

									// 弹出提示框
									layer.msg('您的分数为:' + number + '分', {
										icon: 6, // 图标样式,默认为信息图标
										time: 2000, // 显示时间,默认为2秒
										shade: 0.5, // 遮罩层透明度,默认为0.3
										shadeClose: true // 是否点击遮罩关闭弹框,默认为true
									});
								});
							}
						});


					},
					//点击收藏
					collection(id) {
						var vo = {};
						var user = JSON.parse(sessionStorage.getItem('user'));
						vo.userId = user.id;
						vo.answerId = id;
						$.ajax({
							url: "http://127.0.0.1:8081/common/addCollect",
							async: false,
							type: "POST",
							contentType: 'application/json',
							dataType: 'json',
							data: JSON.stringify(vo),
							success: function(json) {
								layui.use('layer', function() {
									var layer = layui.layer;

									// 弹出提示框
									layer.msg(json.returnMsg, {
										icon: json.returnMsg == '您已收藏过' ? 5 :
										6, // 图标样式,默认为信息图标
										time: 2000, // 显示时间,默认为2秒
										shade: 0.5, // 遮罩层透明度,默认为0.3
										shadeClose: true // 是否点击遮罩关闭弹框,默认为true
									});
								});
							}
						});

					},

				}, //方法结束
				created: function() {
					var vm = this;
					vm.GetAll();
				}, //初始加载方法结束

			}); //vue结束

			function GetQueryString(name) {
				var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
				var r = window.location.search.substr(1).match(reg);
				if (r != null) return unescape(r[2]);
				return null;
			}
		</script>
	</body>
</html>
html 复制代码
<head>
		<meta charset="utf-8" />
		<title>答题统计</title>
	</head>
	<link href="css/index.css" rel="stylesheet" />
	<link href="css/bootstrap.min.css" rel="stylesheet" />
	<body style="background-color: #f7f7f7;">
		<div class="headerBox">
			<div class="logoBox">
				<img src="img/logo1.png" />
				<div class="logoTitle">在线答题闯关</div>
			</div>
			<div class="menuBox">
				<div class="menuItem  blackColor">
					<a href="index.html">练习模式</a>
				</div>
				<div class="menuItem blackColor">
					<a href="challengeLevels.html?param=primary">闯关模式</a>
				</div>
				<div class="menuItem blackColor">
					<a href="wrongQuestion.html">我的错题</a>
				</div>
				<div class="menuItem blackColor">
					<a href="myCollection.html">我的收藏</a>
				</div>
				<div class="menuItem activeMenu">
					<a href="statistics.html">答题统计</a>
				</div>
				<div class="menuItem blackColor">
					<a href="center.html">个人中心</a>
				</div>
				<div class="menuItem blackColor">
					<a href="./login/login.html">退出登录</a>
				</div>
			</div>
		</div>
		<div class="container-fluid" id="content-page">
			<div class="row">
				<div class="col-md-2">&nbsp;</div>
				<div class="col-md-8">
					<div class="searchBox">
						<div class="leftTitle">
							答题统计
						</div>
						<div>

						</div>
					</div>
				</div>
				<div class="col-md-2">&nbsp;</div>
			</div>
			<div class="row">
				<div class="col-md-2">&nbsp;</div>
				<div class="col-md-4">
					<div id="main"></div>
				</div>
				<div class="col-md-4">
					<div id="main2"></div>
				</div>
				<div class="col-md-2">&nbsp;</div>
			</div>
		</div>
		<script type="text/javascript" src="js/jquery.min.js"></script>
		<script type="text/javascript" src="js/echarts.min.js"></script>
		<script>
			function GetQueryString(name) {
				var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
				var r = window.location.search.substr(1).match(reg);
				if (r != null) return unescape(r[2]);
				return null;
			}


			var user = JSON.parse(sessionStorage.getItem('user'));
			$.ajax({
				url: "http://127.0.0.1:8081/common/total?userId=" + user.id,
				async: false,
				type: "POST",
				contentType: 'application/json',
				dataType: 'json',
				data: JSON.stringify({}),
				success: function(json) {

					initCharts1(json.data);
					var inputArray = json.data;
					// 定义对应的题目类型名称数组
					var typeNameArray = ['单选题', '多选题', '判断题'];
					
					// 结果数组
					var resultArray = [];
					
					// 遍历输入数组
					for (var i = 0; i < inputArray.length; i++) {
					    // 构造对象,并添加到结果数组中
					    var item = {
					        value: inputArray[i],
					        name: typeNameArray[i]
					    };
					    resultArray.push(item);
					}
					initCharts(resultArray);
				}
			});


			function initCharts(data1) {
				var chartDom = document.getElementById('main');
				var myChart = echarts.init(chartDom);
				var option;
				option = {
					title: {
						text: '答题数统计',
						subtext: '',
						left: 'center'
					},
					tooltip: {
						trigger: 'item'
					},
					legend: {
						orient: 'vertical',
						left: 'left'
					},
					series: [{
						name: 'Access From',
						type: 'pie',
						radius: '50%',
						data: data1,
						emphasis: {
							itemStyle: {
								shadowBlur: 10,
								shadowOffsetX: 0,
								shadowColor: 'rgba(0, 0, 0, 0.5)'
							}
						}
					}]
				};
				myChart.setOption(option);
			};

			function initCharts1(data2) {
				var chartDom = document.getElementById('main2');
				var myChart = echarts.init(chartDom);
				var option;

				option = {
					xAxis: {
						type: 'category',
						data: ['单选题', '多选题', '判断题']
					},
					yAxis: {
						type: 'value'
					},
					series: [{
						data: data2,
						type: 'bar',
						showBackground: true,
						backgroundStyle: {
							color: 'rgba(180, 180, 180, 0.2)'
						}
					}]
				};

				myChart.setOption(option);
			};
		</script>
	</body>
</html>
相关推荐
_oP_i26 分钟前
Pinpoint 是一个开源的分布式追踪系统
java·分布式·开源
mmsx29 分钟前
android sqlite 数据库简单封装示例(java)
android·java·数据库
武子康1 小时前
大数据-258 离线数仓 - Griffin架构 配置安装 Livy 架构设计 解压配置 Hadoop Hive
java·大数据·数据仓库·hive·hadoop·架构
豪宇刘2 小时前
MyBatis的面试题以及详细解答二
java·servlet·tomcat
秋恬意2 小时前
Mybatis能执行一对一、一对多的关联查询吗?都有哪些实现方式,以及它们之间的区别
java·数据库·mybatis
刘大辉在路上2 小时前
突发!!!GitLab停止为中国大陆、港澳地区提供服务,60天内需迁移账号否则将被删除
git·后端·gitlab·版本管理·源代码管理
FF在路上3 小时前
Knife4j调试实体类传参扁平化模式修改:default-flat-param-object: true
java·开发语言
真的很上进3 小时前
如何借助 Babel+TS+ESLint 构建现代 JS 工程环境?
java·前端·javascript·css·react.js·vue·html
web130933203983 小时前
vue elementUI form组件动态添加el-form-item并且动态添加rules必填项校验方法
前端·vue.js·elementui
众拾达人3 小时前
Android自动化测试实战 Java篇 主流工具 框架 脚本
android·java·开发语言