基于Springboot+Vue实现的在线答题闯关系统全过程
作者:无语小咪
前言
本系统采用前后端分离架构,前端使用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实体类文件夹,并在该文件夹下创建项目用到的实体类
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’ | 存储日期和时间 |
参考代码块
<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" rel="external nofollow" rel="external nofollow" >练习模式</a> </div> <div class="menuItem blackColor"> <a href="challengeLevels.html?param=primary" rel="external nofollow" rel="external nofollow" >闯关模式</a> </div> <div class="menuItem blackColor"> <a href="wrongQuestion.html" rel="external nofollow" rel="external nofollow" >我的错题</a> </div> <div class="menuItem blackColor"> <a href="myCollection.html" rel="external nofollow" rel="external nofollow" >我的收藏</a> </div> <div class="menuItem blackColor"> <a href="statistics.html" rel="external nofollow" rel="external nofollow" >答题统计</a> </div> <div class="menuItem blackColor"> <a href="center.html" rel="external nofollow" rel="external nofollow" >个人中心</a> </div> <div class="menuItem blackColor"> <a href="./login/login.html" rel="external nofollow" rel="external nofollow" >退出登录</a> </div> </div> </div> <div class="container-fluid" id="content-page"> <div class="row"> <div class="col-md-2"> </div> <div class="col-md-8"> <div class="searchBox"> <div class="leftTitle"> {{pageTitle}} </div> </div> </div> <div class="col-md-2"> </div> </div> <div class="row"> <div class="col-md-2"> </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"> </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 + "¶m=" + 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>
<head> <meta charset="utf-8" /> <title>答题统计</title> </head> <link href="css/index.css" rel="external nofollow" rel="stylesheet" /> <link href="css/bootstrap.min.css" rel="external nofollow" 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" rel="external nofollow" rel="external nofollow" >练习模式</a> </div> <div class="menuItem blackColor"> <a href="challengeLevels.html?param=primary" rel="external nofollow" rel="external nofollow" >闯关模式</a> </div> <div class="menuItem blackColor"> <a href="wrongQuestion.html" rel="external nofollow" rel="external nofollow" >我的错题</a> </div> <div class="menuItem blackColor"> <a href="myCollection.html" rel="external nofollow" rel="external nofollow" >我的收藏</a> </div> <div class="menuItem activeMenu"> <a href="statistics.html" rel="external nofollow" rel="external nofollow" >答题统计</a> </div> <div class="menuItem blackColor"> <a href="center.html" rel="external nofollow" rel="external nofollow" >个人中心</a> </div> <div class="menuItem blackColor"> <a href="./login/login.html" rel="external nofollow" rel="external nofollow" >退出登录</a> </div> </div> </div> <div class="container-fluid" id="content-page"> <div class="row"> <div class="col-md-2"> </div> <div class="col-md-8"> <div class="searchBox"> <div class="leftTitle"> 答题统计 </div> <div> </div> </div> </div> <div class="col-md-2"> </div> </div> <div class="row"> <div class="col-md-2"> </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"> </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>
总结
到此这篇关于基于Springboot+Vue实现的在线答题闯关系统的文章就介绍到这了,更多相关Springboot+Vue在线答题闯关系统内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!