基于前后端分离的工商考试管理系统

内容摘要:基于前后端分离的考试学习系统课程设计旨在开发一款满足学生、教师和管理员需求的在线考试和学习平台。主要参考超星学习通考试的系统,系统采用前、后端分离的技术实现,前端使用HTML5、CSS3和JavaScript、node.js、vue3及若依框架来实现页面框架搭建、布局、样式和交互,后端使用SSM框架搭建服务器、mysql创建数据库存储用户信息和考试资料。系统实现包括登录、主页、考试、练习题等功能界面的实现,还实现的调用摄像头的诚信考试功能模块的搭建,同时处理异常和跨域请求,保护系统安全。最终进行系统测试和优化以确保满足性能需求。

关键词:考试学习系统  node.js  vue  SSM  mysql

前言. 1

1 工商考试系统研究现状及意义. 1

1.1工商考试学习系统研究现状. 1

1.2工商考试系统设计意义. 2

1.3 工商考试系统设计流程. 2

2 系统设计实现功能. 3

2.1 需求分析. 3

2.2 需求功能描述. 3

2.2.1 权限控制. 3

2.2.2在线考试. 4

2.2.3 成绩模块. 4

2.2.4 题库模块. 4

2.2.5 题库管理. 4

2.2.6 试题管理. 4

2.2.7 考试管理. 4

2.2.8 考卷批阅. 5

2.2.9 考试统计. 5

2.2.10  用户管理. 5

2.3 管理系统项目架构. 5

2.3.1 后端框架结构. 5

2.3.2 前端项目结构. 6

3.   工商考试系统功能设计. 7

3.1 后端框架搭建. 8

3.2 数据库设计. 9

3.2.1 数据库设计流程:. 9

3.2.2 数据库表设计. 10

3.3 前端框架搭建. 10

4.   工商考试系统界面展示. 12

4.1    登录界面. 12

4.2    注册界面. 13

4.3    公告栏弹窗界面. 14

4.4    项目主界面. 14

4.4.1 产品介绍. 15

4.4.2 在线考试系统介绍. 15

4.5    管理员界面. 16

4.6    教师端功能. 20

4.6.1 教师端功能界面概述. 20

4.6.1教师端功能界面介绍. 20

4.6.3 界面展示. 21

4.7    学生端功能. 23

4.7.1学生界面概述. 23

4.7.2主要功能界面展示. 23

5.   工商考试系统主要功能介绍及核心代码讲解. 25

5.1 后端返回参数展示. 25

5.2 主要功能介绍. 26

5.3 核心代码展示(详见附录1). 27

5.3.1 前端代码. 27

5.3.2 后端核心功能代码. 58

6.   工商考试系统总结. 63

7.   发展与期望. 64

参考文献. 73

前言

在信息技术高速发展的今天,新知识、新技术层出不穷,计算机技术早已广泛的应用于各行各业之中,利用计算机的强大数据处理能力和辅助决策能力叫,实现行业管理的规范化、标准化、效率化。

管理信息系统(Management Information System,简称MIS〉是一个以人为主导,利用计算机软硬件技术以及网络通信技术,实现对信息的收集、传输、储存、更新。

目前,管理信息系统广泛采用Java技术作为开发的主要技术。在经过多年的技术积累与更新,Java技术已经从一种简单的信息浏览和信息交互平台发展为复杂的JavaEE企业级框架技术。

而随着在线学习平台的兴起,考试学习系统成为了学生和教师之间的重要桥梁。传统的考试学习系统往往采用前端和后端一体化的设计模式,这使得系统的维护和扩展变得困难。为了解决这个问题,我们设计了一个基于前后端分离的考试学习系统。该系统主要使用Node.js、Vue、SSM(Spring、SpringMVC、MyBatis)和MySQL等技术实现。

               

1 工商考试系统研究现状及意义

1.1工商考试学习系统研究现状

学习通考试系统是一个综合性的评估系统,旨在通过客观的测试题目来评估学习者的知识和技能水平。近年来,随着技术的发展,考试系统已经经历了从传统纸质考试到数字化考试,再到在线考试系统的转变。

目前,国内外已经开发出多种类型的考试系统,包括但不限于以下几种:

  1. 基于Web的在线考试系统:这种类型的考试系统主要依托于互联网平台,具有灵活、便捷的特点,可以实现远程在线考试、自动评卷等功能。
  2. 基于移动设备的考试系统:随着移动设备的发展,移动端的在线考试系统也逐渐普及。该类型考试系统主要针对手机、平板电脑等移动设备,可以实现在线答题、自动评卷等功能。
  3. 智能考试系统:智能考试系统是一种基于人工智能技术的考试系统,可以根据学习者的学习情况和知识水平动态生成试卷,实现个性化测试和评估。

此外,根据不同的应用场景和目的,考试系统还有多种分类方式。例如,根据考试科目的不同,可以分为英语考试系统、数学考试系统、计算机考试系统等;根据使用对象的不同,可以分为学生考试系统、教师考试系统、企业培训考试系统等。


总的来说,随着技术的不断发展,考试系统的功能和性能也在不断提升和完善。未来,随着人工智能、大数据等技术的进一步发展,考试系统将会更加智能化、个性化和高效化。

图表 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 前端项目目录结构

  1. 工商考试系统功能设计

3.1 后端框架搭建

在工商考试系统的后端框架搭建方面,应本次实训要求,使用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 数据库设计

3.2.1 数据库设计流程:

在创作的工商考试管理系统的数据库设计需要考虑以下几个方面:

  • 实体关系设计:根据系统需求,确定各个实体之间的关系,例如考生与考试科目之间的关系、考试科目与考试成绩之间的关系等。
  • 数据表设计:为每个实体创建相应的数据表,确定每个表的字段和数据类型,例如考生信息表、考试科目表、考试成绩表等。
  • 主键与外键设计:为每个表设置主键和外键,确保数据的唯一性和关联性。
  • 约束设计:为每个表的字段设置相应的约束,例如非空约束、唯一约束、外键约束等。
  • 索引设计:根据查询需求,为表中的字段创建索引,提高查询效率。
  • 视图设计:根据系统需求,创建视图来简化数据操作和展示。
  • 存储过程与触发器设计:根据业务需求,创建存储过程和触发器来实现复杂的业务逻辑和数据操作。

在数据库设计中,需要综合考虑系统的功能需求、数据量、性能等多个方面,以确保设计的合理性和可扩展性。同时,需要遵循数据库设计的范式原则,减少数据冗余和保证数据的一致性。以上是我的工商考试系统数据库设计的基本思路。

图表 5 数据库表结构


3.2.2 数据库表设计

一共设计了考试试题表,试题表,问题表,信息表,用户表,权限表,问题登记表,公告表。

Answer表

exam表

exam_question表

exam_recordnotice表

question表

question_bank表

user表

user_role表

3.3 前端框架搭建

使用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项目启动

  1. 工商考试系统界面展示
    1. 登录界面

登录界面的作用主要是提供用户身份验证和授权的功能。用户可以通过输入用户名/邮箱和密码来进行身份验证,如果输入的信息与数据库中存储的信息匹配,则验证成功,用户就可以进入系统进行相应的操作。如果验证失败,则用户无法进入系统。同时,登录界面还可以提供权限控制的功能,不同权限的用户可以访问不同的系统功能。这样,登录界面可以有效地保护系统的安全性和稳定性。

图表 7 登录界面

    1. 注册界面

图表 8注册界面(用户名存在,验证信息错误)


注册界面主要是用于收集用户信息,并创建新的用户帐户。用户可以在此输入他们的用户名、密码、姓名和其他相关信息,系统会将这些信息存储在数据库中,并创建一个新的用户帐户。一旦用户帐户创建成功,用户就可以使用他们的用户名和密码来访问和操作系统。注册界面还可以提供数据验证和错误处理机制,以确保用户输入的数据符合要求,并在出现错误时提供相应的提示信息。下面就是用户名已经存在以及验证码错误的情况。

注册成功以后自动跳转登陆界面:

图表 9 注册后跳转登录界面

    1. 公告栏弹窗界面

图表 10公告栏弹窗

    1. 项目主界面

项目主界面分为产品介绍以及在线考试系统介绍,主界面在每一个权限下都是相同的,但是只有管理员和教师有题库修改权限,只有管理员具有定义用户权限以及公告栏修改权限。

4.4.1 产品介绍

产品介绍页面用于展示考试系统的基本功能和特点,以吸引用户了解和使用该系统。在该页面中,可以介绍考试系统的背景、目的和功能,以及系统所针对的用户群体。此外,还可以展示系统的优势和特点,例如其易用性、可靠性、高效性和安全性等。通过产品介绍页面,用户可以快速了解系统的基

图表 11主界面产品介绍


本情况,并决定是否需要进一步了解和使用该系统。

4.4.2 在线考试系统介绍

图表 12考试科目类别


在线考试分为三个子模块:在线考试、我的成绩、我的题库,该界面主要是考试系统的考试功能。

图表 13 考试记录

图表 14 考试题库

    1. 管理员界面

管理员具有整个系统的操作权限。

主要分为:考试管理、考试信息统计、系统设置等权限;考试管理主要包括:题库、试题、考试安排、阅卷等管理;考试信息统计:主要通过数据分析得出考试通过率、考试科目各科已考次数占比;系统设置中包括公告管理,有公告信息的发布和修改,角色管理(教师、学生、管理员三类角色)、以及注册后的用户账号密码等信息管理。

这些功能实现都是基于增删改查所做的基本操作,调用接口处理数据请求返回数据。

图表 15管理员界面

图表 16 题库管理

图表 17考试管理

图表 18 考试管理

图表 19阅卷管理

图表 20考试信息统计表

图表 21公告管理

图表 22角色管理

图表 23用户管理

    1. 教师端功能

4.6.1 教师端功能界面概述

教师端功能界面是为教师设计的,用于管理和评估学生的在线考试。该界面提供了丰富的功能,让教师可以轻松地对学生的考试进行批阅评分,并对题库、试卷和考试信息进行发布、编辑和删除等操作。

4.6.1教师端功能界面介绍

  • 题库管理:题库是考试系统的核心之一,教师可以通过题库管理功能添加、编辑和删除试题。教师可以选择不同的题型(如选择题、填空题、问答题等)和难度等级,并输入相应的题目内容和答案。题库管理界面还提供了搜索和排序功能,方便教师快速找到所需的试题。
  • 试卷管理:试卷是考试的载体,教师可以通过试卷管理功能创建和编辑试卷。教师可以选择从题库中选题,设置试卷的难度等级和考试时间,并添加个性化的试卷说明。同时,试卷管理界面还提供了预览和打印功能,方便教师核对试卷信息和打印试卷。
  • 考试信息发布:教师可以通过考试信息发布功能发布考试通知和相关要求。教师可以选择发布考试的日期、时间、地点和注意事项,并通过系统通知或邮件等方式通知学生参加考试。
  • 学生管理:教师可以通过学生管理功能添加、编辑和删除学生的信息。教师可以输入学生的姓名、学号、班级等基本信息,并为学生分配唯一的账号和密码。学生管理界面还提供了搜索和排序功能,方便教师快速找到特定的学生。
  • 考试批阅评分:在考试结束后,教师可以登录教师端功能界面对学生的考试进行批阅评分。批阅评分界面展示了学生的基本信息和试卷答案,教师可以根据标准答案对学生的答案进行评分和给出相应的评语。批阅评分界面还提供了统计和分析功能,方便教师了解学生的整体表现和需要改进的地方。
  • 数据统计和分析:教师可以通过数据统计和分析功能查看和评估考试的相关数据。教师可以查看每个学生的考试成绩、答题时间和错误率等详细信息,并根据这些信息评估学生的学习情况和表现。此外,教师还可以生成报告和图表,以便更好地了解学生的整体表现和需要改进的地方。

4.6.3 界面展示

图表 24 添加试卷

图表 26设置考题

图表27 设置试卷属性

图表 28 发布考题

图表 29试卷批阅

    1. 学生端功能

4.7.1学生界面概述

考试系统学生端功能学生界面是专为学生设计的,以提供便捷的在线考试体验。在这个界面上,学生可以轻松查看教师发布的考试科目和相关通知,选择自己想要参加的科目,并随时查看自己的考试成绩和表现。此外,学生还可以进行个人设置,如修改个人信息等。这个界面设计简洁明了,操作便捷,旨在为学生提供更好的在线考试体验。

4.7.2主要功能界面展示

图表 30 进入考试

图表 31 诚信考试界面

图表 32考试成绩管理界面

图表 33练习题库

  1. 工商考试系统主要功能介绍及核心代码讲解

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>&copy;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">联系QQ1123800047</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()

        //构造postform表单

        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

  1. 工商考试系统总结

以上就是我的系统设计全部内容,本次实训课程设计来源于每次期末临期都会有很多老师在强调“诚信考试”这一话题,所以工商诚信考试管理系统是一个综合性的考试平台,旨在提供一套完整的考试管理平台,促进我们学校的诚信考试和人才培养。

项目结合了前后端框架技术,实现了丰富的功能和高效的数据处理能力。该系统的核心功能包括:用户权限管理;考试管理;成绩管理;摄像头调用管理;数据统计和分析等,可视化的数据界面展示各科考试成绩。实现采用了前后端分离的设计模式,后端使用Node.js和Express框架搭建服务器,处理请求和生成响应。前端使用Vue框架构建用户界面,实现数据绑定和组件化开发。通过API接口进行前后端通信,确保系统的可扩展性和可维护性。

最后,工商考试管理系统是以我们学校为示例,设计去提供一个高效、可靠的考试管理平台,有助于提高考试的公正性和诚信度,也希望所有人都能诚信考试、公平竞争。

  1. 发展与期望

对于本次课程设计所开发的前后端分离项目,已经经过本地测试,未来将会完善更多的功能,并部署在服务器上进行多人同时在线协作功能,完善考试学习系统的访问功能以及扩大数据库题库的存储容量,扩大题库的范围及使用诚信道德考试检测。

附录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 //判断是否IEEdge浏览器

    if (!isIE && !isEdge && !isIE11) {//兼容chromefirefox

      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()

        //构造postform表单

        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>

参考文献

  1. 苏婉怡,揣小龙,王煜尧等.基于Java技术的考试系统设计与实现[J].无线互联科技,2023,20(14):75-77.
  2. 王鹰汉,明小波.基于Vue的在线考试系统设计与实现[J].无线互联科技,2023,20(06):52-54+92.
  3. 魏志强,培训考试管理系统.甘肃省,中铁二十一局集团电务电化工程有限公司,2020-10-01.
  4. 杨涛,特种设备作业人员考试管理系统V1.0.陕西省,陕西海晨电子科技有限公司,2019-07-01.
  5. 杨岚凯.中国民航飞行学院在线考试管理系统的设计与实现[D].电子科技大学,2013.
  6. 郭健.基于SOA的自学考试管理系统的研究[D].湖南科技大学,2011.
  7. 侯颖,李小聪,段渭军等.基于校园一卡通平台的便携式智能考试管理系统的研究[J].中国教育信息化,2011,(05):42-45.
  8. 曾琦华,蒋京,李君君.高校考试管理系统方案设计[J].科教文汇(中旬刊),2010,(11):183+197.
  9. 阎威,徐菁.基于Internet的校园考试管理系统的分析及设计[J].中国市场,2008,(52):210-211.
  10. 梁文静,崔杜武,张亚玲.可灵活配置的考试管理系统设计与实现[J].计算机工程与应用,2004,(27):111-113.
  11. 魏心怡.在线考试系统中考试成绩图形化呈现的设计与开发[J].电子技术与软件工程,2022,(21):239-242.
  12. 刘庆海,徐雪梅.基于Web的考试系统设计与实现[J].电脑编程技巧与维护,2021,(12):17-20.DOI:10.16184/j.cnki.comprg.2021.12.007
基于springboot+vue+elementui开发的在线考试系统【前端源码+后端源码+数据库】.zip 系统基于spring boot + vue开发,前端框架elementui,在若依/Ruoyi(Vue前后端分离版本)整体系统框架上开发了考试系统相关。 整个系统包括试题管理、考试组织、网上考试、以及资料管理四部分,资料管理只是作为图片、文件共享使用 请先阅读doc目录下《考试系统介绍》,内部有较为详尽功能说明,后续文档将说明数据表生成、源程序运行及部署。 内置功能: 用户管理:用户是系统操作者,该功能主要完成系统用户配置。 部门管理:配置系统组织机构(公司、部门、小组),树结构展现支持数据权限。 岗位管理:配置系统用户所属担任职务。 菜单管理:配置系统菜单,操作权限,按钮权限标识等。 角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。 字典管理:对系统中经常使用的一些较为固定的数据进行维护。 参数管理:对系统动态配置常用参数。 通知公告:系统通知公告信息发布维护。 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。 登录日志:系统登录日志记录查询包含登录异常。 在线用户:当前系统中活跃用户状态监控。 定时任务:在线(添加、修改、删除)任务调度包含执行结果日志。 代码生成:前后端代码的生成(java、html、xml、sql)支持CRUD下载 。 系统接口:根据业务代码自动生成相关的api接口文档。 服务监控:监视当前系统CPU、内存、磁盘、堆栈等相关信息。 缓存监控:对系统的缓存信息查询,命令统计等。 在线构建器:拖动表单元素生成相应的HTML代码。 连接池监视:监视当前系统数据库连接池状态,可进行分析SQL找出系统性能瓶颈
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值