内容摘要:基于前后端分离的考试学习系统课程设计旨在开发一款满足学生、教师和管理员需求的在线考试和学习平台。主要参考超星学习通考试的系统,系统采用前、后端分离的技术实现,前端使用HTML5、CSS3和JavaScript、node.js、vue3及若依框架来实现页面框架搭建、布局、样式和交互,后端使用SSM框架搭建服务器、mysql创建数据库存储用户信息和考试资料。系统实现包括登录、主页、考试、练习题等功能界面的实现,还实现的调用摄像头的诚信考试功能模块的搭建,同时处理异常和跨域请求,保护系统安全。最终进行系统测试和优化以确保满足性能需求。
关键词:考试学习系统 node.js vue SSM mysql
在信息技术高速发展的今天,新知识、新技术层出不穷,计算机技术早已广泛的应用于各行各业之中,利用计算机的强大数据处理能力和辅助决策能力叫,实现行业管理的规范化、标准化、效率化。
管理信息系统(Management Information System,简称MIS〉是一个以人为主导,利用计算机软硬件技术以及网络通信技术,实现对信息的收集、传输、储存、更新。
目前,管理信息系统广泛采用Java技术作为开发的主要技术。在经过多年的技术积累与更新,Java技术已经从一种简单的信息浏览和信息交互平台发展为复杂的JavaEE企业级框架技术。
而随着在线学习平台的兴起,考试学习系统成为了学生和教师之间的重要桥梁。传统的考试学习系统往往采用前端和后端一体化的设计模式,这使得系统的维护和扩展变得困难。为了解决这个问题,我们设计了一个基于前后端分离的考试学习系统。该系统主要使用Node.js、Vue、SSM(Spring、SpringMVC、MyBatis)和MySQL等技术实现。
1 工商考试系统研究现状及意义
1.1工商考试学习系统研究现状
学习通考试系统是一个综合性的评估系统,旨在通过客观的测试题目来评估学习者的知识和技能水平。近年来,随着技术的发展,考试系统已经经历了从传统纸质考试到数字化考试,再到在线考试系统的转变。
目前,国内外已经开发出多种类型的考试系统,包括但不限于以下几种:
- 基于Web的在线考试系统:这种类型的考试系统主要依托于互联网平台,具有灵活、便捷的特点,可以实现远程在线考试、自动评卷等功能。
- 基于移动设备的考试系统:随着移动设备的发展,移动端的在线考试系统也逐渐普及。该类型考试系统主要针对手机、平板电脑等移动设备,可以实现在线答题、自动评卷等功能。
- 智能考试系统:智能考试系统是一种基于人工智能技术的考试系统,可以根据学习者的学习情况和知识水平动态生成试卷,实现个性化测试和评估。
此外,根据不同的应用场景和目的,考试系统还有多种分类方式。例如,根据考试科目的不同,可以分为英语考试系统、数学考试系统、计算机考试系统等;根据使用对象的不同,可以分为学生考试系统、教师考试系统、企业培训考试系统等。
总的来说,随着技术的不断发展,考试系统的功能和性能也在不断提升和完善。未来,随着人工智能、大数据等技术的进一步发展,考试系统将会更加智能化、个性化和高效化。
图表 1 考试系统设计意义思维导图
1.2工商考试系统设计意义
工商考试系统设计的意义主要体现在以下几个方面:
- 提高考试效率:工商考试系统可以实现自动化、智能化的考试流程,包括自动组卷、自动评分、自动统计等,大大提高了考试效率。
- 降低考试成本:传统的纸质考试需要大量的人力和物力资源,而工商考试系统可以实现在线考试,无需印刷试卷和安排考场等,大大降低了考试成本。
- 实现考试公平公正:工商考试系统可以实现自动化评卷和统计,避免了人为因素的干扰,保证了考试的公平公正。
- 促进教育培训发展:工商考试系统可以针对不同的岗位和职业需求,设计不同的考试科目和内容,为工商企业提供更全面、更专业的教育培训服务,有利于提高员工的专业素质和能力。
- 推动数字化转型:工商考试系统是数字化转型的重要组成部分,可以提高企业的信息化水平,促进企业的数字化转型。
综上所述,工商考试系统设计具有提高效率、降低成本、实现公平公正、促进教育培训发展以及推动数字化转型等多方面的意义。
1.3 工商考试系统设计流程
工商考试系统设计流程是一个系统性的过程,一般包括以下几个步骤:
- 需求分析:明确考试系统的需求,包括考试科目、考试形式、考试时间、考试地点、考生信息管理、成绩查询等功能需求。
- 系统设计:根据需求分析结果,设计考试系统的整体架构和各个功能模块,包括用户管理、考试管理、成绩管理等功能模块。
- 技术选型:根据系统设计和需求,选择合适的技术和开发工具,包括前端技术、后端技术、数据库技术等。
- 系统开发:按照系统设计和技术选型结果,进行系统的开发和实现,包括各个功能模块的开发和测试。
- 系统测试:对开发完成的工商考试系统进行测试,包括功能测试、性能测试、安全测试等,确保系统的稳定性和可靠性。
- 上线运行:将工商考试系统部署到服务器上,正式上线运行,并进行持续的维护和升级。
- 数据分析与优化:对系统运行过程中产生的数据进行分析,以便优化系统的性能和提高考试效率。
以上是工商考试系统设计的一般流程,具体实现过程可能会因实际需求和技术选择而有所不同。同时,随着技术的不断发展和考试需求的变化,工商考试系统也需要不断进行升级和优化。
2 系统设计实现功能
2.1 需求分析
工商考试学习管理系统需要满足不同使用者的需求,如教师、学生和系统管理员。学生需要能够方便地进行注册、登录、查看考试科目、做题、考试等操作;教师需要能够方便地进行题库管理、试卷生成、考试监控、成绩管理、学习记录管理、在线交流等操作;管理员针对人员注册、权限进行管理。工商考试学习系统是一个多角色在线培训考试系统,系统集成了用户管理、角色管理、题库管理、试 题管理、考试管理、在线考试等功能,考试流程完善。实现一整套完整体系的考试系统,方便用 户在此系统中进行练习并不断提升自己,在考试中不断进步。
2.2 需求功能描述
2.2.1 权限控制
本系统存在三个不同的角色,教师,管理员,学生三种用户,此系统是基于vue+springboot实现的前后端分离,用户权限校验通过JWT生成token令牌发放到用户,并根据令牌对用户的身份合法性进行校验。
2.2.2在线考试
学生用户在注册登录之后,可以在本系统进行在线的考试,考试可由教师和管理员进行布置并设置考试权限(公开,密码),考试题型分为 单选、多选、判断、简答题,并支持题目附带配图。考试过程中需开启摄像头进行考试,系统会自动抓拍考生实时考试状态。
2.2.3 成绩模块
参加考试后的学生用户,在提交试卷后进入考试结果页面,页面会自动核对学生用户的逻辑题的对错,对于简答题需要老师或者超级管理员进行批阅。对于学生用户参与的考试,学生用户可以查看到考试的详情并可以查看到自己所错的逻辑题。
2.2.4 题库模块
学生用户在题库模块中可以进行题目的功能训练,训练模式分为,顺序练习,随机练习,也可以根据题型练习(单选,多选,判断)。用户答题时会实时判断正确与否,并有错题解析功能。
2.2.5 题库管理
超级管理员和教师可以对本考试系统已有的题库进行管理,实现对题库信息的CRUD操作
2.2.6 试题管理
教师和系统管理员用户有权限对本系统的所有试题进行操作,本系统试题支持复杂类型的题目,考试题目支持多插图,选项答案支持单插图功能。
2.2.7 考试管理
教师和系统管理员用户有权限对系统存在的考试进行操作,本系统考试支持公开考试和密码口令考试,并可以对考试进行禁用也可以在设置考试时间段,对于考试可以进行很便利的进行组卷,系统内置两种组卷模式,题库组卷和自由选题组卷。
2.2.8 考卷批阅
对于本系统中存在的复杂考试的题目,可以又对应的老师进行批阅,此系统的逻辑题无需老师用户进行批阅,老师的工作仅仅是批阅简答题这种无准确答案类型的题目,极大地减轻了老师用户的工作量
2.2.9 考试统计
本系统针对每一次考试进行数据统计和报表,让使用本系统的老师用户能够直观的了解到每一次考试人员的进步。
2.2.10 用户管理
超级管理员可以对注册本系统用户的进行授权,并拥有操作一切用户的权限。
2.3 管理系统项目架构
2.3.1 后端框架结构
SSM后端框架实现的功能主要是接收请求并处理数据,同时保障系统的安全性和可扩展性,使得系统易于维护和使用。后端框架结构包括SpringMVC、Mybatis和Spring三个部分。这些部分共同协作,实现了后端应用程序的开发和运行。同时在项目开发中我使用了若依框架进行快速开发。若依框架的项目结构是基于Spring Boot和Spring Security的快速开发平台,通过分层设计使得代码结构更加清晰和易于维护。同时,若依框架还提供了丰富的功能模块和插件机制,方便我们使用中进行扩展和定制。
图表 2 系统项目架构
2.3.2 前端项目结构
前端Vue框架可以实现构建用户界面、数据绑定、组件化、指令和过滤器、路由和状态管理、集成第三方库以及易于学习和使用等功能,考试管理系统在前端使用Vue框架可以实现更高效、更直观、更易于维护和扩展的前端页面开发。
我们使用Vue.js作为前端框架,它是建立在JavaScript ES6基础之上的,可以创建用户界面和处理用户交互。Vue.js具有组件化的特点,我们可以创建可重用的自定义元素,这使得代码更具可维护性和可读性。为了管理状态,我们使用Vuex,它是Vue.js的状态管理模式和库。在这里我使用的vue2.x的开发版本以及node.js16版本,为了适配一些编程语法的写法,我将数据库改成了5.7,高版本可以向下兼容,除了修改驱动以外无特殊修改说明的地方。此外,我们利用Vue Router来处理路由,实现页面之间的切换和跳转。这意味着当用户在网站的不同页面之间跳转时,服务器不需要为每个页面发送新的请求。
在后端,我们使用Node.js和Express.js来创建服务器和处理请求。Node.js是一个能运行在服务器端的JavaScript运行环境,它使得开发人员可以使用JavaScript来创建服务器端应用程序。Express.js是一个建立在Node.js上的web应用框架。
前后端之间的连接是通过API实现的。我们在后端创建RESTful API,它允许前端发送请求并获取数据。前端通过调用这些API来获取数据并进行展示。
最终的实现效果是:用户可以通过访问我们的网站(前端)得到所需的考试信息,如试题、考试成绩等。他们可以在网站上查看考试详情,也可以进行用户注册和登录等操作。所有的数据交互都是通过后端服务器处理的,前端和后端通过API进行连接和通信。
图表 3 前端项目目录结构
在工商考试系统的后端框架搭建方面,应本次实训要求,使用Spring Boot作为后端框架,其中有参考学习通仿例及若依框架的学习。Spring Boot是一个基于Java的开源框架,它可以帮助开发人员快速构建和部署基于Spring的应用程序。
以下是使用Spring Boot框架搭建工商考试系统后端框架的基本步骤:
- 创建Spring Boot项目:使用Spring Initializr或STS等工具创建一个新的Spring Boot项目。
- 配置数据库连接:在application.properties或application.yml文件中配置数据库连接信息,包括数据库URL、用户名和密码等。
- 创建数据模型:根据工商考试系统的需求,创建相应的数据模型类,例如考生信息、考试科目、考试成绩等。
- 定义数据访问对象(DAO):使用JPA或MyBatis等持久层框架定义数据访问对象,实现数据访问层的功能。
- 创建服务层:根据业务需求,创建服务层,封装具体的业务逻辑,例如考试报名、成绩查询等。
- 创建控制器(Controller):根据系统的需求,创建控制器,处理前端的请求并返回相应的响应,例如RESTful API。
- 配置安全性:在Spring Boot中可以配置安全性相关的功能,例如用户认证和授权、防止跨站请求伪造(CSRF)等。
- 配置日志和监控:使用Spring Boot的日志和监控功能,记录系统的运行状态和性能指标。
- 部署和测试:将应用程序部署到服务器上并进行测试,确保系统的稳定性和功能性。
通过以上步骤,可以搭建起工商考试系统的后端框架,实现系统的基本功能和安全性要求。
同时,结合前端框架(如Vue)的使用,可以构建出完整的工商考试系统。
图表 4 SpringBoot项目启动 |
3.2.1 数据库设计流程:
在创作的工商考试管理系统的数据库设计需要考虑以下几个方面:
- 实体关系设计:根据系统需求,确定各个实体之间的关系,例如考生与考试科目之间的关系、考试科目与考试成绩之间的关系等。
- 数据表设计:为每个实体创建相应的数据表,确定每个表的字段和数据类型,例如考生信息表、考试科目表、考试成绩表等。
- 主键与外键设计:为每个表设置主键和外键,确保数据的唯一性和关联性。
- 约束设计:为每个表的字段设置相应的约束,例如非空约束、唯一约束、外键约束等。
- 索引设计:根据查询需求,为表中的字段创建索引,提高查询效率。
- 视图设计:根据系统需求,创建视图来简化数据操作和展示。
- 存储过程与触发器设计:根据业务需求,创建存储过程和触发器来实现复杂的业务逻辑和数据操作。
在数据库设计中,需要综合考虑系统的功能需求、数据量、性能等多个方面,以确保设计的合理性和可扩展性。同时,需要遵循数据库设计的范式原则,减少数据冗余和保证数据的一致性。以上是我的工商考试系统数据库设计的基本思路。
图表 5 数据库表结构 |
一共设计了考试试题表,试题表,问题表,信息表,用户表,权限表,问题登记表,公告表。
Answer表
exam表
exam_question表
exam_recordnotice表
question表
question_bank表
user表
user_role表
使用Node.js和Vue搭建考试管理系统前端框架的实现步骤如下:
- 安装Node.js和Vue CLI:首先,确保你的计算机上已经安装了Node.js和Vue CLI。
- 创建Vue项目:使用Vue CLI创建一个新的Vue项目。在命令行中执行以下命令:
“vue create exam-system”
根据提示选择适合的配置选项,或直接按回车键使用默认配置。
- 安装所需的依赖:进入项目文件夹,并安装所需的依赖包。执行以下命令:
“npm install”
这将安装项目所需的依赖包,包括Vue核心库、Vue Router、Vuex等。
- 创建Vue组件:在src文件夹中创建Vue组件,用于构建用户界面。同时我们使用了element组件来直接完成界面的设计,采用模板的方式让我们更加便捷的开发一个项目,并且在命名方面不会出现例如中英文混杂这样的细节问题。
- 配置路由:在src文件夹中创建一个router文件夹,并定义路由配置。在router/index.js文件中,可以定义各个页面的路由信息,例如考题类型、考试内容等。通过使用Vue Router,我们可以实现页面的切换和跳转。
- 创建Vuex store:在src文件夹中创建一个store文件夹,并定义Vuex store。Vuex是Vue的状态管理库,它可以集中存储和管理应用程序的状态。在store/index.js文件中,可以定义全局状态管理,并在各个组件中通过使用mapActions和mapGetters等辅助函数来访问和更新状态。
- 构建前端应用:在项目根目录下执行以下命令,构建前端应用:
“npm run build”
这将生成dist文件夹,其中包含构建后的前端应用文件。可以将这些文件部署到Web服务器上,以供用户访问。
- 配置后端API接口:在后端服务器中创建API接口,用于与前端进行通信。我们使用Node.js和Express框架来创建RESTful API。在后端服务器中定义相应的路由和处理逻辑,以实现数据的增删改查等操作。
- 前后端联调:将前端应用和后端服务器进行联调,确保前后端之间的数据交互和功能实现正常。可以使用工具如Postman来测试API接口,并在前端中调用这些接口来实现数据的动态更新和页面渲染。
- 测试与部署:对整个系统进行测试,包括功能测试、性能测试和安全测试等。确保系统的稳定性和正确性。然后将系统部署到服务器上,供用户访问和使用。
以上是使用Node.js和Vue搭建考试管理系统前端框架的基本实现步骤。
图表 6 vue项目启动 |
登录界面的作用主要是提供用户身份验证和授权的功能。用户可以通过输入用户名/邮箱和密码来进行身份验证,如果输入的信息与数据库中存储的信息匹配,则验证成功,用户就可以进入系统进行相应的操作。如果验证失败,则用户无法进入系统。同时,登录界面还可以提供权限控制的功能,不同权限的用户可以访问不同的系统功能。这样,登录界面可以有效地保护系统的安全性和稳定性。
图表 7 登录界面 |
图表 8注册界面(用户名存在,验证信息错误) |
注册界面主要是用于收集用户信息,并创建新的用户帐户。用户可以在此输入他们的用户名、密码、姓名和其他相关信息,系统会将这些信息存储在数据库中,并创建一个新的用户帐户。一旦用户帐户创建成功,用户就可以使用他们的用户名和密码来访问和操作系统。注册界面还可以提供数据验证和错误处理机制,以确保用户输入的数据符合要求,并在出现错误时提供相应的提示信息。下面就是用户名已经存在以及验证码错误的情况。
注册成功以后自动跳转登陆界面:
图表 9 注册后跳转登录界面 |
图表 10公告栏弹窗
项目主界面分为产品介绍以及在线考试系统介绍,主界面在每一个权限下都是相同的,但是只有管理员和教师有题库修改权限,只有管理员具有定义用户权限以及公告栏修改权限。
产品介绍页面用于展示考试系统的基本功能和特点,以吸引用户了解和使用该系统。在该页面中,可以介绍考试系统的背景、目的和功能,以及系统所针对的用户群体。此外,还可以展示系统的优势和特点,例如其易用性、可靠性、高效性和安全性等。通过产品介绍页面,用户可以快速了解系统的基
图表 11主界面产品介绍 |
本情况,并决定是否需要进一步了解和使用该系统。
图表 12考试科目类别 |
在线考试分为三个子模块:在线考试、我的成绩、我的题库,该界面主要是考试系统的考试功能。
图表 13 考试记录 |
图表 14 考试题库
管理员具有整个系统的操作权限。
主要分为:考试管理、考试信息统计、系统设置等权限;考试管理主要包括:题库、试题、考试安排、阅卷等管理;考试信息统计:主要通过数据分析得出考试通过率、考试科目各科已考次数占比;系统设置中包括公告管理,有公告信息的发布和修改,角色管理(教师、学生、管理员三类角色)、以及注册后的用户账号密码等信息管理。
这些功能实现都是基于增删改查所做的基本操作,调用接口处理数据请求返回数据。
图表 15管理员界面
图表 16 题库管理
图表 17考试管理
图表 18 考试管理
图表 19阅卷管理
图表 20考试信息统计表
图表 21公告管理
图表 22角色管理
图表 23用户管理
4.6.1 教师端功能界面概述
教师端功能界面是为教师设计的,用于管理和评估学生的在线考试。该界面提供了丰富的功能,让教师可以轻松地对学生的考试进行批阅评分,并对题库、试卷和考试信息进行发布、编辑和删除等操作。
4.6.1教师端功能界面介绍
- 题库管理:题库是考试系统的核心之一,教师可以通过题库管理功能添加、编辑和删除试题。教师可以选择不同的题型(如选择题、填空题、问答题等)和难度等级,并输入相应的题目内容和答案。题库管理界面还提供了搜索和排序功能,方便教师快速找到所需的试题。
- 试卷管理:试卷是考试的载体,教师可以通过试卷管理功能创建和编辑试卷。教师可以选择从题库中选题,设置试卷的难度等级和考试时间,并添加个性化的试卷说明。同时,试卷管理界面还提供了预览和打印功能,方便教师核对试卷信息和打印试卷。
- 考试信息发布:教师可以通过考试信息发布功能发布考试通知和相关要求。教师可以选择发布考试的日期、时间、地点和注意事项,并通过系统通知或邮件等方式通知学生参加考试。
- 学生管理:教师可以通过学生管理功能添加、编辑和删除学生的信息。教师可以输入学生的姓名、学号、班级等基本信息,并为学生分配唯一的账号和密码。学生管理界面还提供了搜索和排序功能,方便教师快速找到特定的学生。
- 考试批阅评分:在考试结束后,教师可以登录教师端功能界面对学生的考试进行批阅评分。批阅评分界面展示了学生的基本信息和试卷答案,教师可以根据标准答案对学生的答案进行评分和给出相应的评语。批阅评分界面还提供了统计和分析功能,方便教师了解学生的整体表现和需要改进的地方。
- 数据统计和分析:教师可以通过数据统计和分析功能查看和评估考试的相关数据。教师可以查看每个学生的考试成绩、答题时间和错误率等详细信息,并根据这些信息评估学生的学习情况和表现。此外,教师还可以生成报告和图表,以便更好地了解学生的整体表现和需要改进的地方。
4.6.3 界面展示
图表 24 添加试卷 |
图表 26设置考题 |
图表27 设置试卷属性 |
图表 28 发布考题 |
图表 29试卷批阅
4.7.1学生界面概述
考试系统学生端功能学生界面是专为学生设计的,以提供便捷的在线考试体验。在这个界面上,学生可以轻松查看教师发布的考试科目和相关通知,选择自己想要参加的科目,并随时查看自己的考试成绩和表现。此外,学生还可以进行个人设置,如修改个人信息等。这个界面设计简洁明了,操作便捷,旨在为学生提供更好的在线考试体验。
4.7.2主要功能界面展示
图表 30 进入考试
图表 31 诚信考试界面
图表 32考试成绩管理界面
图表 33练习题库
5.1 后端返回参数展示
图表 34后端数据请求 |
图表 35后端返回数据
5.2 主要功能介绍
工商诚信考试系统是一个综合性的考试平台,旨在提供公正、公平、透明的考试环境。以下是该系统的主要功能介绍:
- 学生信息管理:系统可以管理学生的基本信息,包括姓名、身份证号、联系方式等。
- 考试科目管理:系统可以添加和管理不同的考试科目,每个科目都有独立的题库和考试时间。
- 考试安排管理:系统可以安排考试时间和内容,并在公告栏弹窗的方式通知学生进行考试。
- 诚信监考:系统可以实时监控考试过程,同时记录学生的诚信记录,如前面所示考试情况,我们会打开摄像头调用摄像功能来对考试者进行图像采集,确保本人无干扰考试,防止作弊行为。
- 成绩管理:系统可以自动评分并管理学生成绩,提供成绩查询和证书打印功能。
5.3 核心代码展示(详见附录1)
5.3.1 前端代码
- 主页index界面“
<template>
<el-scrollbar :native="false" style="height: 100%">
<div>
<h1 class="title">
<a href="https://gitee.com/Ye_Jiang1/exam_test" target="_blank">工商考试系统</a>
</h1>
<el-row>
<el-col :span="15" :offset="5">
<h3>1、推荐项目:</h3>
<ul>
<li><a href="https://gitee.com/Ye_Jiang1/exam_test" target="_blank">个人仓库主页</a></li>
<li><a href="https://blog.csdn.net/m0_65137405?spm=1000.2115.3001.5343" target="_blank">个人博客主页</a></li>
</ul>
<h3>2、系统描述:</h3>
<p>工商考试学习系统是一个多角色在线培训考试系统,系统集成了用户管理、角色管理、题库管理、试
题管理、考试管理、在线考试等功能,考试流程完善。实现一整套完整体系的考试系统,方便用
户在此系统中进行练习并不断提升自己,在考试中不断进步。</p>
<h3>3、主要功能</h3>
<div style="margin-left: 2em">
<h4>1.权限控制</h4>
<p>本系统存在三个不同的角色,教师,管理员,学生三种用户,此系统是基于vue+springboot实现的前后端分离,用户权限校验通过JWT生成token令牌发放到用户,并根据令牌对用户的身份合法性进行校验。</p>
<h4>2.在线考试</h4>
<p>学生用户在注册登录之后,可以在本系统进行在线的考试,考试可由教师和管理员进行布置并设置考试权限(公开,密码),考试题型分为
单选、多选、判断、简答题,并支持题目附带配图。考试过程中需开启摄像头进行考试,系统会自动抓拍考生实时考试状态。</p>
<h4>3.成绩模块</h4>
<p>
参加考试后的学生用户,在提交试卷后进入考试结果页面,页面会自动核对学生用户的逻辑题的对错,对于简答题需要老师或者超级管理员进行批阅。对于学生用户参与的考试,学生用户可以查看到考试的详情并可以查看到自己所错的逻辑题。</p>
<h4>4.题库模块</h4>
<p>学生用户在题库模块中可以进行题目的功能训练,训练模式分为,顺序练习,随机练习,也可以根据题型练习(单选,多选,判断)。用户答题时会实时判断正确与否,并有错题解析功能。</p>
<h4>5.题库管理</h4>
<p>超级管理员和教师可以对本考试系统已有的题库进行管理,实现对题库信息的CRUD操作</p>
<h4>6.试题管理</h4>
<p>教师和系统管理员用户有权限对本系统的所有试题进行操作,本系统试题支持复杂类型的题目,考试题目支持多插图,选项答案支持单插图功能。</p>
<h4>7.考试管理</h4>
<p>
教师和系统管理员用户有权限对系统存在的考试进行操作,本系统考试支持公开考试和密码口令考试,并可以对考试进行禁用也可以在设置考试时间段,对于考试可以进行很便利的进行组卷,系统内置两种组卷模式,题库组卷和自由选题组卷。</p>
<h4>8.考卷批阅</h4>
<p>对于本系统中存在的复杂考试的题目,可以又对应的老师进行批阅,此系统的逻辑题无需老师用户进行批阅,老师的工作仅仅是批阅简答题这种无准确答案类型的题目,极大地减轻了老师用户的工作量</p>
<h4>9.考试统计</h4>
<p>本系统针对每一次考试进行数据统计和报表,让使用本系统的老师用户能够直观的了解到每一次考试人员的进步。</p>
<h4>10. 用户管理</h4>
<p>超级管理员可以对注册本系统用户的进行授权,并拥有操作一切用户的权限。</p>
</div>
</el-col>
</el-row>
</div>
</el-scrollbar>
</template>
<script>
export default {
name: 'Dashboard',
created () {
// 调用父组件Main的展示系统公告方法
this.$emit('showSystemNotice')
},
}
</script>
<style scoped lang="scss">
div {
animation: leftMoveIn .7s ease-in;
}
@keyframes leftMoveIn {
0% {
transform: translateX(-100%);
opacity: 0;
}
100% {
transform: translateX(0%);
opacity: 1;
}
}
.title {
text-align: center;
font-size: 25px;
}
p {
text-indent: 2em;
}
a {
text-decoration: none
}
</style>
- Main.vue
<template>
<el-container>
<!--用户头部菜单-->
<el-aside id="aside" width="210px">
<el-menu :default-active="activeMenu" @select="handleSelect" :router="true" :collapse="isCollapse">
<el-menu-item index="/index" disabled style="text-align: center">
<i class="el-icon-sunny"></i>
<span slot="title">
工商考试学习系统
</span>
</el-menu-item>
<!-- 单独的导航 -->
<el-menu-item @click="changeBreadInfo(menuInfo[0].topMenuName,menuInfo[0].topMenuName,menuInfo[0].url)"
index="/dashboard"
v-if="!menuInfo[0].submenu">
<i :class="menuInfo[0].topIcon"></i>
<span slot="title">{{ menuInfo[0].topMenuName }}</span>
</el-menu-item>
<!--具有子导航的-->
<el-submenu v-if="menu.submenu" v-for="(menu,index) in menuInfo" :key="index" :index="index+''">
<template slot="title">
<i :class="menu.topIcon"></i>
<span slot="title">{{ menu.topMenuName }}</span>
</template>
<!--子导航的分组-->
<el-menu-item-group>
<el-menu-item @click="changeBreadInfo(menu.topMenuName,sub.name,sub.url)" :index="sub.url"
v-for="(sub,index) in menu.submenu" :key="index">
<i :class="sub.icon"></i>
<span slot="title">{{ sub.name }}</span>
</el-menu-item>
</el-menu-item-group>
</el-submenu>
</el-menu>
</el-aside>
<!--右侧的面板-->
<el-main>
<el-container>
<el-header height="100px">
<el-card class="box-card">
<div slot="header">
<!--缩小图标-->
<el-tooltip class="item" effect="dark" content="缩小侧边栏" placement="top-start">
<i class="el-icon-s-fold" @click="changeIsCollapse"
style="cursor:pointer;font-size: 25px;font-weight: 100"></i>
</el-tooltip>
<!--面包屑-->
<el-breadcrumb style="margin-left: 15px">
<el-breadcrumb-item>{{ breadInfo.top }}</el-breadcrumb-item>
<el-breadcrumb-item>{{ breadInfo.sub }}</el-breadcrumb-item>
</el-breadcrumb>
<!--右侧的个人信息下拉框-->
<el-dropdown trigger="click" style="float: right;color: black;cursor:pointer;" @command="handleCommand">
<span class="el-dropdown-link">
{{ currentUserInfo.username }}
<i class="el-icon-caret-bottom"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="personInfo">个人资料</el-dropdown-item>
<el-dropdown-item command="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<!--右侧的放大图标-->
<el-tooltip effect="dark" content="全屏预览" placement="top-start">
<i class="el-icon-full-screen" id="full" @click="fullShow" style="float: right;margin-right:10px;
margin-bottom:5px;cursor:pointer;font-size: 25px;font-weight: 100"></i>
</el-tooltip>
<!--右侧的查看公告图标-->
<el-tooltip effect="dark" content="查看公告" placement="top-start">
<i class="el-icon-bell" @click="showSystemNotice" style="float: right;margin-right:10px;
margin-bottom:5px;cursor:pointer;font-size: 25px;font-weight: 100"></i>
</el-tooltip>
</div>
<!--卡片面板的主内容-->
<div>
<el-tag @close="handleClose(index)" v-for="(item,index) in tags"
type="info" size="small" :key="index" :class="item.highlight ? 'active' : ''"
:closable="item.name !== '产品介绍'" @click="changeHighlightTag(item.name)"
effect="plain">
<i class="el-icon-s-opportunity" style="margin-right: 2px"
v-if="item.highlight"></i>
{{ item.name }}
</el-tag>
</div>
</el-card>
</el-header>
<el-main style="margin-top: 25px;">
<router-view @giveChildChangeBreakInfo="giveChildChangeBreakInfo" @showSystemNotice="showSystemNotice"
@giveChildAddTag="giveChildAddTag" :tagInfo="tags" @updateTagInfo="updateTagInfo"></router-view>
</el-main>
</el-container>
<el-dialog title="更新用户信息" center :visible.sync="updateCurrentUserDialog">
<el-form :model="currentUserInfo2" :rules="updateUserFormRules" ref="updateUserForm">
<el-form-item label="用户名">
<el-input v-model="currentUserInfo2.username" disabled></el-input>
</el-form-item>
<el-form-item label="真实姓名" prop="trueName">
<el-input v-model="currentUserInfo2.trueName"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="currentUserInfo2.password" placeholder="不更改请留空"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="updateCurrentUserDialog = false">取 消</el-button>
<el-button type="primary" @click="updateCurrentUser">确 定</el-button>
</div>
</el-dialog>
</el-main>
</el-container>
</template>
<script>
export default {
name: 'Main',
data () {
const validatePassword = (rule, value, callback) => {
if (value === '') {
callback()
} else if (value.length < 5) {
callback(new Error('新密码少于5位数!'))
} else {
callback()
}
}
return {
//菜单信息
menuInfo: [
{
'topIcon': '',
'url': '',
'children': [
{
'url': ''
}
]
}
],
//面板是否收缩
isCollapse: false,
//当前是否全屏显示
isFullScreen: false,
//当前登录的用户信息
currentUserInfo: {
'username': ''
},
//当前激活的菜单
activeMenu: '',
//面包屑信息
breadInfo: {
'top': '产品介绍',//顶级菜单信息
'sub': '产品介绍'//当前的菜单信息
},
//面包屑下的标签数据
tags: [
{
'name': '产品介绍',
'url': '/dashboard',
'highlight': true
}
],
//跟新当前用户的信息的对话框
updateCurrentUserDialog: false,
//当前用户的信息
currentUserInfo2: {},
//更新信息表单信息
updateUserFormRules: {
trueName: [
{
required: true,
message: '请输入真实姓名',
trigger: 'blur'
}
],
password: [
{
validator: validatePassword,
trigger: 'blur'
}
]
}
}
},
created () {
this.getMenu()
//获取登录用户信息
this.getUserInfoByCheckToken()
},
mounted () {
//根据当前链接的hash设置对应高亮的菜单
this.activeMenu = window.location.hash.substring(1)
document.querySelector('.el-container').style.maxHeight = screen.height + 'px'
// 根据设备大小调整侧边栏
if (screen.width <= 1080) {
this.isCollapse = !this.isCollapse
document.querySelector('#aside').style.width = 65 + 'px'
document.querySelector('.el-container').style.minWidth = 1080 + 'px'
}
},
watch: {
//监察路径变化,改变菜单的高亮
'$route.path': function (o, n) {
this.activeMenu = o
//如果没有该标签就创建改标签
let flag = false
//判断是否含有改标签
this.tags.map(item => {
if (item.url === this.activeMenu) {//如果有含有该标签
flag = true
}
})
if (!flag) {//对应链接的标签不存在
//先找到该标签的名字
this.createHighlightTag()
} else {//改标签存在,则高亮
this.tags.map(item => {
//取消高亮别的标签
item.highlight = false
//高亮当前标签
if (item.url === this.activeMenu) {
item.highlight = true
}
})
}
}
},
methods: {
//查看系统公告
showSystemNotice () {
notice.getCurrentNewNotice().then((resp) => {
if (resp.code === 200) {
this.$alert(resp.data, '最新公告', {
dangerouslyUseHTMLString: true,
closeOnPressEscape: true,
lockScroll: false
})
} else {
this.$notify({
title: 'Tips',
message: '公告获取失败',
type: 'error',
duration: 2000
})
}
})
},
//根据token后台判断用户权限,传递相对应的菜单
getMenu () {
menu.getMenuInfo().then((resp) => {
if (resp.code === 200) {
this.menuInfo = JSON.parse(resp.data)
//根据链接创建不存在的tag标签并高亮
this.createHighlightTag()
} else {//后台认证失败,跳转登录页面
this.$message.error('权限认证失败')
this.$router.push('/')
}
})
},
//放大缩小侧边栏
changeIsCollapse () {
const aside = document.querySelector('#aside')
if (this.isCollapse) {
aside.style.width = 210 + 'px'
} else {
aside.style.width = 65 + 'px'
}
this.isCollapse = !this.isCollapse
},
//是否全屏显示
fullShow () {
let docElm = document.documentElement
const full = document.querySelector('#full')
if (this.isFullScreen) {//退出全屏模式
//切换图标样式
full.className = 'el-icon-full-screen'
//W3C
if (document.exitFullscreen) {
document.exitFullscreen()
}
//FireFox
else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen()
}
//Chrome等
else if (document.webkitCancelFullScreen) {
document.webkitCancelFullScreen()
}
//IE11
else if (document.msExitFullscreen) {
document.msExitFullscreen()
}
} else {//进入全屏模式
//W3C
//切换图标样式
full.className = 'el-icon-switch-button'
if (docElm.requestFullscreen) {
docElm.requestFullscreen()
}
//FireFox
else if (docElm.mozRequestFullScreen) {
docElm.mozRequestFullScreen()
}
//Chrome等
else if (docElm.webkitRequestFullScreen) {
docElm.webkitRequestFullScreen()
}
//IE11
else if (docElm.msRequestFullscreen) {
docElm.msRequestFullscreen()
}
}
//改变标志位
this.isFullScreen = !this.isFullScreen
},
//处理右上角下拉菜单的处理事件
handleCommand (command) {
if (command === 'logout') {//退出
this.logout()
} else if (command === 'personInfo') {
this.updateCurrentUserDialog = true
user.getCurrentUser().then((resp) => {
if (resp.code === 200) {
resp.data.password = ''
this.currentUserInfo2 = resp.data
}
})
}
},
//退出登录
async logout () {
window.localStorage.removeItem('authorization')
//右侧提示通知
this.$notify({
title: 'Tips',
message: '注销成功',
type: 'success',
duration: 2000
})
await this.$router.push('/')
},
//检查token获取其中的用户信息
getUserInfoByCheckToken () {
auth.checkToken().then(resp => {
this.currentUserInfo = resp.data
localStorage.setItem('username', this.currentUserInfo.username)
}).catch(err => {
this.$notify({
title: 'Tips',
message: err.response.data.errMsg,
type: 'error',
duration: 2000
})
localStorage.removeItem('authorization')
this.$router.push('/')
})
},
//关闭tag标签
handleClose (index) {//当前点击的tag的下标
if (this.tags[index].highlight) {
this.tags[index - 1].highlight = true
//关闭之后,路由调跳转,高亮菜单和标签
this.$router.push(this.tags[index - 1].url)
this.handleSelect(this.tags[index - 1].url)
}
this.tags.splice(index, 1)
},
//菜单的高亮变化
handleSelect (currentMenu) {
this.activeMenu = currentMenu
},
//处理面包屑信息和面包屑下的标签信息
changeBreadInfo (curTopMenuName, curMenuName, url) {
//面包屑信息
this.breadInfo.top = curTopMenuName
this.breadInfo.sub = curMenuName
//标签信息
let flag = false//当前是否有此菜单信息(防止无限点击,无线生成)
this.tags.map(item => {
if (item.name === curMenuName) flag = true
})
if (!flag) {//不存在当前点击的菜单
this.tags.push({
'name': curMenuName,
'url': url,
'highlight': true
})
} //高亮菜单tag
this.changeHighlightTag(curMenuName)
},
//处理高亮的tag
changeHighlightTag (curMenuName) {//当前需要高亮的名字
let curMenu
this.tags.map((item, i) => {
if (item.name === curMenuName) curMenu = item
item.highlight = item.name === curMenuName
})
//调用改变面包屑的方法
this.changeTopBreakInfo(curMenu.name)
this.$router.push(curMenu.url)
},
//创建当前高亮的tags
createHighlightTag () {
//根据链接创建不存在的tag标签并高亮
let menuName
this.menuInfo.map(item => {
if (item.submenu !== undefined) {
item.submenu.map(subItem => {
if (subItem.url === this.activeMenu) menuName = subItem.name
})
}
})
if (menuName !== undefined && this.tags.indexOf(menuName) === -1) {
this.tags.push({
'name': menuName,
'url': this.activeMenu,
'highlight': true
})
//高亮对应的标签
this.tags.map(item => {
if (item.url === window.location.hash.substring(1)) this.changeHighlightTag(item.name)
})
}
},
//改变头部的面包屑信息
changeTopBreakInfo (subMenuName) {
let topMenuName
this.menuInfo.map(item => {
if (item.submenu !== undefined) {
item.submenu.map(i2 => {
if (i2.name === subMenuName) topMenuName = item.topMenuName
})
}
})
this.breadInfo.top = topMenuName
this.breadInfo.sub = subMenuName
},
//提供给子组件改变面包屑最后的信息
giveChildChangeBreakInfo (subMenuName, topMenuName) {
this.breadInfo.sub = subMenuName
this.breadInfo.top = topMenuName
},
//提供给子组件创建tag标签使用
giveChildAddTag (menuName, url) {
this.tags.map(item => {
item.highlight = false
})
this.tags.push({
'name': menuName,
'url': url,
'highlight': true
})
},
//提供给子组件修改tag标签使用
updateTagInfo (menuName, url) {
this.tags.map((item, index) => {
item.highlight = false
if (item.name === menuName) {
this.tags.splice(index, 1)
}
})
this.tags.push({
'name': menuName,
'url': url,
'highlight': true
})
},
//更新当前用户
updateCurrentUser () {
utils.validFormAndInvoke(this.$refs['updateUserForm'], () => {
user.updateCurrentUser(this.currentUserInfo2).then((resp) => {
if (resp.code === 200) {
this.$notify({
title: 'Tips',
message: resp.message,
type: 'success',
duration: 2000
})
this.logout()
}
})
})
}
}
}
</script>
<style scoped lang="scss">
@import "../../assets/css/index/main";
</style>
- 路由配置:
// 进度条配置项
NProgress.configure({
showSpinner: false
})
Vue.use(VueRouter)
const routes = [
{
path: '/',
component: () => import('../views/auth/Login')
},
{
path: '/register',
component: () => import('../views/auth/Register')
},
{
path: '/index',
component: () => import('../views/index/Main'),
redirect: '/dashboard',
children: [
//仪表盘介绍(all)
{
path: '/dashboard',
component: () => import('../views/index/Dashboard')
},
//用户管理(超级管理员)
{
path: '/userManage',
component: () => import('../views/admin/UserManage')
},
//角色信息(超级管理员)
{
path: '/roleManage',
component: () => import('../views/admin/RoleManage')
},
//题库管理(老师和超级管理员)
{
path: '/questionManage',
component: () => import('../views/teacher/QuestionManage')
},
//题库管理(老师和超级管理员)
{
path: '/questionBankMange',
component: () => import('../views/teacher/QuestionBankManage')
},
//我的题库(all)
{
path: '/myQuestionBank',
component: () => import('../views/student/MyQuestionBank')
},
//题库训练页(学生和管理员)
{
path: '/train/:bankId/:trainType',
name: 'trainPage',
component: () => import('../views/student/TrainPage')
},
//考试管理(老师和超级管理员)
{
path: '/examManage',
component: () => import('../views/teacher/ExamManage')
},
//添加考试(老师和超级管理员)
{
path: '/addExam',
component: () => import('../views/teacher/AddExam')
},
//修改考试信息(老师和超级管理员)
{
path: '/updateExam/:examId',
name: 'updateExam',
component: () => import('../views/teacher/UpdateExam')
},
//在线考试页面选择考试(学生和超级管理员)
{
path: '/examOnline',
component: () => import('../views/student/ExamOnline')
},
//考试结果页(学生和超级管理员)
{
path: '/examResult/:recordId',
name: 'examResult',
component: () => import('../views/student/ExamResult')
},
//阅卷管理页面(老师和超级管理员)
{
path: '/markManage',
component: () => import('../views/teacher/MarkManage')
},
//批阅试卷(老师和管理员)
{
path: '/markExam/:recordId',
name: 'markExam',
component: () => import('../views/teacher/MarkExamPage')
},
//我的成绩(学生和管理员)
{
path: '/myGrade',
component: () => import('../views/student/MyGrade')
},
//统计总览页面(老师和管理员)
{
path: '/staticOverview',
component: () => import('../views/teacher/StatisticOverview')
},
// 公告管理(管理员)
{
path: '/noticeManage',
component: () => import('../views/admin/NoticeManage')
}
]
},
//考试界面(管理员和学生)
{
path: '/exam/:examId',
name: 'exam',
component: () => import('../views/student/ExamPage')
}
]
const router = new VueRouter({
routes
})
router.beforeEach((to, from, next) => {
NProgress.start()
const token = window.localStorage.getItem('authorization')
//2个不用token的页面请求
if (to.path === '/' || to.path === '/register') {
return next()
}
//没有token的情况 直接返回登录页
if (!token) return next('/')
//属于超级管理员的功能
if (to.path === '/userManage' || to.path === '/roleManage' || to.path === '/noticeManage') {
axios.get('/common/checkToken').then((resp) => {
if (resp.data.code === 200 && resp.data.data.roleId === 3) {//当前用户携带的token信息正确并且是管理员
next()
}
}).catch(err => {
this.$notify({
title: 'Tips',
message: err.response.data.errMsg,
type: 'error',
duration: 2000
})
localStorage.removeItem('authorization')
return next('/index')
})
}
//属于超级管理员又属于老师
if (to.path === '/questionManage' || to.path === '/questionBankMange' || to.path === '/examManage'
|| to.path === '/addExam' || to.name === 'updateExam' || to.path === '/markManage' || to.name === 'markExam') {
axios.get('/common/checkToken').then((resp) => {
if (resp.data.code === 200 && resp.data.data.roleId === 3 || resp.data.data.roleId === 2) {
next()
}
}).catch(err => {
this.$notify({
title: 'Tips',
message: err.response.data.errMsg,
type: 'error',
duration: 2000
})
localStorage.removeItem('authorization')
return next('/index')
})
}
//超级管理员 + 学生
if (to.path === '/myQuestionBank' || to.name === 'trainPage' || to.path === '/examOnline'
|| to.name === 'exam' || to.name === 'examResult' || to.path === '/myGrade') {
axios.get('/common/checkToken').then((resp) => {
if (resp.data.code === 200 && resp.data.data.roleId !== 2) {
next()
}
}).catch(err => {
this.$notify({
title: 'Tips',
message: err.response.data.errMsg,
type: 'error',
duration: 2000
})
localStorage.removeItem('authorization')
return next('/index')
})
}
next()
})
router.afterEach(() => {
NProgress.done() // 结束Progress
})
export default router
- 登录界面的效验功能:
// 登录表单数据信息
const loginForm = {
username: '',
password: '',
// 验证码
code: ''
}
// 自定义验证码校验规则
const validateCode = (rule, value, callback) => {
// 验证码不区分大小写
if (value.toString().toLocaleLowerCase() !== code.toString().toLocaleLowerCase()) {
callback(new Error('验证码输入错误'))
} else {
callback()
}
}
// 登录表单的校验规则
const loginFormRules = {
username: [
{
required: true,
message: '请输入账号',
trigger: 'blur'
},
],
password: [
{
required: true,
message: '请输入密码',
trigger: 'blur'
},
{
min: 5,
message: '密码不能小于5个字符',
trigger: 'blur'
}
],
code: [
{
required: true,
message: '请输入验证码',
trigger: 'blur'
},
{
validator: validateCode,
trigger: 'blur'
}
],
}
// 后台验证码id
let codeId
// 后台的验证码
let code
// 获取后台验证码
const getCode = () => {
auth.getCode(codeId).then(resp => {
code = resp.data
})
}
// 点击图片刷新验证码
const changeCode = () => {
const codeImg = document.querySelector('#code')
codeId = utils.getRandomId();
codeImg.src = `${process.env.VUE_APP_CAPTCHA_URL}/util/getCodeImg?id=` + codeId
codeImg.onload = () => getCode()
}
const toRegisterPage = () => {
router.push('/register')
}
// 登录
const login = (formEl) => {
utils.validFormAndInvoke(formEl, () => {
auth.login(loginForm).then(resp => {
if (resp.code === 200) {
localStorage.setItem('authorization', resp.data)
Vue.prototype.$notify({
title: 'Tips',
message: '登陆成功^_^',
type: 'success',
duration: 2000
})
router.push('/index')
}
}).catch(err => {
console.log(err)
//请求出错
changeCode()
getCode()
Vue.prototype.$notify({
title: 'Tips',
message: err.response.data.errMsg,
type: 'error',
duration: 2000
})
})
})
}
export default {
loginForm,
loginFormRules,
code,
getCode,
changeCode,
toRegisterPage,
login
}
- 底部栏展示:
<template>
<el-footer>
<span>©2023-11 Power By JiangPei</span>
<br>
<i class="el-icon-thumb"></i>
/
<a href="https://blog.csdn.net/m0_65137405?spm=1000.2115.3001.5343" target="_blank">也江个人博客网站</a>
/
<span style="color: blueviolet">联系QQ:1123800047</span>
</el-footer>
</template>
<script>
export default {
name: 'Footer'
}
- 题库的接口api编写:
import request from '@/utils/request'
export default {
getBankHaveQuestionSumByType (queryInfo) {
return request({
url: '/public/getBankHaveQuestionSumByType',
method: 'get',
params: queryInfo
})
},
deleteQuestionBank (ids) {
return request({
url: '/teacher/deleteQuestionBank',
method: 'get',
params: ids
})
},
addQuestionBank (questionBank) {
return request({
url: '/teacher/addQuestionBank',
method: 'post',
data: questionBank
})
},
getQuestionBank () {
return request({
url: '/teacher/getQuestionBank',
method: 'get'
})
},
addBankQuestion (params) {
return request({
url: '/teacher/addBankQuestion',
method: 'get',
params: params
})
},
removeBankQuestion (params) {
return request({
url: '/teacher/removeBankQuestion',
method: 'get',
params: params
})
},
getQuestionByBank (params) {
return request({
url: '/public/getQuestionByBank',
method: 'get',
params: params
})
}
}
- 摄像头开启调用代码:
//调用摄像头
getCamera() {
let constraints = {
video: {
width: 200,
height: 200
},
audio: false
}
//获得video摄像头
let video = document.getElementById('video')
let promise = navigator.mediaDevices.getUserMedia(constraints)
promise.then((mediaStream) => {
this.mediaStreamTrack = typeof mediaStream.stop === 'function' ? mediaStream : mediaStream.getTracks()[1]
video.srcObject = mediaStream
video.play()
this.cameraOn = true
}).catch((back) => {
this.$message({
duration: 1500,
message: '请开启摄像头权限o(╥﹏╥)o!',
type: 'error'
})
})
},
//拍照
async takePhoto() {
if (this.cameraOn) {//摄像头是否开启 开启了才执行上传信用图片
//获得Canvas对象
let video = document.getElementById('video')
let canvas = document.getElementById('canvas')
let ctx = canvas.getContext('2d')
ctx.drawImage(video, 0, 0, 200, 200)
// toDataURL --- 可传入'image/png'---默认, 'image/jpeg'
let img = document.getElementById('canvas').toDataURL()
//构造post的form表单
let formData = new FormData()
//convertBase64UrlToBlob函数是将base64编码转换为Blob
formData.append('file', this.base64ToFile(img, 'examTakePhoto.png'))
//上传阿里云OSS
await ossUtils.uploadImage(formData).then((resp) => {
if (resp.code === 200) this.takePhotoUrl.push(resp.data)
})
}
},
//关闭摄像头
closeCamera() {
let stream = document.getElementById('video').srcObject
let tracks = stream.getTracks()
tracks.forEach(function (track) {
track.stop()
})
document.getElementById('video').srcObject = null
},
//将摄像头截图的base64串转化为file提交后台
base64ToFile(urlData, fileName) {
let arr = urlData.split(',')
let mime = arr[0].match(/:(.*?);/)[1]
let bytes = atob(arr[1]) // 解码base64
let n = bytes.length
let ia = new Uint8Array(n)
while (n--) {
ia[n] = bytes.charCodeAt(n)
}
return new File([ia], fileName, {type: mime})
},
watch: {
//监控考试的剩余时间
async duration(newVal) {
const examDuration = {
duration: newVal,
timestamp: Date.now()
};
localStorage.setItem('examDuration' + this.examInfo.examId, JSON.stringify(examDuration));
//摄像头数据
let constraints = {
video: {
width: 200,
height: 200
},
audio: false
}
//通过调用摄像头判断用户是否中途关闭摄像头
let promise = navigator.mediaDevices.getUserMedia(constraints)
promise.catch((back) => {
this.cameraOn = false
})
if (!this.cameraOn) {//如果摄像头未开启,就再次调用开启
this.getCamera()
}
//考试时间结束了提交试卷
if (newVal < 1) {
if (this.cameraOn) {
//结束的时候拍照上传一张
await this.takePhoto()
this.closeCamera()
}
let data = {}
data.questionIds = []
data.userAnswers = this.userAnswer.join('-')
this.questionInfo.forEach((item, index) => {
data.questionIds.push(item.questionId)
//当前数据不完整,用户回答不完整(我们自动补充空答案,防止业务出错)
if (index > this.userAnswer.length) {
data.userAnswers += ' -'
}
})
//如果所有题目全部未答
if (data.userAnswers === '') {
this.questionInfo.forEach(item => {
data.userAnswers += ' -'
})
data.userAnswers.split(0, data.userAnswers.length - 1)
}
data.examId = parseInt(this.$route.params.examId)
data.questionIds = data.questionIds.join(',')
data.creditImgUrl = this.takePhotoUrl.join(',')
exam.addExamRecord(data).then((resp) => {
if (resp.code === 200) {
this.$notify({
title: 'Tips',
message: '考试时间结束,已为您自动提交 *^▽^*',
type: 'success',
duration: 2000
})
this.$router.push('/examResult/' + resp.data)
}
})
}
},
}
}
- 界面代码:
<template>
<el-container>
<el-header height="220">
<el-select @change="typeChange" clearable v-model="queryInfo.examType" placeholder="请选择考试类型">
<el-option
v-for="item in examType"
:key="item.type"
:label="item.info"
:value="item.type">
</el-option>
</el-select>
<el-input v-model="queryInfo.examName" placeholder="考试名称" @blur="getExamInfo"
style="margin-left: 5px;width: 220px"
prefix-icon="el-icon-search"></el-input>
</el-header>
<el-main>
<el-table
ref="questionTable"
highlight-current-row
v-loading="loading"
:border="true"
:data="examInfo"
tooltip-effect="dark"
style="width: 100%;">
<el-table-column align="center" label="考试名称">
<template slot-scope="scope">
{{ scope.row.examName }}
</template>
</el-table-column>
<el-table-column align="center" label="考试类型">
<template slot-scope="scope">
{{ scope.row.type === 1 ? '完全公开' : '需要密码' }}
</template>
</el-table-column>
<el-table-column align="center" label="考试时间">
<template slot-scope="scope">
{{
scope.row.startTime !== 'null' && scope.row.endTime !== 'null' ?
scope.row.startTime + ' ~' + scope.row.endTime : '不限时'
}}
</template>
</el-table-column>
<el-table-column align="center" label="考试时长">
<template slot-scope="scope">
{{ scope.row.duration + '分钟' }}
</template>
</el-table-column>
<el-table-column align="center" prop="totalScore" label="考试总分"></el-table-column>
<el-table-column align="center" prop="passScore" label="及格分数"></el-table-column>
<el-table-column align="center" label="操作">
<template slot-scope="scope">
<el-button size="small" :disabled="!checkExam(scope.row)" @click="toStartExam(scope.row)"
:icon="checkExam(scope.row) ? 'el-icon-caret-right' : 'el-icon-close'"
:type="checkExam(scope.row) ? 'primary' : 'info'">
{{ checkExam(scope.row) ? '去考试' : '暂不开放' }}
</el-button>
</template>
</el-table-column>
</el-table>
<!--分页-->
<el-pagination style="margin-top: 25px"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="queryInfo.pageNo"
:page-sizes="[10, 20, 30, 50]"
:page-size="queryInfo.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
</el-main>
<el-dialog
title="考试提示"
:visible.sync="startExamDialog" center
width="50%">
<el-alert
title="点击`开始考试`后将自动进入考试,请诚信考试,考试过程中可能会对用户行为、用户视频进行截图采样,请知悉!"
type="error">
</el-alert>
<script>
import exam from '@/api/exam'
export default {
name: 'ExamOnline',
data () {
return {
queryInfo: {
'examType': null,
'startTime': null,
'endTime': null,
'examName': null,
'pageNo': 0,
'pageSize': 10
},
//表格是否在加载
loading: true,
//考试类型信息
examType: [
{
info: '公开考试',
type: 1
},
{
info: '需要密码',
type: 2
}
],
//考试信息
examInfo: [],
//查询到的考试总数
total: 0,
//开始考试的提示框
startExamDialog: false,
//当前选中的考试的信息
currentSelectedExam: {}
}
},
created () {
this.getExamInfo()
},
methods: {
//考试类型搜索
typeChange (val) {
this.queryInfo.examType = val
this.getExamInfo()
},
//查询考试信息
getExamInfo () {
exam.getExamInfo(this.queryInfo).then((resp) => {
if (resp.code === 200) {
resp.data.data.forEach(item => {
item.startTime = String(item.startTime).substring(0, 10)
item.endTime = String(item.endTime).substring(0, 10)
})
this.examInfo = resp.data.data
this.total = resp.data.total
this.loading = false
}
})
},
//分页页面大小改变
handleSizeChange (val) {
this.queryInfo.pageSize = val
this.getExamInfo()
},
//分页插件的页数
handleCurrentChange (val) {
this.queryInfo.pageNo = val
this.getExamInfo()
},
//去考试准备页面
toStartExam (row) {
if (row.type === 2) {
this.$prompt('请提供考试密码', 'Tips', {
confirmButtonText: '确定',
cancelButtonText: '取消',
}).then(({ value }) => {
if (value === row.password) {
this.startExamDialog = true
this.currentSelectedExam = row
} else {
this.$message.error('密码错误o(╥﹏╥)o')
}
}).catch(() => {
})
} else {
this.startExamDialog = true
this.currentSelectedExam = row
}
}
},
computed: {
//检查考试的合法性
checkExam (row) {
return (row) => {
let date = new Date()
if (row.status === 2) return false
if (row.startTime === 'null' && row.endTime === 'null') {
return true
} else if (row.startTime === 'null') {
return date < new Date(row.endTime)
} else if (row.endTime === 'null') {
return date > new Date(row.startTime)
} else if (date > new Date(row.startTime) && date < new Date(row.endTime)) return true
}
}
}
}
</script>
<style scoped lang="scss">
@import "../../assets/css/student/examOnline";
</style>
5.3.2 后端核心功能代码
- Controller包中工具接口类:
- package com.jp.controller;
import com.jp.utils.CreateVerificationCode;
import com.jp.utils.RedisUtil;
import com.jp.vo.CommonResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.IOException;
@RestController
@Api(tags = "工具类接口")
@RequestMapping("/util")
@RequiredArgsConstructor
public class UtilController {
private final RedisUtil redisUtil;
@GetMapping("/getCodeImg")
@ApiOperation(value = "获取验证码图片流")
@ApiImplicitParams({
@ApiImplicitParam(name = "id", value = "帮助前端生成验证码", dataType = "string", paramType = "query")
})
public void getIdentifyImage(@RequestParam("id") String id, HttpServletResponse response) throws IOException {
//设置不缓存图片
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "No-cache");
response.setDateHeader("Expires", 0);
//指定生成的响应图片
response.setContentType("image/jpeg");
CreateVerificationCode code = new CreateVerificationCode();
BufferedImage image = code.getIdentifyImg();
code.getG().dispose();
//将图形验证码IO流传输至前端
ImageIO.write(image, "JPEG", response.getOutputStream());
redisUtil.set(id, code.getCode());
}
@GetMapping("/getCode")
@ApiOperation(value = "获取验证码")
@ApiImplicitParams({
@ApiImplicitParam(name = "id", value = "帮助前端生成验证码", dataType = "string", paramType = "query")
})
public CommonResult<String> getCode(@RequestParam("id") String id) {
return CommonResult.<String>builder()
.data((String) redisUtil.get(id))
.build();
}
} - 登陆界面:
- public class LoginDto { @NotBlank private String username; @NotBlank private String password; }
- entity配置实体类,如考试信息类:
public class Exam implements Serializable {
// 不需要主键自增id type 后面需要手动配置id
@TableId
@ApiModelProperty(value = "主键 考试id", example = "1")
private Integer examId;
@ApiModelProperty(value = "考试名称", example = "小学一年级考试")
private String examName;
@ApiModelProperty(value = "考试描述", example = "这是一场考试的描述")
private String examDesc;
@ApiModelProperty(value = "考试类型1公开,2需密码", example = "1")
private Integer type;
@ApiModelProperty(value = "考试密码,当type=2时候存在", example = "12345")
@TableField(strategy = FieldStrategy.IGNORED)
private String password;
@ApiModelProperty(value = "考试时间", example = "125(分钟)")
private Integer duration;
@DateTimeFormat(pattern = "yyyy-MM-dd")
@JsonFormat(pattern = "yyyy-MM-dd")
@ApiModelProperty(value = "考试开始时间", example = "2020-11-01")
@TableField(strategy = FieldStrategy.IGNORED)
private Date startTime;
@DateTimeFormat(pattern = "yyyy-MM-dd")
@JsonFormat(pattern = "yyyy-MM-dd")
@ApiModelProperty(value = "考试结束时间", example = "2020-12-01")
@TableField(strategy = FieldStrategy.IGNORED)
private Date endTime;
@ApiModelProperty(value = "考试总分", example = "100")
private Integer totalScore;
@ApiModelProperty(value = "考试及格线", example = "60")
private Integer passScore;
@ApiModelProperty(value = "考试状态 1有效 2无效", example = "1")
private Integer status;
}
- 登录信息验证java类:
- package com.jp.exception; public enum CommonErrorCode implements ErrorCode { UNAUTHORIZED(401, "未认证"), FORBIDDEN(403, "权限不足"), SIGNATURE_FAILED(403, "验签失败"), E_100101(100101, "账号密码错误, 或用户被封禁"), E_100102(100102, "用户不存在!"), E_100103(100103, "用户名已存在!"), E_100104(1000104, "验证码输入错误!"), E_100105(1000105, "操作用户的模式非法!"), E_100106(1000106, "操作考试的模式非法!"), E_200001(200001, "token异常"), E_300001(300001, "发布新公告异常"), E_300002(300002, "修改公告异常"), E_400001(400001, "考试不存在"), /** * 切面类错误 */ E_800001(800001, "目标方法返回null"), /** * 未知错误 */ UNKNOWN(999999, "未知错误"); private final int code; private final String desc; public int getCode() { return code; } public String getDesc() { return desc; } CommonErrorCode(int code, String desc) { this.code = code; this.desc = desc; } public static CommonErrorCode setErrorCode(int code) { for (CommonErrorCode errorCode : CommonErrorCode.values()) { if (errorCode.getCode() == code) { return errorCode; } } return null; } }
- 配置拦截器类:
- public class TeacherInterceptor implements HandlerInterceptor { public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 判断用户的token信息是否满足 TokenVo tokenVo = JwtUtils.getUserInfoByToken(request); if (tokenVo != null && (Objects.equals(tokenVo.getRoleId(), 3) || Objects.equals(tokenVo.getRoleId(), 2) || Objects.equals(tokenVo.getRoleId(), 1))) { return true; } // 当前不满足条件,直接跳转拦截 response.getWriter().print("Access denied"); return false; } }
- 添加Mapper映射文件;
配置Mapper.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jp.mapper.NoticeMapper">
<update id="setAllNoticeIsHistoryNotice">
update notice set status = 0;
</update>
</mapper>
- 设置前端vue启动命令:
- public PerformanceInterceptor performanceInterceptor() {
PerformanceInterceptor performanceInterceptor = new PerformanceInterceptor();
performanceInterceptor.setMaxTime(1000);// ms 设置sql执行的最大时间,如果超过了则不执行
performanceInterceptor.setFormat(true);
return performanceInterceptor;
}
- 配置数据库源及redis:
redis:
host: localhost
port: 6379
password: 123456
datasource:
username: root
password: 123456
url: jdbc:mysql://localhost:3306/exam_system?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
以上就是我的系统设计全部内容,本次实训课程设计来源于每次期末临期都会有很多老师在强调“诚信考试”这一话题,所以工商诚信考试管理系统是一个综合性的考试平台,旨在提供一套完整的考试管理平台,促进我们学校的诚信考试和人才培养。
项目结合了前后端框架技术,实现了丰富的功能和高效的数据处理能力。该系统的核心功能包括:用户权限管理;考试管理;成绩管理;摄像头调用管理;数据统计和分析等,可视化的数据界面展示各科考试成绩。实现采用了前后端分离的设计模式,后端使用Node.js和Express框架搭建服务器,处理请求和生成响应。前端使用Vue框架构建用户界面,实现数据绑定和组件化开发。通过API接口进行前后端通信,确保系统的可扩展性和可维护性。
最后,工商考试管理系统是以我们学校为示例,设计去提供一个高效、可靠的考试管理平台,有助于提高考试的公正性和诚信度,也希望所有人都能诚信考试、公平竞争。
对于本次课程设计所开发的前后端分离项目,已经经过本地测试,未来将会完善更多的功能,并部署在服务器上进行多人同时在线协作功能,完善考试学习系统的访问功能以及扩大数据库题库的存储容量,扩大题库的范围及使用诚信道德考试检测。
附录1:摄像头测试运行与分析程序
export default {
name: 'ExamPage',
data() {
return {
//当前考试的信息
examInfo: {},
//当前的考试题目
questionInfo: [
{
questionType: ''
}
],
//当前题目的索引值
curIndex: 0,
//控制大图的对话框
bigImgDialog: false,
//当前要展示的大图的url
bigImgUrl: '',
//用户选择的答案
userAnswer: [],
//页面数据加载
loading: {},
//页面绘制是否开始
show: false,
//答案的选项名abcd数据
optionName: ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'],
//考试总时长
duration: 0,
//摄像头对象
mediaStreamTrack: null,
//诚信照片的url
takePhotoUrl: [],
//摄像头是否开启
cameraOn: false,
}
},
created() {
this.getExamInfo()
//页面数据加载的等待状态栏
this.loading = this.$Loading.service({
body: true,
lock: true,
text: '数据拼命加载中,(*╹▽╹*)',
spinner: 'el-icon-loading',
})
//开启摄像头
window.onload = () => {
setTimeout(() => {
this.getCamera()
}, 1000)
//生成3次时间点截图
let times = []
for (let i = 0; i < 2; i++) {
times.push(Math.ceil(Math.random() * this.duration * 1000))
}
times.push(10000)
//一次考试最多3次随机的诚信截图
times.forEach(item => {
window.setTimeout(() => {
this.takePhoto()
}, item)
})
}
},
mounted() {
//关闭浏览器窗口的时候移除 localstorage的时长
var userAgent = navigator.userAgent //取得浏览器的userAgent字符串
var isOpera = userAgent.indexOf('Opera') > -1 //判断是否Opera浏览器
var isIE = userAgent.indexOf('compatible') > -1 && userAgent.indexOf('MSIE') > -1 && !isOpera //判断是否IE浏览器
var isIE11 = userAgent.indexOf('rv:11.0') > -1 //判断是否是IE11浏览器
var isEdge = userAgent.indexOf('Edge') > -1 && !isIE //判断是否IE的Edge浏览器
if (!isIE && !isEdge && !isIE11) {//兼容chrome和firefox
var _beforeUnload_time = 0, _gap_time = 0
var is_fireFox = navigator.userAgent.indexOf('Firefox') > -1//是否是火狐浏览器
window.onunload = function () {
_gap_time = new Date().getTime() - _beforeUnload_time
if (_gap_time <= 5) {
localStorage.removeItem('examDuration' + this.examInfo.examId)
} else {//谷歌浏览器刷新
}
}
window.onbeforeunload = function () {
_beforeUnload_time = new Date().getTime()
if (is_fireFox) {//火狐关闭执行
} else {//火狐浏览器刷新
}
}
}
},
methods: {
//查询当前考试的信息
getExamInfo() {
exam.getExamInfoById(this.$route.params).then((resp) => {
if (resp.code === 200) {
this.examInfo = resp.data
//设置定时(秒)
try {
const examDuration = JSON.parse(localStorage.getItem('examDuration' + this.examInfo.examId) || "{}");
if (examDuration.duration === 0 || Date.now() >= (examDuration.timestamp || Date.now()) + (examDuration.duration * 1000 || Date.now())) {
localStorage.removeItem('examDuration' + this.examInfo.examId)
}
this.duration = Math.min(JSON.parse(localStorage.getItem('examDuration' + this.examInfo.examId) || "{}").duration || resp.data.examDuration * 60, resp.data.examDuration * 60)
} catch (e) {
localStorage.removeItem('examDuration' + this.examInfo.examId)
}
//考试剩余时间定时器
this.timer = window.setInterval(() => {
if (this.duration > 0) this.duration--
}, 1000)
this.getQuestionInfo(resp.data.questionIds.split(','))
}
})
},
//查询考试的题目信息
async getQuestionInfo(ids) {
await question.getQuestionByIds({'ids': ids.join(',')}).then(resp => {
if (resp.code === 200) {
this.questionInfo = resp?.data?.data || []
//重置问题的顺序 单选 多选 判断 简答
this.questionInfo = this.questionInfo.sort(function (a, b) {
return a.questionType - b.questionType
})
}
})
this.loading.close()
this.show = true
},
//点击展示高清大图
showBigImg(url) {
this.bigImgUrl = url
this.bigImgDialog = true
},
//检验单选题的用户选择的答案
checkSingleAnswer(index) {
this.$set(this.userAnswer, this.curIndex, index)
},
//多选题用户的答案选中
selectedMultipleAnswer(index) {
if (this.userAnswer[this.curIndex] === undefined) {//当前是多选的第一个答案
this.$set(this.userAnswer, this.curIndex, index)
} else if (String(this.userAnswer[this.curIndex]).split(',').includes(index + '')) {//取消选中
let newArr = []
String(this.userAnswer[this.curIndex]).split(',').forEach(item => {
if (item !== '' + index) newArr.push(item)
})
if (newArr.length === 0) {
this.$set(this.userAnswer, this.curIndex, undefined)
} else {
this.$set(this.userAnswer, this.curIndex, newArr.join(','))
//答案格式化顺序DBAC -> ABCD
this.userAnswer[this.curIndex] = String(this.userAnswer[this.curIndex]).split(',').sort(function (a, b) {
return a - b
}).join(',')
}
} else if (!((this.userAnswer[this.curIndex] + '').split(',').includes(index + ''))) {//第n个答案
this.$set(this.userAnswer, this.curIndex, this.userAnswer[this.curIndex] += ',' + index)
//答案格式化顺序DBAC -> ABCD
this.userAnswer[this.curIndex] = String(this.userAnswer[this.curIndex]).split(',').sort(function (a, b) {
return a - b
}).join(',')
}
},
//调用摄像头
getCamera() {
let constraints = {
video: {
width: 200,
height: 200
},
audio: false
}
//获得video摄像头
let video = document.getElementById('video')
let promise = navigator.mediaDevices.getUserMedia(constraints)
promise.then((mediaStream) => {
this.mediaStreamTrack = typeof mediaStream.stop === 'function' ? mediaStream : mediaStream.getTracks()[1]
video.srcObject = mediaStream
video.play()
this.cameraOn = true
}).catch((back) => {
this.$message({
duration: 1500,
message: '请开启摄像头权限o(╥﹏╥)o!',
type: 'error'
})
})
},
//拍照
async takePhoto() {
if (this.cameraOn) {//摄像头是否开启 开启了才执行上传信用图片
//获得Canvas对象
let video = document.getElementById('video')
let canvas = document.getElementById('canvas')
let ctx = canvas.getContext('2d')
ctx.drawImage(video, 0, 0, 200, 200)
// toDataURL --- 可传入'image/png'---默认, 'image/jpeg'
let img = document.getElementById('canvas').toDataURL()
//构造post的form表单
let formData = new FormData()
//convertBase64UrlToBlob函数是将base64编码转换为Blob
formData.append('file', this.base64ToFile(img, 'examTakePhoto.png'))
//上传阿里云OSS
await ossUtils.uploadImage(formData).then((resp) => {
if (resp.code === 200) this.takePhotoUrl.push(resp.data)
})
}
},
//关闭摄像头
closeCamera() {
let stream = document.getElementById('video').srcObject
let tracks = stream.getTracks()
tracks.forEach(function (track) {
track.stop()
})
document.getElementById('video').srcObject = null
},
//将摄像头截图的base64串转化为file提交后台
base64ToFile(urlData, fileName) {
let arr = urlData.split(',')
let mime = arr[0].match(/:(.*?);/)[1]
let bytes = atob(arr[1]) // 解码base64
let n = bytes.length
let ia = new Uint8Array(n)
while (n--) {
ia[n] = bytes.charCodeAt(n)
}
return new File([ia], fileName, {type: mime})
},
//上传用户考试信息进入后台
async uploadExamToAdmin() {
if (this.cameraOn) await this.takePhoto()//结束的时候拍照上传一张
// 正则
var reg = new RegExp('-', 'g')
// 去掉用户输入的非法分割符号(-),保证后端接受数据处理不报错
this.userAnswer.forEach((item, index) => {
if (this.questionInfo[index].questionType === 4) {//简答题答案处理
this.userAnswer[index] = item.replace(reg, ' ')
}
})
// 标记题目是否全部做完
let flag = true
for (let i = 0; i < this.userAnswer.length; i++) {// 检测用户是否题目全部做完
if (this.userAnswer[i] === undefined) {
flag = false
}
}
// 如果用户所有答案的数组长度小于题目长度,这个时候也要将标志位置为false
if (this.userAnswer.length < this.questionInfo.length) {
flag = false
}
//题目未做完
if (!flag) {
// if (this.userAnswer.some((item) => item === undefined)) {
this.$confirm('当前试题暂未做完, 是否继续提交o(╥﹏╥)o ?', 'Tips', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
let data = {}
data.questionIds = []
data.userAnswers = this.userAnswer.join('-')
this.questionInfo.forEach((item, index) => {
data.questionIds.push(item.questionId)
//当前数据不完整,用户回答不完整(我们自动补充空答案,防止业务出错)
if (index > (this.userAnswer.length - 1)) {
data.userAnswers += '- '
}
})
//如果所有题目全部未答
if (this.userAnswer.length === 0) {
this.questionInfo.forEach(item => {
data.userAnswers += ' -'
})
data.userAnswers.split(0, data.userAnswers.length - 1)
}
data.examId = parseInt(this.$route.params.examId)
data.questionIds = data.questionIds.join(',')
data.creditImgUrl = this.takePhotoUrl.join(',')
exam.addExamRecord(data).then((resp) => {
if (resp.code === 200) {
this.$notify({
title: 'Tips',
message: '考试结束 *^▽^*',
type: 'success',
duration: 2000
})
this.$router.push('/examResult/' + resp.data)
}
})
}).catch(() => {
this.$notify({
title: 'Tips',
message: '继续加油! *^▽^*',
type: 'success',
duration: 2000
})
})
} else {//当前题目做完了
if (this.cameraOn) {
//结束的时候拍照上传一张
await this.takePhoto()
this.closeCamera()
}
let data = {}
data.questionIds = []
data.userAnswers = this.userAnswer.join('-')
data.examId = parseInt(this.$route.params.examId)
data.creditImgUrl = this.takePhotoUrl.join(',')
this.questionInfo.forEach((item, index) => {
data.questionIds.push(item.questionId)
})
data.questionIds = data.questionIds.join(',')
exam.addExamRecord(data).then((resp) => {
if (resp.code === 200) {
this.$notify({
title: 'Tips',
message: '考试结束 *^▽^*',
type: 'success',
duration: 2000
})
this.$router.push('/examResult/' + resp.data)
}
})
}
}
},
watch: {
//监控考试的剩余时间
async duration(newVal) {
const examDuration = {
duration: newVal,
timestamp: Date.now()
};
localStorage.setItem('examDuration' + this.examInfo.examId, JSON.stringify(examDuration));
//摄像头数据
let constraints = {
video: {
width: 200,
height: 200
},
audio: false
}
//通过调用摄像头判断用户是否中途关闭摄像头
let promise = navigator.mediaDevices.getUserMedia(constraints)
promise.catch((back) => {
this.cameraOn = false
})
if (!this.cameraOn) {//如果摄像头未开启,就再次调用开启
this.getCamera()
}
//考试时间结束了提交试卷
if (newVal < 1) {
if (this.cameraOn) {
//结束的时候拍照上传一张
await this.takePhoto()
this.closeCamera()
}
let data = {}
data.questionIds = []
data.userAnswers = this.userAnswer.join('-')
this.questionInfo.forEach((item, index) => {
data.questionIds.push(item.questionId)
//当前数据不完整,用户回答不完整(我们自动补充空答案,防止业务出错)
if (index > this.userAnswer.length) {
data.userAnswers += ' -'
}
})
//如果所有题目全部未答
if (data.userAnswers === '') {
this.questionInfo.forEach(item => {
data.userAnswers += ' -'
})
data.userAnswers.split(0, data.userAnswers.length - 1)
}
data.examId = parseInt(this.$route.params.examId)
data.questionIds = data.questionIds.join(',')
data.creditImgUrl = this.takePhotoUrl.join(',')
exam.addExamRecord(data).then((resp) => {
if (resp.code === 200) {
this.$notify({
title: 'Tips',
message: '考试时间结束,已为您自动提交 *^▽^*',
type: 'success',
duration: 2000
})
this.$router.push('/examResult/' + resp.data)
}
})
}
},
}
}
</script>
- 苏婉怡,揣小龙,王煜尧等.基于Java技术的考试系统设计与实现[J].无线互联科技,2023,20(14):75-77.
- 王鹰汉,明小波.基于Vue的在线考试系统设计与实现[J].无线互联科技,2023,20(06):52-54+92.
- 魏志强,培训考试管理系统.甘肃省,中铁二十一局集团电务电化工程有限公司,2020-10-01.
- 杨涛,特种设备作业人员考试管理系统V1.0.陕西省,陕西海晨电子科技有限公司,2019-07-01.
- 杨岚凯.中国民航飞行学院在线考试管理系统的设计与实现[D].电子科技大学,2013.
- 郭健.基于SOA的自学考试管理系统的研究[D].湖南科技大学,2011.
- 侯颖,李小聪,段渭军等.基于校园一卡通平台的便携式智能考试管理系统的研究[J].中国教育信息化,2011,(05):42-45.
- 曾琦华,蒋京,李君君.高校考试管理系统方案设计[J].科教文汇(中旬刊),2010,(11):183+197.
- 阎威,徐菁.基于Internet的校园考试管理系统的分析及设计[J].中国市场,2008,(52):210-211.
- 梁文静,崔杜武,张亚玲.可灵活配置的考试管理系统设计与实现[J].计算机工程与应用,2004,(27):111-113.
- 魏心怡.在线考试系统中考试成绩图形化呈现的设计与开发[J].电子技术与软件工程,2022,(21):239-242.
- 刘庆海,徐雪梅.基于Web的考试系统设计与实现[J].电脑编程技巧与维护,2021,(12):17-20.DOI:10.16184/j.cnki.comprg.2021.12.007