├── doc
├── deploy
│ ├── README.md
│ └── nginx.conf
├── images
│ ├── exam_join.png
│ ├── exam_list.png
│ ├── exam_create.png
│ ├── exam_detail.png
│ ├── exam_join2.png
│ ├── exam_record.png
│ ├── exam_update.png
│ ├── question_list.png
│ ├── question_create.png
│ └── question_update.png
├── references
│ └── images
│ │ ├── JWT概要.png
│ │ ├── 登录校验方案.png
│ │ ├── 单机tomcat应用登录检验.png
│ │ └── tomcat开启session共享.png
└── README.md
├── frontend
├── src
│ ├── components
│ │ ├── tools
│ │ │ ├── index.js
│ │ │ ├── DetailList.vue
│ │ │ ├── Logo.vue
│ │ │ ├── Breadcrumb.vue
│ │ │ ├── HeadInfo.vue
│ │ │ ├── UserMenu.vue
│ │ │ └── TwoStepCaptcha.vue
│ │ ├── Menu
│ │ │ ├── index.js
│ │ │ └── SideMenu.vue
│ │ ├── Result
│ │ │ ├── index.js
│ │ │ └── Result.vue
│ │ ├── NoticeIcon
│ │ │ ├── index.js
│ │ │ └── NoticeIcon.vue
│ │ ├── PageHeader
│ │ │ └── index.js
│ │ ├── GlobalFooter
│ │ │ ├── index.js
│ │ │ └── GlobalFooter.vue
│ │ ├── GlobalHeader
│ │ │ └── index.js
│ │ ├── Exception
│ │ │ ├── index.js
│ │ │ ├── type.js
│ │ │ └── ExceptionPage.vue
│ │ ├── SettingDrawer
│ │ │ ├── index.js
│ │ │ ├── SettingItem.vue
│ │ │ └── settingConfig.js
│ │ ├── DescriptionList
│ │ │ └── index.js
│ │ ├── MultiTab
│ │ │ ├── index.js
│ │ │ └── index.less
│ │ ├── StandardFormRow
│ │ │ └── index.js
│ │ ├── index.less
│ │ ├── PageLoading
│ │ │ └── index.jsx
│ │ ├── index.js
│ │ ├── _util
│ │ │ └── util.js
│ │ └── TagSelect
│ │ │ ├── TagSelectOption.jsx
│ │ │ └── index.jsx
│ ├── assets
│ │ ├── logo.png
│ │ ├── icons
│ │ │ ├── mine.svg
│ │ │ ├── question-admin.svg
│ │ │ ├── exam-list.svg
│ │ │ └── bx-analyse.svg
│ │ ├── background.svg
│ │ └── logo.svg
│ ├── utils
│ │ ├── permissions.js
│ │ ├── domUtil.js
│ │ ├── axios.js
│ │ ├── filter.js
│ │ ├── device.js
│ │ ├── utils.less
│ │ ├── helper
│ │ │ └── permission.js
│ │ ├── storage.js
│ │ ├── util.js
│ │ ├── request.js
│ │ └── mixin.js
│ ├── views
│ │ ├── 404.vue
│ │ ├── exception
│ │ │ ├── 403.vue
│ │ │ ├── 404.vue
│ │ │ └── 500.vue
│ │ ├── Home.vue
│ │ ├── home
│ │ │ ├── default.less
│ │ │ ├── ListItem.vue
│ │ │ ├── List.vue
│ │ │ ├── Banner.vue
│ │ │ └── Page1.vue
│ │ ├── user
│ │ │ └── RegisterResult.vue
│ │ ├── list
│ │ │ └── modules
│ │ │ │ ├── QuestionViewModal.vue
│ │ │ │ ├── CreateForm.vue
│ │ │ │ └── UpdateAvatarModal.vue
│ │ ├── test
│ │ │ └── SummerNoteDemo.vue
│ │ └── account
│ │ │ └── settings
│ │ │ ├── Custom.vue
│ │ │ └── AvatarModal.vue
│ ├── layouts
│ │ ├── BlankLayout.vue
│ │ ├── index.js
│ │ └── RouteView.vue
│ ├── router
│ │ └── index.js
│ ├── plugins
│ │ ├── summernote.js
│ │ └── bootstrap-table.js
│ ├── api
│ │ ├── user.js
│ │ ├── index.js
│ │ └── login.js
│ ├── store
│ │ ├── index.js
│ │ ├── getters.js
│ │ ├── mutation-types.js
│ │ └── modules
│ │ │ └── permission.js
│ ├── core
│ │ ├── lazy_use.js
│ │ ├── use.js
│ │ ├── icons.js
│ │ ├── directives
│ │ │ └── action.js
│ │ ├── bootstrap.js
│ │ └── lazy_lib
│ │ │ └── components_use.js
│ ├── main.js
│ ├── App.vue
│ ├── config
│ │ └── defaultSettings.js
│ └── permission.js
├── .gitattributes
├── .env
├── tests
│ └── unit
│ │ └── .eslintrc.js
├── .prettierrc
├── public
│ ├── logo.png
│ ├── avatar2.jpg
│ ├── home
│ │ ├── cover1.jpg
│ │ ├── cover2.jpg
│ │ ├── cover3.jpg
│ │ ├── cover4.jpg
│ │ └── cover5.jpg
│ ├── loading
│ │ ├── loading.html
│ │ ├── option2
│ │ │ ├── loading.css
│ │ │ ├── loading.svg
│ │ │ └── html_code_segment.html
│ │ └── loading.css
│ └── index.html
├── .travis.yml
├── jsconfig.json
├── .gitignore
├── webstorm.config.js
├── babel.config.js
├── docs
│ ├── multi-tabs.md
│ ├── add-page-loading-animate.md
│ ├── webpack-bundle-analyzer.md
│ └── load-on-demand.md
├── jest.config.js
├── .editorconfig
├── README.md
└── vue.config.js
├── entrypoint.sh
├── backend
├── .gitignore
├── images
│ ├── 拦截器注入配置文件属性.png
│ └── 拦截器注入配置文件属性2.png
├── src
│ └── main
│ │ ├── java
│ │ └── lsgwr
│ │ │ └── exam
│ │ │ ├── dto
│ │ │ ├── package-info.java
│ │ │ └── RegisterDTO.java
│ │ │ ├── config
│ │ │ ├── package-info.java
│ │ │ ├── ServletConfig.java
│ │ │ ├── IntercepterConfig.java
│ │ │ ├── CORSConf.java
│ │ │ └── Swagger2Config.java
│ │ │ ├── entity
│ │ │ ├── package-info.java
│ │ │ ├── QuestionOption.java
│ │ │ ├── Page.java
│ │ │ ├── ExamRecordLevel.java
│ │ │ ├── Action.java
│ │ │ ├── Role.java
│ │ │ ├── QuestionLevel.java
│ │ │ ├── QuestionType.java
│ │ │ ├── QuestionCategory.java
│ │ │ ├── User.java
│ │ │ ├── ExamRecord.java
│ │ │ ├── Question.java
│ │ │ └── Exam.java
│ │ │ ├── exception
│ │ │ ├── package-info.java
│ │ │ └── ExamException.java
│ │ │ ├── service
│ │ │ ├── package-info.java
│ │ │ ├── UserService.java
│ │ │ └── ExamService.java
│ │ │ ├── controller
│ │ │ └── package-info.java
│ │ │ ├── repository
│ │ │ ├── package-info.java
│ │ │ ├── PageRepository.java
│ │ │ ├── RoleRepository.java
│ │ │ ├── ActionRepository.java
│ │ │ ├── QuestionTypeRepository.java
│ │ │ ├── QuestionLevelRepository.java
│ │ │ ├── QuestionOptionRepository.java
│ │ │ ├── ExamRecordLevelRepository.java
│ │ │ ├── QuestionCategoryRepository.java
│ │ │ ├── ExamRepository.java
│ │ │ ├── ExamRecordRepository.java
│ │ │ ├── QuestionRepository.java
│ │ │ └── UserRepository.java
│ │ │ ├── utils
│ │ │ ├── package-info.java
│ │ │ ├── ResultVOUtil.java
│ │ │ └── JwtUtils.java
│ │ │ ├── qo
│ │ │ ├── package-info.java
│ │ │ ├── DownloadQo.java
│ │ │ ├── UploadModel.java
│ │ │ ├── UploadModel2.java
│ │ │ └── LoginQo.java
│ │ │ ├── ExamApplication.java
│ │ │ ├── enums
│ │ │ ├── LoginTypeEnum.java
│ │ │ ├── QuestionEnum.java
│ │ │ ├── RoleEnum.java
│ │ │ └── ResultEnum.java
│ │ │ └── vo
│ │ │ ├── ActionVo.java
│ │ │ ├── PageVo.java
│ │ │ ├── QuestionOptionCreateVo.java
│ │ │ ├── ExamRecordVo.java
│ │ │ ├── RoleVo.java
│ │ │ ├── ExamDetailVo.java
│ │ │ ├── ExamQuestionSelectVo.java
│ │ │ ├── QuestionOptionVo.java
│ │ │ ├── ExamQuestionTypeVo.java
│ │ │ ├── ExamCardVo.java
│ │ │ ├── QuestionSelectionVo.java
│ │ │ ├── ExamPageVo.java
│ │ │ ├── ResultVO.java
│ │ │ ├── QuestionPageVo.java
│ │ │ ├── RecordDetailVo.java
│ │ │ ├── QuestionDetailVo.java
│ │ │ ├── UserVo.java
│ │ │ ├── UserInfoVo.java
│ │ │ ├── QuestionCreateSimplifyVo.java
│ │ │ ├── ExamCreateVo.java
│ │ │ ├── QuestionCreateVo.java
│ │ │ ├── JsonData.java
│ │ │ ├── QuestionVo.java
│ │ │ └── ExamVo.java
│ │ └── resources
│ │ └── application.yml
└── README.md
├── start.sh
├── Dockerfile
├── .gitignore
├── LICENSE
└── README.md
/doc/deploy/README.md:
--------------------------------------------------------------------------------
1 | # 在线考试系统的部署文档
--------------------------------------------------------------------------------
/frontend/src/components/tools/index.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/.gitattributes:
--------------------------------------------------------------------------------
1 | public/* linguist-vendored
--------------------------------------------------------------------------------
/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | nohup java -jar app.jar &
3 |
--------------------------------------------------------------------------------
/frontend/.env:
--------------------------------------------------------------------------------
1 | NODE_ENV=production
2 | VUE_APP_PREVIEW=false
3 |
--------------------------------------------------------------------------------
/frontend/src/components/Menu/index.js:
--------------------------------------------------------------------------------
1 | import SMenu from './menu'
2 | export default SMenu
3 |
--------------------------------------------------------------------------------
/frontend/src/components/Result/index.js:
--------------------------------------------------------------------------------
1 | import Result from './Result.vue'
2 | export default Result
3 |
--------------------------------------------------------------------------------
/frontend/tests/unit/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | jest: true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/frontend/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 120,
3 | "semi": false,
4 | "singleQuote": true
5 | }
6 |
--------------------------------------------------------------------------------
/doc/images/exam_join.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lsgwr/spring-boot-online-exam/HEAD/doc/images/exam_join.png
--------------------------------------------------------------------------------
/doc/images/exam_list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lsgwr/spring-boot-online-exam/HEAD/doc/images/exam_list.png
--------------------------------------------------------------------------------
/frontend/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lsgwr/spring-boot-online-exam/HEAD/frontend/public/logo.png
--------------------------------------------------------------------------------
/backend/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | *.iml
3 | src/main/resources/public
4 | src/main/resources/static
5 | target/
6 | dist/
--------------------------------------------------------------------------------
/doc/images/exam_create.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lsgwr/spring-boot-online-exam/HEAD/doc/images/exam_create.png
--------------------------------------------------------------------------------
/doc/images/exam_detail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lsgwr/spring-boot-online-exam/HEAD/doc/images/exam_detail.png
--------------------------------------------------------------------------------
/doc/images/exam_join2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lsgwr/spring-boot-online-exam/HEAD/doc/images/exam_join2.png
--------------------------------------------------------------------------------
/doc/images/exam_record.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lsgwr/spring-boot-online-exam/HEAD/doc/images/exam_record.png
--------------------------------------------------------------------------------
/doc/images/exam_update.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lsgwr/spring-boot-online-exam/HEAD/doc/images/exam_update.png
--------------------------------------------------------------------------------
/frontend/src/components/NoticeIcon/index.js:
--------------------------------------------------------------------------------
1 | import NoticeIcon from './NoticeIcon'
2 | export default NoticeIcon
3 |
--------------------------------------------------------------------------------
/frontend/src/components/PageHeader/index.js:
--------------------------------------------------------------------------------
1 | import PageHeader from './PageHeader'
2 | export default PageHeader
3 |
--------------------------------------------------------------------------------
/doc/images/question_list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lsgwr/spring-boot-online-exam/HEAD/doc/images/question_list.png
--------------------------------------------------------------------------------
/frontend/public/avatar2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lsgwr/spring-boot-online-exam/HEAD/frontend/public/avatar2.jpg
--------------------------------------------------------------------------------
/frontend/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lsgwr/spring-boot-online-exam/HEAD/frontend/src/assets/logo.png
--------------------------------------------------------------------------------
/frontend/src/components/GlobalFooter/index.js:
--------------------------------------------------------------------------------
1 | import GlobalFooter from './GlobalFooter'
2 | export default GlobalFooter
3 |
--------------------------------------------------------------------------------
/frontend/src/components/GlobalHeader/index.js:
--------------------------------------------------------------------------------
1 | import GlobalHeader from './GlobalHeader'
2 | export default GlobalHeader
3 |
--------------------------------------------------------------------------------
/backend/images/拦截器注入配置文件属性.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lsgwr/spring-boot-online-exam/HEAD/backend/images/拦截器注入配置文件属性.png
--------------------------------------------------------------------------------
/backend/images/拦截器注入配置文件属性2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lsgwr/spring-boot-online-exam/HEAD/backend/images/拦截器注入配置文件属性2.png
--------------------------------------------------------------------------------
/doc/images/question_create.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lsgwr/spring-boot-online-exam/HEAD/doc/images/question_create.png
--------------------------------------------------------------------------------
/doc/images/question_update.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lsgwr/spring-boot-online-exam/HEAD/doc/images/question_update.png
--------------------------------------------------------------------------------
/doc/references/images/JWT概要.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lsgwr/spring-boot-online-exam/HEAD/doc/references/images/JWT概要.png
--------------------------------------------------------------------------------
/frontend/public/home/cover1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lsgwr/spring-boot-online-exam/HEAD/frontend/public/home/cover1.jpg
--------------------------------------------------------------------------------
/frontend/public/home/cover2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lsgwr/spring-boot-online-exam/HEAD/frontend/public/home/cover2.jpg
--------------------------------------------------------------------------------
/frontend/public/home/cover3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lsgwr/spring-boot-online-exam/HEAD/frontend/public/home/cover3.jpg
--------------------------------------------------------------------------------
/frontend/public/home/cover4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lsgwr/spring-boot-online-exam/HEAD/frontend/public/home/cover4.jpg
--------------------------------------------------------------------------------
/frontend/public/home/cover5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lsgwr/spring-boot-online-exam/HEAD/frontend/public/home/cover5.jpg
--------------------------------------------------------------------------------
/frontend/src/components/Exception/index.js:
--------------------------------------------------------------------------------
1 | import ExceptionPage from './ExceptionPage.vue'
2 | export default ExceptionPage
3 |
--------------------------------------------------------------------------------
/frontend/src/components/SettingDrawer/index.js:
--------------------------------------------------------------------------------
1 | import SettingDrawer from './SettingDrawer'
2 | export default SettingDrawer
3 |
--------------------------------------------------------------------------------
/doc/references/images/登录校验方案.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lsgwr/spring-boot-online-exam/HEAD/doc/references/images/登录校验方案.png
--------------------------------------------------------------------------------
/frontend/src/components/DescriptionList/index.js:
--------------------------------------------------------------------------------
1 | import DescriptionList from './DescriptionList'
2 | export default DescriptionList
3 |
--------------------------------------------------------------------------------
/frontend/src/components/MultiTab/index.js:
--------------------------------------------------------------------------------
1 | import MultiTab from './MultiTab'
2 | import './index.less'
3 |
4 | export default MultiTab
5 |
--------------------------------------------------------------------------------
/frontend/src/components/StandardFormRow/index.js:
--------------------------------------------------------------------------------
1 | import StandardFormRow from './StandardFormRow'
2 |
3 | export default StandardFormRow
4 |
--------------------------------------------------------------------------------
/doc/references/images/单机tomcat应用登录检验.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lsgwr/spring-boot-online-exam/HEAD/doc/references/images/单机tomcat应用登录检验.png
--------------------------------------------------------------------------------
/doc/references/images/tomcat开启session共享.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lsgwr/spring-boot-online-exam/HEAD/doc/references/images/tomcat开启session共享.png
--------------------------------------------------------------------------------
/frontend/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 10.15.0
4 | cache: yarn
5 | script:
6 | - yarn
7 | - yarn run lint --no-fix && yarn run build
8 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/dto/package-info.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Created By Liang Shan Guang at 2019-05-16 23:38
3 | * Description : 用于接口数据传输
4 | */
5 | package lsgwr.exam.dto;
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/config/package-info.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Created By Liang Shan Guang at 2019-05-14 08:20
3 | * Description : 项目自定义配置
4 | */
5 | package lsgwr.exam.config;
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/entity/package-info.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Created By Liang Shan Guang at 2019-05-14 00:34
3 | * Description : 数据库实体类
4 | */
5 | package lsgwr.exam.entity;
--------------------------------------------------------------------------------
/frontend/src/components/index.less:
--------------------------------------------------------------------------------
1 | @import "~ant-design-vue/lib/style/index";
2 |
3 | // The prefix to use on all css classes from ant-pro.
4 | @ant-pro-prefix : ant-pro;
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/exception/package-info.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Created By Liang Shan Guang at 2019-05-17 07:51
3 | * Description : 自定义异常
4 | */
5 | package lsgwr.exam.exception;
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/service/package-info.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Created By Liang Shan Guang at 2019-05-14 08:20
3 | * Description : 服务接口与实现
4 | */
5 | package lsgwr.exam.service;
--------------------------------------------------------------------------------
/start.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | docker rm -f exam
3 | docker run -d -p 80:9527 -p 3306:3306 --name exam --privileged=true waterknife/exam /usr/sbin/init
4 | docker exec exam /lsg/entrypoint.sh
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/controller/package-info.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Created By Liang Shan Guang at 2019-05-14 08:20
3 | * Description : 对外REST接口
4 | */
5 | package lsgwr.exam.controller;
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/repository/package-info.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Created By Liang Shan Guang at 2019-05-14 08:20
3 | * Description : 数据库操作接口
4 | */
5 | package lsgwr.exam.repository;
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/utils/package-info.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Created By Liang Shan Guang at 2019-05-14 08:19
3 | * Description : 工具类, 大部分工具都可以在Hutool中找到
4 | */
5 | package lsgwr.exam.utils;
--------------------------------------------------------------------------------
/frontend/src/components/tools/DetailList.vue:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/frontend/src/utils/permissions.js:
--------------------------------------------------------------------------------
1 | export function actionToObject (json) {
2 | try {
3 | return JSON.parse(json)
4 | } catch (e) {
5 | console.log('err', e.message)
6 | }
7 | return []
8 | }
9 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # build docker for project
2 | FROM waterknife/centos-nginx-jdk8-mysql
3 | WORKDIR /lsg/
4 | COPY backend/target/*.jar ./app.jar
5 | COPY entrypoint.sh ./
6 | RUN chmod 777 entrypoint.sh
7 | EXPOSE 9527
8 |
--------------------------------------------------------------------------------
/frontend/public/loading/loading.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/views/404.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | 404 page
4 |
5 |
6 |
7 |
12 |
13 |
16 |
--------------------------------------------------------------------------------
/frontend/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es6",
4 | "baseUrl": ".",
5 | "paths": {
6 | "../*": ["src/*"]
7 | }
8 | },
9 | "exclude": ["node_modules", "dist"],
10 | "include": ["src/**/*"]
11 | }
12 |
--------------------------------------------------------------------------------
/doc/README.md:
--------------------------------------------------------------------------------
1 | # 在线考试系统功能开发
2 |
3 | ## 用户基本功能
4 |
5 | + 注册
6 | + 登录
7 | + 个人信息(查看和修改,头像头像修改需要用到FastDFS相关的接口)
8 |
9 | ## 考试展示和参加
10 |
11 | + 单选、多选、判断都能正常展示和参加考试做题
12 | + 计时和计分的功能
13 |
14 | ## 管理
15 |
16 | + 试题管理(单选、多选、判断的录入)
17 | + 考试管理(设置有效时间和给试卷组题)
18 |
--------------------------------------------------------------------------------
/frontend/src/layouts/BlankLayout.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
13 |
14 |
17 |
--------------------------------------------------------------------------------
/frontend/src/components/PageLoading/index.jsx:
--------------------------------------------------------------------------------
1 | import { Spin } from 'ant-design-vue'
2 |
3 | export default {
4 | name: 'PageLoading',
5 | render () {
6 | return (
7 |
8 |
)
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/frontend/src/layouts/index.js:
--------------------------------------------------------------------------------
1 | import UserLayout from './UserLayout'
2 | import BlankLayout from './BlankLayout'
3 | import BasicLayout from './BasicLayout'
4 | import RouteView from './RouteView'
5 | import PageView from './PageView'
6 |
7 | export { UserLayout, BasicLayout, BlankLayout, RouteView, PageView }
8 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/qo/package-info.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @note : 用于请求参数的实体类(Query Object)
3 | * @author : 梁山广
4 | * @version : V1.0 at 2018/7/18 17:32
5 | ***********************************************************/
6 | package lsgwr.exam.qo;
7 |
--------------------------------------------------------------------------------
/frontend/src/views/exception/403.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
14 |
15 |
18 |
--------------------------------------------------------------------------------
/frontend/src/views/exception/404.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
14 |
15 |
18 |
--------------------------------------------------------------------------------
/frontend/src/views/exception/500.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
14 |
15 |
18 |
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 | # local env files
6 | .env.local
7 | .env.*.local
8 |
9 | # Log files
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 |
14 | # Editor directories and files
15 | .idea
16 | .vscode
17 | *.suo
18 | *.ntvs*
19 | *.njsproj
20 | *.sln
21 | *.sw*
22 |
--------------------------------------------------------------------------------
/frontend/public/loading/option2/loading.css:
--------------------------------------------------------------------------------
1 | .preloading-animate{background:#ffffff;width:100%;height:100%;position:fixed;left:0;top:0;z-index:299;}.preloading-animate .preloading-wrapper{position:absolute;width:5rem;height:5rem;left:50%;top:50%;transform:translate(-50%,-50%);}.preloading-animate .preloading-wrapper .preloading-balls{font-size:5rem;}
--------------------------------------------------------------------------------
/frontend/src/router/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Router from 'vue-router'
3 | import { constantRouterMap } from '../config/router.config'
4 |
5 | Vue.use(Router)
6 |
7 | export default new Router({
8 | mode: 'history',
9 | base: process.env.BASE_URL,
10 | scrollBehavior: () => ({ y: 0 }),
11 | routes: constantRouterMap
12 | })
13 |
--------------------------------------------------------------------------------
/frontend/src/plugins/summernote.js:
--------------------------------------------------------------------------------
1 | // 引入summernote,参考https://blog.csdn.net/qq_24734285/article/details/80246093
2 | import 'bootstrap/dist/js/bootstrap.bundle.min'
3 | import 'bootstrap/dist/css/bootstrap.css'
4 | import 'font-awesome/css/font-awesome.css'
5 | import 'summernote'
6 | import 'summernote/dist/lang/summernote-zh-CN'
7 | import 'summernote/dist/summernote.css'
8 |
--------------------------------------------------------------------------------
/frontend/webstorm.config.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const path = require('path')
3 |
4 | function resolve (dir) {
5 | return path.join(__dirname, '.', dir)
6 | }
7 |
8 | module.exports = {
9 | context: path.resolve(__dirname, './'),
10 | resolve: {
11 | extensions: ['.js', '.vue', '.json'],
12 | alias: {
13 | '@': resolve('src')
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled class file
2 | *.class
3 |
4 | # Log file
5 | *.log
6 |
7 | # BlueJ files
8 | *.ctxt
9 |
10 | # Mobile Tools for Java (J2ME)
11 | .mtj.tmp/
12 |
13 | # Package Files #
14 | *.jar
15 | *.war
16 | *.nar
17 | *.ear
18 | *.zip
19 | *.tar.gz
20 | *.rar
21 |
22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
23 | hs_err_pid*
24 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/ExamApplication.java:
--------------------------------------------------------------------------------
1 | package lsgwr.exam;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 |
6 | @SpringBootApplication
7 | public class ExamApplication {
8 |
9 | public static void main(String[] args) {
10 | SpringApplication.run(ExamApplication.class, args);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/frontend/src/views/Home.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
22 |
--------------------------------------------------------------------------------
/frontend/src/api/user.js:
--------------------------------------------------------------------------------
1 | // 自己的借口呀:用户的注册和登录等服务,注意所有的接口应该都现在index.js里面注册,方便统一管理
2 |
3 | import api from './index'
4 | import { axios } from '../utils/request'
5 |
6 | export function login (parameter) {
7 | return axios({
8 | url: api.UserLogin,
9 | method: 'post',
10 | data: parameter
11 | })
12 | }
13 |
14 | export function register (parameter) {
15 | return axios({
16 | url: api.UserRegister,
17 | method: 'post',
18 | data: parameter
19 | })
20 | }
21 |
--------------------------------------------------------------------------------
/frontend/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@vue/app',
4 | [
5 | '@babel/preset-env',
6 | {
7 | 'useBuiltIns': 'entry'
8 | }
9 | ]
10 | ]
11 | // if your use import on Demand, Use this code
12 | // ,
13 | // plugins: [
14 | // [ 'import', {
15 | // 'libraryName': 'ant-design-vue',
16 | // 'libraryDirectory': 'es',
17 | // 'style': true // `style: true` 会加载 less 文件
18 | // } ]
19 | // ]
20 | }
21 |
--------------------------------------------------------------------------------
/frontend/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 |
4 | import app from './modules/app'
5 | import user from './modules/user'
6 | import permission from './modules/permission'
7 | import getters from './getters'
8 |
9 | Vue.use(Vuex)
10 |
11 | export default new Vuex.Store({
12 | modules: {
13 | app,
14 | user,
15 | permission
16 | },
17 | state: {
18 |
19 | },
20 | mutations: {
21 |
22 | },
23 | actions: {
24 |
25 | },
26 | getters
27 | })
28 |
--------------------------------------------------------------------------------
/frontend/docs/multi-tabs.md:
--------------------------------------------------------------------------------
1 | 多(页签)标签 模式
2 | ====
3 |
4 |
5 | ## 让框架支持打开的页面增加多标签,可随时切换
6 |
7 | ### 关于如何移除该功能 组件
8 | 1. 移除 `/src/components/layouts/BasicLayout.vue` L3, L12, L19
9 | ```vue
10 |
11 | ```
12 | 2. 移除 `/src/config/defaultSettings.js` L25
13 |
14 | 3. 移除 `src/store/modules/app.js` L27, L76-L79, L118-L120
15 |
16 | 4. 移除 `src/utils/mixin.js` L21
17 |
18 | 5. 删除组件目录 `src/components/MultiTab`
19 |
20 | > 以上 `L x` 均代表行N ,如 L3 = 行3
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/qo/DownloadQo.java:
--------------------------------------------------------------------------------
1 | package lsgwr.exam.qo;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.Data;
5 | import lombok.NoArgsConstructor;
6 |
7 | /***********************************************************
8 | * @note : 要下载的文件的路径
9 | * @author : 梁山广
10 | * @version : V1.0 at 2019/5/19 20:10
11 | ***********************************************************/
12 | @Data
13 | @AllArgsConstructor
14 | @NoArgsConstructor
15 | public class DownloadQo {
16 | String path;
17 | }
18 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/enums/LoginTypeEnum.java:
--------------------------------------------------------------------------------
1 | package lsgwr.exam.enums;
2 |
3 | import lombok.Getter;
4 |
5 | /**
6 | * 用户账号类型的枚举
7 | * @author liangshanguang
8 | */
9 | @Getter
10 | public enum LoginTypeEnum {
11 | /**
12 | * 用户的账号类型,1代表用户名,2代表邮箱
13 | */
14 | USERNAME(1, "用户名"),
15 | EMAIL(2, "邮箱");
16 |
17 |
18 | LoginTypeEnum(Integer type, String name) {
19 | this.type = type;
20 | this.name = name;
21 | }
22 |
23 | private Integer type;
24 | private String name;
25 | }
26 |
--------------------------------------------------------------------------------
/frontend/src/store/getters.js:
--------------------------------------------------------------------------------
1 | const getters = {
2 | device: state => state.app.device,
3 | theme: state => state.app.theme,
4 | color: state => state.app.color,
5 | token: state => state.user.token,
6 | avatar: state => state.user.avatar,
7 | nickname: state => state.user.name,
8 | welcome: state => state.user.welcome,
9 | roles: state => state.user.roles,
10 | userInfo: state => state.user.info,
11 | addRouters: state => state.permission.addRouters,
12 | multiTab: state => state.app.multiTab
13 | }
14 |
15 | export default getters
16 |
--------------------------------------------------------------------------------
/frontend/src/views/home/default.less:
--------------------------------------------------------------------------------
1 | @import "~ant-design-vue/lib/style/themes/default";
2 | @site-heading-color: #0d1a26;
3 | @site-text-color: #314659;
4 | @site-text-color-secondary: #697b8c;
5 | @site-border-color-split: #ebedf0;
6 | @border-color: rgba(229, 231, 235, 100);
7 |
8 | article, aside, blockquote, body, button, code, dd, details, div, dl, dt, fieldset, figcaption, figure, footer, form, h1, h2, h3, h4, h5, h6, header, hgroup, hr, input, legend, li, menu, nav, ol, p, pre, section, td, textarea, th, ul {
9 | margin: 0;
10 | padding: 0;
11 | }
12 |
--------------------------------------------------------------------------------
/frontend/src/components/Exception/type.js:
--------------------------------------------------------------------------------
1 | const types = {
2 | 403: {
3 | img: 'https://gw.alipayobjects.com/zos/rmsportal/wZcnGqRDyhPOEYFcZDnb.svg',
4 | title: '403',
5 | desc: '抱歉,你无权访问该页面'
6 | },
7 | 404: {
8 | img: 'https://gw.alipayobjects.com/zos/rmsportal/KpnpchXsobRgLElEozzI.svg',
9 | title: '404',
10 | desc: '抱歉,你访问的页面不存在或仍在开发中'
11 | },
12 | 500: {
13 | img: 'https://gw.alipayobjects.com/zos/rmsportal/RVRUAYdCGeYNBWoKiIwB.svg',
14 | title: '500',
15 | desc: '抱歉,服务器出错了'
16 | }
17 | }
18 |
19 | export default types
20 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/repository/PageRepository.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : 页面的数据库操作类
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-05-26 12:41
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.repository;
8 |
9 | import lsgwr.exam.entity.Page;
10 | import org.springframework.data.jpa.repository.JpaRepository;
11 |
12 | public interface PageRepository extends JpaRepository {
13 | }
14 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/repository/RoleRepository.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : 角色的数据库操作类
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-05-14 08:29
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.repository;
8 |
9 | import lsgwr.exam.entity.Role;
10 | import org.springframework.data.jpa.repository.JpaRepository;
11 |
12 | public interface RoleRepository extends JpaRepository {
13 | }
14 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/repository/ActionRepository.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : Action的数据库操作类
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-05-26 12:39
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.repository;
8 |
9 | import lsgwr.exam.entity.Action;
10 | import org.springframework.data.jpa.repository.JpaRepository;
11 |
12 | public interface ActionRepository extends JpaRepository {
13 | }
14 |
--------------------------------------------------------------------------------
/frontend/src/core/lazy_use.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import VueStorage from 'vue-ls'
3 | import config from '../config/defaultSettings'
4 |
5 | // base library
6 | import '../core/lazy_lib/components_use'
7 | import Viser from 'viser-vue'
8 |
9 | // ext library
10 | import VueClipboard from 'vue-clipboard2'
11 | import PermissionHelper from '../utils/helper/permission'
12 | import './directives/action'
13 |
14 | VueClipboard.config.autoSetContainer = true
15 |
16 | Vue.use(Viser)
17 |
18 | Vue.use(VueStorage, config.storageOptions)
19 | Vue.use(VueClipboard)
20 | Vue.use(PermissionHelper)
21 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/repository/QuestionTypeRepository.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description :
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-05-14 08:28
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.repository;
8 |
9 | import lsgwr.exam.entity.QuestionType;
10 | import org.springframework.data.jpa.repository.JpaRepository;
11 |
12 | public interface QuestionTypeRepository extends JpaRepository {
13 | }
14 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/repository/QuestionLevelRepository.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description :
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-05-14 08:26
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.repository;
8 |
9 | import lsgwr.exam.entity.QuestionLevel;
10 | import org.springframework.data.jpa.repository.JpaRepository;
11 |
12 | public interface QuestionLevelRepository extends JpaRepository {
13 | }
14 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/repository/QuestionOptionRepository.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description :
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-05-14 08:27
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.repository;
8 |
9 | import lsgwr.exam.entity.QuestionOption;
10 | import org.springframework.data.jpa.repository.JpaRepository;
11 |
12 | public interface QuestionOptionRepository extends JpaRepository {
13 | }
14 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/repository/ExamRecordLevelRepository.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description :
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-05-14 08:24
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.repository;
8 |
9 | import lsgwr.exam.entity.ExamRecordLevel;
10 | import org.springframework.data.jpa.repository.JpaRepository;
11 |
12 | public interface ExamRecordLevelRepository extends JpaRepository {
13 | }
14 |
--------------------------------------------------------------------------------
/frontend/src/views/home/ListItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
![]()
7 |
8 |
{{ item.title }}
9 |
{{ item.content }}
10 |
11 |
12 |
13 |
14 |
25 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/repository/QuestionCategoryRepository.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description :
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-05-14 08:25
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.repository;
8 |
9 | import lsgwr.exam.entity.QuestionCategory;
10 | import org.springframework.data.jpa.repository.JpaRepository;
11 |
12 | public interface QuestionCategoryRepository extends JpaRepository {
13 | }
14 |
--------------------------------------------------------------------------------
/frontend/src/components/index.js:
--------------------------------------------------------------------------------
1 | // pro components
2 | import STable from '../components/Table'
3 | import MultiTab from '../components/MultiTab'
4 | import Result from '../components/Result'
5 | import TagSelect from '../components/TagSelect'
6 | import ExceptionPage from '../components/Exception'
7 | import StandardFormRow from '../components/StandardFormRow'
8 | import DescriptionList from '../components/DescriptionList'
9 |
10 | export {
11 | ExceptionPage,
12 | Result,
13 | STable,
14 | MultiTab,
15 | TagSelect,
16 | StandardFormRow,
17 | // 兼容写法,请勿继续使用
18 | DescriptionList as DetailList
19 | }
20 |
--------------------------------------------------------------------------------
/frontend/src/utils/domUtil.js:
--------------------------------------------------------------------------------
1 | export const setDocumentTitle = function (title) {
2 | document.title = title
3 | const ua = navigator.userAgent
4 | // eslint-disable-next-line
5 | const regex = /\bMicroMessenger\/([\d\.]+)/
6 | if (regex.test(ua) && /ip(hone|od|ad)/i.test(ua)) {
7 | const i = document.createElement('iframe')
8 | i.src = '/favicon.ico'
9 | i.style.display = 'none'
10 | i.onload = function () {
11 | setTimeout(function () {
12 | i.remove()
13 | }, 9)
14 | }
15 | document.body.appendChild(i)
16 | }
17 | }
18 |
19 | export const domTitle = 'Online Exam'
20 |
--------------------------------------------------------------------------------
/frontend/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | moduleFileExtensions: [
3 | 'js',
4 | 'jsx',
5 | 'json',
6 | 'vue'
7 | ],
8 | transform: {
9 | '^.+\\.vue$': 'vue-jest',
10 | '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',
11 | '^.+\\.jsx?$': 'babel-jest'
12 | },
13 | moduleNameMapper: {
14 | '^../(.*)$': '/src/$1'
15 | },
16 | snapshotSerializers: [
17 | 'jest-serializer-vue'
18 | ],
19 | testMatch: [
20 | '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
21 | ],
22 | testURL: 'http://localhost/'
23 | }
24 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/dto/RegisterDTO.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : 注册接口参数
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-05-16 23:40
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.dto;
8 |
9 | import lombok.Data;
10 |
11 | @Data
12 | public class RegisterDTO {
13 | private String email;
14 | private String password;
15 | private String password2;
16 | private String mobile;
17 | /**
18 | * 验证码
19 | */
20 | private String captcha;
21 | }
22 |
--------------------------------------------------------------------------------
/frontend/src/components/MultiTab/index.less:
--------------------------------------------------------------------------------
1 | @import '../index';
2 |
3 | @multi-tab-prefix-cls: ~"@{ant-pro-prefix}-multi-tab";
4 | @multi-tab-wrapper-prefix-cls: ~"@{ant-pro-prefix}-multi-tab-wrapper";
5 |
6 | /*
7 | .topmenu .@{multi-tab-prefix-cls} {
8 | max-width: 1200px;
9 | margin: -23px auto 24px auto;
10 | }
11 | */
12 | .@{multi-tab-prefix-cls} {
13 | margin: -23px -24px 24px -24px;
14 | background: #fff;
15 | }
16 |
17 | .topmenu .@{multi-tab-wrapper-prefix-cls} {
18 | max-width: 1200px;
19 | margin: 0 auto;
20 | }
21 |
22 | .topmenu.content-width-Fluid .@{multi-tab-wrapper-prefix-cls} {
23 | max-width: 100%;
24 | margin: 0 auto;
25 | }
26 |
--------------------------------------------------------------------------------
/frontend/src/main.js:
--------------------------------------------------------------------------------
1 | // ie polyfill
2 | import '@babel/polyfill'
3 |
4 | import Vue from 'vue'
5 | import App from './App.vue'
6 | import router from './router'
7 | import store from './store/'
8 | import { VueAxios } from './utils/request'
9 |
10 | import bootstrap from './core/bootstrap'
11 | import './core/use'
12 | import './permission' // permission control
13 | import './utils/filter' // global filter
14 | Vue.config.productionTip = false
15 |
16 | // mount axios Vue.$http and this.$http
17 | Vue.use(VueAxios)
18 |
19 | new Vue({
20 | router,
21 | store,
22 | created () {
23 | bootstrap()
24 | },
25 | render: h => h(App)
26 | }).$mount('#app')
27 |
--------------------------------------------------------------------------------
/frontend/src/components/tools/Logo.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ title }}
6 |
7 |
8 |
9 |
10 |
32 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/entity/QuestionOption.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : 问题的选项,适用于单选、多选和判断
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019/5/14 07:48
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.entity;
8 |
9 |
10 | import lombok.Data;
11 |
12 | import javax.persistence.Entity;
13 | import javax.persistence.Id;
14 |
15 | @Data
16 | @Entity
17 | public class QuestionOption {
18 | @Id
19 | private String questionOptionId;
20 | private String questionOptionContent;
21 | private String questionOptionDescription;
22 | }
23 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/vo/ActionVo.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : Action的前端展示类
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-05-26 13:50
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.vo;
8 |
9 | import com.fasterxml.jackson.annotation.JsonProperty;
10 | import lombok.Data;
11 |
12 | @Data
13 | public class ActionVo {
14 | @JsonProperty("action")
15 | private String actionName;
16 |
17 | @JsonProperty("describe")
18 | private String actionDescription;
19 |
20 | @JsonProperty("defaultCheck")
21 | private Boolean defaultCheck;
22 | }
23 |
--------------------------------------------------------------------------------
/frontend/src/core/use.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import VueStorage from 'vue-ls'
3 | import config from '../config/defaultSettings'
4 |
5 | // base library
6 | import Antd from 'ant-design-vue'
7 | import Viser from 'viser-vue'
8 | import VueCropper from 'vue-cropper'
9 | import 'ant-design-vue/dist/antd.less'
10 |
11 | // ext library
12 | import VueClipboard from 'vue-clipboard2'
13 | import PermissionHelper from '../utils/helper/permission'
14 | // import '../components/use'
15 | import './directives/action'
16 |
17 | VueClipboard.config.autoSetContainer = true
18 |
19 | Vue.use(Antd)
20 | Vue.use(Viser)
21 |
22 | Vue.use(VueStorage, config.storageOptions)
23 | Vue.use(VueClipboard)
24 | Vue.use(PermissionHelper)
25 | Vue.use(VueCropper)
26 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/repository/ExamRepository.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description :
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-05-14 08:22
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.repository;
8 |
9 | import lsgwr.exam.entity.Exam;
10 | import org.springframework.data.jpa.repository.JpaRepository;
11 | import org.springframework.data.jpa.repository.Query;
12 |
13 | import java.util.List;
14 |
15 | public interface ExamRepository extends JpaRepository {
16 | @Query("select e from Exam e order by e.updateTime desc")
17 | List findAll();
18 | }
19 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/qo/UploadModel.java:
--------------------------------------------------------------------------------
1 | package lsgwr.exam.qo;
2 |
3 | /***********************************************************
4 | * @Description : 文件传输的实体类
5 | * @author : 梁山广
6 | * @date : 2017/8/19 15:51
7 | * @version : V1.0
8 | ***********************************************************/
9 |
10 | import lombok.AllArgsConstructor;
11 | import lombok.Data;
12 | import lombok.NoArgsConstructor;
13 | import org.springframework.web.multipart.MultipartFile;
14 |
15 | @Data
16 | @AllArgsConstructor
17 | @NoArgsConstructor
18 | public class UploadModel {
19 | /**
20 | * 要保存的文件
21 | */
22 | private MultipartFile[] files;
23 | /**
24 | * 文件要存储的文件夹
25 | */
26 | private String dir;
27 | }
28 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/qo/UploadModel2.java:
--------------------------------------------------------------------------------
1 | package lsgwr.exam.qo;
2 |
3 | /***********************************************************
4 | * @Description : 文件传输的实体类,支持单个文件
5 | * @author : 梁山广
6 | * @date : 2018/5/19 15:51
7 | * @version : V1.0
8 | ***********************************************************/
9 |
10 | import lombok.AllArgsConstructor;
11 | import lombok.Data;
12 | import lombok.NoArgsConstructor;
13 | import org.springframework.web.multipart.MultipartFile;
14 |
15 | @Data
16 | @AllArgsConstructor
17 | @NoArgsConstructor
18 | public class UploadModel2 {
19 | /**
20 | * 要保存的文件列表
21 | */
22 | private MultipartFile file;
23 | /**
24 | * 文件要存储的文件夹
25 | */
26 | private String dir;
27 | }
28 |
--------------------------------------------------------------------------------
/frontend/docs/add-page-loading-animate.md:
--------------------------------------------------------------------------------
1 | 为首屏增加 加载动画
2 | ====
3 |
4 |
5 |
6 | ## 需求
7 |
8 | > 为了缓解用户第一次访问时,加载 JS 过大所导致用户等待白屏时间过长导致的用户体验不好,进行的一个优化动效。
9 |
10 |
11 |
12 | ## 实现方案
13 |
14 | 1. 将 动画加载 dom 元素放在 #app 内,Vue 生命周期开始时,会自动清掉 #app 下的所有元素。
15 | 2. 将 动画加载 dom 元素放在 body 下,Vue 生命周期开始时 App.vue (created, mounted) 调用 `../utils/utll` 下的 removeLoadingAnimate(#id, timeout) 则会移除加载动画
16 |
17 | 最后一步:
18 | 将样式插入到 `public/index.html` 文件的 `` 最好写成内联 ``
19 |
20 |
21 |
22 | ----
23 |
24 | 目前提供有两个样式,均在 `public/loading` 文件夹内。且 pro 已经默认使用了一套 loading 动画方案,可以直接参考 `public/index.html`
25 |
26 |
27 | ## 写在最后
28 |
29 | 目前 pro 有页面 overflow 显示出浏览器滚动条时,页面会抖动一下的问题。
30 |
31 | 欢迎各位提供能解决的方案和实现 demo。如果在条件允许的情况下,建议请直接使用 pro 进行改造,也欢迎直接 PR 到 pro 的仓库
32 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/entity/Page.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : 前端页面实体类
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-05-26 12:30
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.entity;
8 |
9 | import lombok.Data;
10 |
11 | import javax.persistence.Entity;
12 | import javax.persistence.GeneratedValue;
13 | import javax.persistence.Id;
14 |
15 | @Data
16 | @Entity
17 | public class Page {
18 | @Id
19 | @GeneratedValue
20 | private Integer pageId;
21 |
22 | private String pageName;
23 |
24 | private String pageDescription;
25 |
26 | private String actionIds;
27 | }
28 |
--------------------------------------------------------------------------------
/frontend/src/store/mutation-types.js:
--------------------------------------------------------------------------------
1 | export const ACCESS_TOKEN = 'Access-Token'
2 | export const SIDEBAR_TYPE = 'SIDEBAR_TYPE'
3 | export const DEFAULT_THEME = 'DEFAULT_THEME'
4 | export const DEFAULT_LAYOUT_MODE = 'DEFAULT_LAYOUT_MODE'
5 | export const DEFAULT_COLOR = 'DEFAULT_COLOR'
6 | export const DEFAULT_COLOR_WEAK = 'DEFAULT_COLOR_WEAK'
7 | export const DEFAULT_FIXED_HEADER = 'DEFAULT_FIXED_HEADER'
8 | export const DEFAULT_FIXED_SIDEMENU = 'DEFAULT_FIXED_SIDEMENU'
9 | export const DEFAULT_FIXED_HEADER_HIDDEN = 'DEFAULT_FIXED_HEADER_HIDDEN'
10 | export const DEFAULT_CONTENT_WIDTH_TYPE = 'DEFAULT_CONTENT_WIDTH_TYPE'
11 | export const DEFAULT_MULTI_TAB = 'DEFAULT_MULTI_TAB'
12 |
13 | export const CONTENT_WIDTH_TYPE = {
14 | Fluid: 'Fluid',
15 | Fixed: 'Fixed'
16 | }
17 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/entity/ExamRecordLevel.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : 考试得分级别
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019/5/14 07:44
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.entity;
8 |
9 |
10 | import lombok.Data;
11 |
12 | import javax.persistence.Entity;
13 | import javax.persistence.GeneratedValue;
14 | import javax.persistence.Id;
15 |
16 | @Data
17 | @Entity
18 | public class ExamRecordLevel {
19 | @Id
20 | @GeneratedValue
21 | private Integer examRecordLevelId;
22 | private String examRecordLevelName;
23 | private String examRecordLevelDescription;
24 | }
25 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/enums/QuestionEnum.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : 问题类型的的枚举
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-05-18 12:00
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.enums;
8 |
9 | import lombok.Getter;
10 |
11 | @Getter
12 | public enum QuestionEnum {
13 |
14 | /**
15 | * 问题类型
16 | */
17 | RADIO(1, "单选题"),
18 | CHECK(2, "多选题"),
19 | JUDGE(3, "判断题");
20 |
21 |
22 | QuestionEnum(Integer id, String role) {
23 | this.id = id;
24 | this.role = role;
25 | }
26 |
27 | private Integer id;
28 | private String role;
29 | }
30 |
--------------------------------------------------------------------------------
/frontend/src/utils/axios.js:
--------------------------------------------------------------------------------
1 | const VueAxios = {
2 | vm: {},
3 | // eslint-disable-next-line no-unused-vars
4 | install (Vue, instance) {
5 | if (this.installed) {
6 | return
7 | }
8 | this.installed = true
9 |
10 | if (!instance) {
11 | // eslint-disable-next-line no-console
12 | console.error('You have to install axios')
13 | return
14 | }
15 |
16 | Vue.axios = instance
17 |
18 | Object.defineProperties(Vue.prototype, {
19 | axios: {
20 | get: function get () {
21 | return instance
22 | }
23 | },
24 | $http: {
25 | get: function get () {
26 | return instance
27 | }
28 | }
29 | })
30 | }
31 | }
32 |
33 | export {
34 | VueAxios
35 | }
36 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/entity/Action.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : 前端的操作
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-05-26 12:31
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.entity;
8 |
9 | import lombok.Data;
10 |
11 | import javax.persistence.Entity;
12 | import javax.persistence.GeneratedValue;
13 | import javax.persistence.Id;
14 |
15 | @Data
16 | @Entity
17 | public class Action {
18 | @Id
19 | @GeneratedValue
20 | private Integer actionId;
21 |
22 | private String actionName;
23 |
24 | private String actionDescription;
25 |
26 | private Boolean defaultCheck;
27 | }
28 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/enums/RoleEnum.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : 用户角色的枚举
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-05-18 12:00
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.enums;
8 |
9 | import lombok.Getter;
10 |
11 | @Getter
12 | public enum RoleEnum {
13 |
14 | /**
15 | * 用户角色,和数据库里面的role表相对应
16 | */
17 | ADMIN(1, "管理员"),
18 | TEACHER(2, "教师"),
19 | STUDENT(3, "学生");
20 |
21 |
22 | RoleEnum(Integer id, String role) {
23 | this.id = id;
24 | this.role = role;
25 | }
26 |
27 | private Integer id;
28 | private String role;
29 | }
30 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/vo/PageVo.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : Action的前端展示类
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-05-26 13:46
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.vo;
8 |
9 | import com.fasterxml.jackson.annotation.JsonProperty;
10 | import lombok.Data;
11 |
12 | import java.util.List;
13 |
14 | @Data
15 | public class PageVo {
16 | @JsonProperty("actionEntitySet")
17 | private List actionVoList;
18 |
19 | @JsonProperty("permissionId")
20 | private String pageName;
21 |
22 | @JsonProperty("permissionName")
23 | private String pageDescription;
24 | }
25 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/vo/QuestionOptionCreateVo.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : 问题选项的外层对象
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-06-02 20:23
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.vo;
8 |
9 | import com.fasterxml.jackson.annotation.JsonProperty;
10 | import lombok.Data;
11 |
12 | @Data
13 | public class QuestionOptionCreateVo {
14 |
15 | /**
16 | * 问题的内容
17 | */
18 | @JsonProperty("content")
19 | private String questionOptionContent;
20 |
21 | /**
22 | * 当前的选项是否是问题大答案
23 | */
24 | @JsonProperty("answer")
25 | private Boolean answer = false;
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/frontend/src/utils/filter.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import moment from 'moment'
3 | import 'moment/locale/zh-cn'
4 | import $ from 'jquery'
5 | moment.locale('zh-cn')
6 |
7 | Vue.filter('NumberFormat', function (value) {
8 | if (!value) {
9 | return '0'
10 | }
11 | const intPartFormat = value.toString().replace(/(\d)(?=(?:\d{3})+$)/g, '$1,') // 将整数部分逢三一断
12 | return intPartFormat
13 | })
14 |
15 | Vue.filter('dayjs', function (dataStr, pattern = 'YYYY-MM-DD HH:mm:ss') {
16 | return moment(dataStr).format(pattern)
17 | })
18 |
19 | Vue.filter('moment', function (dataStr, pattern = 'YYYY-MM-DD HH:mm:ss') {
20 | return moment(dataStr).format(pattern)
21 | })
22 |
23 | Vue.filter('imgSrcFilter', function (content) {
24 | return $(content).children('img').attr('src')
25 | })
26 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/qo/LoginQo.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : 登录的查询参数
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-05-19 20:18
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.qo;
8 |
9 | import lombok.AllArgsConstructor;
10 | import lombok.Data;
11 | import lombok.NoArgsConstructor;
12 |
13 | @Data
14 | @AllArgsConstructor
15 | @NoArgsConstructor
16 | public class LoginQo {
17 | /**
18 | * 1表示用户名,2表示邮箱
19 | */
20 | private Integer loginType;
21 | /**
22 | * 用户名/邮箱的字符串
23 | */
24 | private String userInfo;
25 | /**
26 | * 用户密码
27 | */
28 | private String password;
29 | }
30 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/vo/ExamRecordVo.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : 考试记录VO
3 | * @author : 梁山广(Liang Shan Guang)
4 | * @date : 2019/10/25 7:42
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.vo;
8 |
9 | import lsgwr.exam.entity.Exam;
10 | import lsgwr.exam.entity.ExamRecord;
11 | import lsgwr.exam.entity.User;
12 | import lombok.Data;
13 |
14 | @Data
15 | public class ExamRecordVo {
16 | /**
17 | * 当前考试记录对应的考试
18 | */
19 | private Exam exam;
20 |
21 | /**
22 | * 当前考试对应的内容
23 | */
24 | private ExamRecord examRecord;
25 |
26 | /**
27 | * 参加考试的用户信息
28 | */
29 | private User user;
30 | }
31 |
--------------------------------------------------------------------------------
/frontend/src/core/icons.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Custom icon list
3 | * All icons are loaded here for easy management
4 | * @see https://vue.ant.design/components/icon/#Custom-Font-Icon
5 | *
6 | * 自定义图标加载表
7 | * 所有图标均从这里加载,方便管理
8 | */
9 | import bxAnaalyse from '../assets/icons/bx-analyse.svg?inline' // path to your '*.svg?inline' file.
10 | import examList from '../assets/icons/exam-list.svg?inline' // path to your '*.svg?inline' file.
11 | import examAdmin from '../assets/icons/exam-admin.svg?inline' // path to your '*.svg?inline' file.
12 | import questionAdmin from '../assets/icons/question-admin.svg?inline' // path to your '*.svg?inline' file.
13 | import mine from '../assets/icons/mine.svg?inline' // path to your '*.svg?inline' file.
14 |
15 | export { bxAnaalyse, examList, examAdmin, questionAdmin, mine }
16 |
--------------------------------------------------------------------------------
/frontend/src/views/home/List.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
35 |
36 |
39 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/enums/ResultEnum.java:
--------------------------------------------------------------------------------
1 | package lsgwr.exam.enums;
2 |
3 | import lombok.Getter;
4 |
5 | @Getter
6 | public enum ResultEnum {
7 | // 下面是本项目用到的所有错误码
8 | REGISTER_SUCCESS(0, "注册成功"),
9 | REGISTER_FAILED(-2, "注册失败"),
10 | LOGIN_SUCCESS(0, "登录成功"),
11 | LOGIN_FAILED(-1, "用户名或者密码错误"),
12 | GET_INFO_SUCCESS(0, "获取用户信息成功"),
13 | PARAM_ERR(1, "参数不正确"),
14 | PRODUCT_NOT_EXIST(10, "用户不存在"),
15 | PRODUCT_STOCK_ERR(11, "考试信息异常"),
16 | ORDER_STATUS_ERR(14, "考试状态异常"),
17 | ORDER_UPDATE_ERR(15, "考试更新异常"),
18 | ORDER_DETAIL_EMPTY(16, "用户详情为空");
19 |
20 | ResultEnum(Integer code, String message) {
21 | this.code = code;
22 | this.message = message;
23 | }
24 |
25 | private Integer code;
26 | private String message;
27 | }
28 |
--------------------------------------------------------------------------------
/frontend/src/assets/icons/mine.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/vo/RoleVo.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : 角色的实体类
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-05-26 13:27
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.vo;
8 |
9 | import com.fasterxml.jackson.annotation.JsonProperty;
10 | import lombok.Data;
11 |
12 | import java.util.List;
13 |
14 | @Data
15 | public class RoleVo {
16 | @JsonProperty("id")
17 | private String roleName;
18 |
19 | @JsonProperty("name")
20 | private String roleDescription;
21 |
22 | @JsonProperty("describe")
23 | private String roleDetail;
24 |
25 | @JsonProperty("permissions")
26 | private List pageVoList;
27 | }
28 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/repository/ExamRecordRepository.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description :
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-05-14 08:23
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.repository;
8 |
9 | import lsgwr.exam.entity.ExamRecord;
10 | import org.springframework.data.jpa.repository.JpaRepository;
11 |
12 | import java.util.List;
13 |
14 | public interface ExamRecordRepository extends JpaRepository {
15 | /**
16 | * 获取指定用户参加过的所有考试
17 | *
18 | * @param userId 用户id
19 | * @return 用户参加过的所有考试
20 | */
21 | List findByExamJoinerIdOrderByExamJoinDateDesc(String userId);
22 | }
23 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/vo/ExamDetailVo.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : 考试详情的实体类
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-06-24 08:14
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.vo;
8 |
9 | import lsgwr.exam.entity.Exam;
10 | import lombok.Data;
11 |
12 | @Data
13 | public class ExamDetailVo {
14 | /**
15 | * 考试的基本信息对象
16 | */
17 | private Exam exam;
18 |
19 | /**
20 | * 单选题的id数组
21 | */
22 | private String[] radioIds;
23 |
24 | /**
25 | * 多选题的id数组
26 | */
27 | private String[] checkIds;
28 |
29 | /**
30 | * 判断题的id数组
31 | */
32 | private String[] judgeIds;
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/config/ServletConfig.java:
--------------------------------------------------------------------------------
1 | package lsgwr.exam.config;
2 |
3 | import org.springframework.boot.web.server.ConfigurableWebServerFactory;
4 | import org.springframework.boot.web.server.ErrorPage;
5 | import org.springframework.boot.web.server.WebServerFactoryCustomizer;
6 | import org.springframework.context.annotation.Bean;
7 | import org.springframework.context.annotation.Configuration;
8 | import org.springframework.http.HttpStatus;
9 |
10 | @Configuration
11 | public class ServletConfig {
12 | @Bean
13 | public WebServerFactoryCustomizer webServerFactoryCustomizer() {
14 | return factory -> {
15 | ErrorPage error404Page = new ErrorPage(HttpStatus.NOT_FOUND, "/");
16 | factory.addErrorPages(error404Page);
17 | };
18 | }
19 | }
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/repository/QuestionRepository.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description :
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-05-14 08:25
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.repository;
8 |
9 | import lsgwr.exam.entity.Question;
10 | import org.springframework.data.jpa.repository.JpaRepository;
11 | import org.springframework.data.jpa.repository.Query;
12 |
13 | import java.util.List;
14 |
15 | public interface QuestionRepository extends JpaRepository {
16 | List findByQuestionTypeId(Integer id);
17 | @Query("select q from Question q order by q.updateTime desc")
18 | List findAll();
19 | }
20 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/vo/ExamQuestionSelectVo.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : 考试中的题目
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-06-17 23:10
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.vo;
8 |
9 | import com.fasterxml.jackson.annotation.JsonProperty;
10 | import lombok.Data;
11 |
12 | @Data
13 | public class ExamQuestionSelectVo {
14 | @JsonProperty("id")
15 | private String questionId;
16 |
17 | @JsonProperty("name")
18 | private String questionName;
19 |
20 | /**
21 | * 这个问题是否被选为了考试中的题目.默认是false,经过前端修改后可能会变成true,
22 | * 传回来用于创建问题
23 | */
24 | @JsonProperty("checked")
25 | private Boolean checked = false;
26 | }
27 |
--------------------------------------------------------------------------------
/frontend/src/assets/background.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/plugins/bootstrap-table.js:
--------------------------------------------------------------------------------
1 | import 'bootstrap/dist/css/bootstrap.min.css'
2 | import 'bootstrap-vue/dist/bootstrap-vue.css'
3 | import 'bootstrap-table/dist/bootstrap-table.min.css'
4 | import '@fortawesome/fontawesome-free/css/all.min.css'
5 | import Vue from 'vue'
6 | import 'bootstrap'
7 | import 'tableexport.jquery.plugin/libs/FileSaver/FileSaver.min.js'
8 | import 'tableexport.jquery.plugin/tableExport.min.js'
9 | import 'bootstrap-table/dist/bootstrap-table'
10 | import BootstrapTable from 'bootstrap-table/dist/bootstrap-table-vue.esm'
11 | import 'bootstrap-table/dist/extensions/export/bootstrap-table-export'
12 | import 'bootstrap-table/dist/extensions/toolbar/bootstrap-table-toolbar.min'
13 | import jQuery from 'jquery'
14 | window.jQuery = jQuery
15 | window.$ = jQuery
16 | Vue.component('BootstrapTable', BootstrapTable)
17 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/vo/QuestionOptionVo.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : 问题选项的自定义实体类
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-06-01 09:45
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.vo;
8 |
9 | import com.fasterxml.jackson.annotation.JsonProperty;
10 | import lombok.Data;
11 |
12 | @Data
13 | public class QuestionOptionVo {
14 | @JsonProperty("id")
15 | private String questionOptionId;
16 |
17 | @JsonProperty("content")
18 | private String questionOptionContent;
19 |
20 | @JsonProperty("answer")
21 | private Boolean answer = false;
22 |
23 | @JsonProperty("description")
24 | private String questionOptionDescription;
25 | }
26 |
--------------------------------------------------------------------------------
/frontend/.editorconfig:
--------------------------------------------------------------------------------
1 | [*]
2 | charset=utf-8
3 | end_of_line=lf
4 | insert_final_newline=false
5 | indent_style=space
6 | indent_size=2
7 |
8 | [{*.ng,*.sht,*.html,*.shtm,*.shtml,*.htm}]
9 | indent_style=space
10 | indent_size=2
11 |
12 | [{*.jhm,*.xslt,*.xul,*.rng,*.xsl,*.xsd,*.ant,*.tld,*.fxml,*.jrxml,*.xml,*.jnlp,*.wsdl}]
13 | indent_style=space
14 | indent_size=2
15 |
16 | [{.babelrc,.stylelintrc,jest.config,.eslintrc,.prettierrc,*.json,*.jsb3,*.jsb2,*.bowerrc}]
17 | indent_style=space
18 | indent_size=2
19 |
20 | [*.svg]
21 | indent_style=space
22 | indent_size=2
23 |
24 | [*.js.map]
25 | indent_style=space
26 | indent_size=2
27 |
28 | [*.less]
29 | indent_style=space
30 | indent_size=2
31 |
32 | [*.vue]
33 | indent_style=space
34 | indent_size=2
35 |
36 | [{.analysis_options,*.yml,*.yaml}]
37 | indent_style=space
38 | indent_size=2
39 |
40 |
--------------------------------------------------------------------------------
/frontend/src/components/SettingDrawer/SettingItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ title }}
4 |
5 |
6 |
7 |
8 |
9 |
24 |
25 |
39 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/entity/Role.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : 用户角色表
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019/5/14 07:49
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.entity;
8 |
9 |
10 | import lombok.Data;
11 |
12 | import javax.persistence.Entity;
13 | import javax.persistence.GeneratedValue;
14 | import javax.persistence.Id;
15 |
16 | @Data
17 | @Entity
18 | public class Role {
19 | @Id
20 | @GeneratedValue
21 | private Integer roleId;
22 | private String roleName;
23 | private String roleDescription;
24 | private String roleDetail;
25 | /**
26 | * 角色所能访问的页面的主键集合(用-连接起来字符串)
27 | */
28 | private String rolePageIds;
29 | }
30 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/exception/ExamException.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description :
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-05-17 07:50
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.exception;
8 |
9 | import lsgwr.exam.enums.ResultEnum;
10 | import lombok.Getter;
11 |
12 | @Getter
13 | public class ExamException extends RuntimeException {
14 | private Integer code;
15 |
16 | public ExamException(ResultEnum resultEnum) {
17 | super(resultEnum.getMessage());
18 | this.code = resultEnum.getCode();
19 | }
20 |
21 | public ExamException( Integer code, String message) {
22 | super(message);
23 | this.code = code;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/frontend/src/layouts/RouteView.vue:
--------------------------------------------------------------------------------
1 |
33 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/vo/ExamQuestionTypeVo.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : 按照单选、多选和判断题返回对应的问题列表
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-06-23 11:00
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.vo;
8 |
9 | import com.fasterxml.jackson.annotation.JsonProperty;
10 | import lombok.Data;
11 |
12 | import java.util.List;
13 |
14 | @Data
15 | public class ExamQuestionTypeVo {
16 | @JsonProperty("radios")
17 | private List examQuestionSelectVoRadioList;
18 |
19 | @JsonProperty("checks")
20 | private List examQuestionSelectVoCheckList;
21 |
22 | @JsonProperty("judges")
23 | private List examQuestionSelectVoJudgeList;
24 | }
25 |
--------------------------------------------------------------------------------
/backend/src/main/resources/application.yml:
--------------------------------------------------------------------------------
1 | server:
2 | port: 9527 # 这个端口要和vue项目里vue.config.js里的devServer里面配置的相同
3 | spring:
4 | datasource:
5 | driver-class-name: com.mysql.cj.jdbc.Driver
6 | username: root
7 | password: aA111111
8 | url: jdbc:mysql://localhost:3306/exam?characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
9 | jpa:
10 | # 调试的时候用,用于打印完成SQL语句(但是不打印参数),联合下面的logging.level一同打印最完整的SQL信息(语句+参数)
11 | show-sql: false
12 | hibernate:
13 | ddl-auto: update
14 | servlet:
15 | multipart:
16 | max-file-size: 100MB # 最大支持文件大小
17 | max-request-size: 100MB # 最大支持请求大小
18 |
19 | # 拦截器相关的配置
20 | interceptors:
21 | # 不需要进行鉴权的接口地址,用逗号隔开
22 | auth-ignore-uris: /api/user/register,/api/user/login
23 |
24 | # SQL语句打印(能打印参数,设置为trace是打印完整语句,默认我们就关掉吧)
25 | logging:
26 | level:
27 | org.hibernate.type.descriptor.sql.BasicBinder: off
28 |
--------------------------------------------------------------------------------
/frontend/src/utils/device.js:
--------------------------------------------------------------------------------
1 | import enquireJs from 'enquire.js'
2 |
3 | export const DEVICE_TYPE = {
4 | DESKTOP: 'desktop',
5 | TABLET: 'tablet',
6 | MOBILE: 'mobile'
7 | }
8 |
9 | export const deviceEnquire = function (callback) {
10 | const matchDesktop = {
11 | match: () => {
12 | callback && callback(DEVICE_TYPE.DESKTOP)
13 | }
14 | }
15 |
16 | const matchLablet = {
17 | match: () => {
18 | callback && callback(DEVICE_TYPE.TABLET)
19 | }
20 | }
21 |
22 | const matchMobile = {
23 | match: () => {
24 | callback && callback(DEVICE_TYPE.MOBILE)
25 | }
26 | }
27 |
28 | // screen and (max-width: 1087.99px)
29 | enquireJs
30 | .register('screen and (max-width: 576px)', matchMobile)
31 | .register('screen and (min-width: 576px) and (max-width: 1199px)', matchLablet)
32 | .register('screen and (min-width: 1200px)', matchDesktop)
33 | }
34 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/entity/QuestionLevel.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : 题目难度等级,比如难、中、易等
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019/5/14 07:47
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.entity;
8 |
9 |
10 | import com.fasterxml.jackson.annotation.JsonProperty;
11 | import lombok.Data;
12 |
13 | import javax.persistence.Entity;
14 | import javax.persistence.GeneratedValue;
15 | import javax.persistence.Id;
16 |
17 | @Entity
18 | @Data
19 | public class QuestionLevel {
20 | @Id
21 | @GeneratedValue
22 | @JsonProperty("id")
23 | private Integer questionLevelId;
24 |
25 | @JsonProperty("name")
26 | private String questionLevelName;
27 |
28 | @JsonProperty("description")
29 | private String questionLevelDescription;
30 | }
31 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/entity/QuestionType.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : 题目类型表,从功能角度划分,比如单选、多选、判断等
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019/5/14 07:48
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.entity;
8 |
9 |
10 | import com.fasterxml.jackson.annotation.JsonProperty;
11 | import lombok.Data;
12 |
13 | import javax.persistence.Entity;
14 | import javax.persistence.GeneratedValue;
15 | import javax.persistence.Id;
16 |
17 | @Data
18 | @Entity
19 | public class QuestionType {
20 | @Id
21 | @GeneratedValue
22 | @JsonProperty("id")
23 | private Integer questionTypeId;
24 |
25 | @JsonProperty("name")
26 | private String questionTypeName;
27 |
28 | @JsonProperty("description")
29 | private String questionTypeDescription;
30 | }
31 |
--------------------------------------------------------------------------------
/frontend/docs/webpack-bundle-analyzer.md:
--------------------------------------------------------------------------------
1 | 先增加依赖
2 |
3 | ```bash
4 | // npm
5 | $ npm install --save-dev webpack-bundle-analyzer
6 |
7 | // or yarn
8 | $ yarn add webpack-bundle-analyzer -D
9 | ```
10 |
11 | 配置文件 `vue.config.js` 增加 `configureWebpack.plugins` 参数
12 |
13 | ```
14 | const path = require('path')
15 | const webpack = require('webpack')
16 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
17 |
18 | function resolve (dir) {
19 | return path.join(__dirname, dir)
20 | }
21 |
22 | // vue.config.js
23 | module.exports = {
24 | configureWebpack: {
25 | plugins: [
26 | // Ignore all locale files of moment.js
27 | new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
28 | // 依赖大小分析工具
29 | new BundleAnalyzerPlugin(),
30 | ]
31 | },
32 |
33 |
34 | ...
35 | }
36 | ```
37 |
38 |
39 |
40 | 启动 `cli` 的 `build` 命令进行项目编译,编译完成时,会自动运行一个 http://localhost:8888 的地址,完整显示了支持库依赖
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/vo/ExamCardVo.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : 考试卡片列表
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-06-23 19:30
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.vo;
8 |
9 | import com.fasterxml.jackson.annotation.JsonProperty;
10 | import lombok.Data;
11 |
12 | @Data
13 | public class ExamCardVo {
14 | @JsonProperty("id")
15 | private String examId;
16 | @JsonProperty("title")
17 | private String examName;
18 | @JsonProperty("avatar")
19 | private String examAvatar;
20 | @JsonProperty("content")
21 | private String examDescription;
22 | @JsonProperty("score")
23 | private Integer examScore;
24 | /**
25 | * 考试限制的时间,单位为分钟
26 | */
27 | @JsonProperty("elapse")
28 | private Integer examTimeLimit;
29 | }
30 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/entity/QuestionCategory.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : 题目的类别表,从内容角度划分,比如数学、语文、英语等
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019/5/14 07:46
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.entity;
8 |
9 |
10 | import com.fasterxml.jackson.annotation.JsonProperty;
11 | import lombok.Data;
12 |
13 | import javax.persistence.Entity;
14 | import javax.persistence.GeneratedValue;
15 | import javax.persistence.Id;
16 |
17 | @Data
18 | @Entity
19 | public class QuestionCategory {
20 |
21 | @Id
22 | @GeneratedValue
23 | @JsonProperty("id")
24 | private Integer questionCategoryId;
25 |
26 | @JsonProperty("name")
27 | private String questionCategoryName;
28 |
29 | @JsonProperty("description")
30 | private String questionCategoryDescription;
31 | }
32 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/utils/ResultVOUtil.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : 成功或失败时的消息返回
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-05-17 07:43
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.utils;
8 |
9 | import lsgwr.exam.vo.ResultVO;
10 |
11 | public class ResultVOUtil {
12 |
13 | public static ResultVO success(Integer code, String msg, Object object) {
14 | return new ResultVO(code, msg, object);
15 | }
16 |
17 | public static ResultVO success(Object object) {
18 | return new ResultVO(0, "成功", object);
19 | }
20 |
21 | public static ResultVO success() {
22 | return new ResultVO(0, "成功", null);
23 | }
24 |
25 |
26 | public static ResultVO error(Integer code, String msg) {
27 | return new ResultVO(code, msg, null);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/vo/QuestionSelectionVo.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : 前端创建问题时的下拉列表选择
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-06-03 07:35
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.vo;
8 |
9 | import com.fasterxml.jackson.annotation.JsonProperty;
10 | import lsgwr.exam.entity.QuestionCategory;
11 | import lsgwr.exam.entity.QuestionLevel;
12 | import lsgwr.exam.entity.QuestionType;
13 | import lombok.Data;
14 |
15 | import java.util.List;
16 |
17 | @Data
18 | public class QuestionSelectionVo {
19 | @JsonProperty("types")
20 | private List questionTypeList;
21 |
22 | @JsonProperty("categories")
23 | private List questionCategoryList;
24 |
25 | @JsonProperty("levels")
26 | private List questionLevelList;
27 | }
28 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/repository/UserRepository.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : 用户表的操作类
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-05-14 08:30
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.repository;
8 |
9 | import lsgwr.exam.entity.User;
10 | import org.springframework.data.jpa.repository.JpaRepository;
11 |
12 | public interface UserRepository extends JpaRepository {
13 | /**
14 | * 根据用户名查找到合适的用户
15 | *
16 | * @param username 用户名
17 | * @return 唯一符合的用户(实际用户名字段已经在数据库设置unique了 , 肯定只会返回1条)
18 | */
19 | User findByUserUsername(String username);
20 |
21 | /**
22 | * 根据用户邮箱查找合适用户
23 | *
24 | * @param email 邮箱
25 | * @return 唯一符合的用户(实际邮箱字段已经在数据库设置unique了 , 肯定只会返回1条)
26 | */
27 | User findByUserEmail(String email);
28 | }
29 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/vo/ExamPageVo.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : 考试列表获取
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-06-22 17:00
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.vo;
8 |
9 | import com.fasterxml.jackson.annotation.JsonProperty;
10 | import lombok.Data;
11 |
12 | import java.util.List;
13 |
14 | @Data
15 | public class ExamPageVo {
16 | /**
17 | * 分页时每个分页的大小
18 | */
19 | private Integer pageSize;
20 |
21 | /**
22 | * 当前是在第几页,注意要比前端传过来地小1
23 | */
24 | private Integer pageNo;
25 |
26 | /**
27 | * 一共有多少条符合条件的记录
28 | */
29 | private Long totalCount;
30 |
31 | /**
32 | * 一共有多少页
33 | */
34 | private Integer totalPage;
35 |
36 | /**
37 | * 当前页的详细数据
38 | */
39 | @JsonProperty("data")
40 | private List examVoList;
41 | }
42 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/vo/ResultVO.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : 响应结果的通用类
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-05-17 07:42
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.vo;
8 |
9 | import com.fasterxml.jackson.annotation.JsonInclude;
10 | import lombok.Data;
11 |
12 | @Data
13 | @JsonInclude(JsonInclude.Include.NON_NULL) // 避免返回NULL的字段
14 | public class ResultVO {
15 |
16 |
17 | public ResultVO(Integer code, String msg, T data) {
18 | this.code = code;
19 | this.msg = msg;
20 | this.data = data;
21 | }
22 |
23 | public ResultVO() {
24 | }
25 |
26 | /**
27 | * 错误码
28 | */
29 | private Integer code;
30 |
31 | /**
32 | * 提示信息
33 | */
34 | private String msg = "";
35 |
36 | /**
37 | * 具体内容
38 | */
39 | private T data;
40 | }
41 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/vo/QuestionPageVo.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : 问题列表反馈给前端的对象
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-05-28 22:09
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.vo;
8 |
9 | import com.fasterxml.jackson.annotation.JsonProperty;
10 | import lombok.Data;
11 |
12 | import java.util.List;
13 |
14 | @Data
15 | public class QuestionPageVo {
16 |
17 | /**
18 | * 分页时每个分页的大小
19 | */
20 | private Integer pageSize;
21 |
22 | /**
23 | * 当前是在第几页,注意要比前端传过来地小1
24 | */
25 | private Integer pageNo;
26 |
27 | /**
28 | * 一共有多少条符合条件的记录
29 | */
30 | private Long totalCount;
31 |
32 | /**
33 | * 一共有多少页
34 | */
35 | private Integer totalPage;
36 |
37 | /**
38 | * 当前页的详细数据
39 | */
40 | @JsonProperty("data")
41 | private List questionVoList;
42 | }
43 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/vo/RecordDetailVo.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : 考试记录详情的VO
3 | * @author : 梁山广(Liang Shan Guang)
4 | * @date : 2019/10/27 15:37
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.vo;
8 |
9 | import lsgwr.exam.entity.ExamRecord;
10 | import lombok.Data;
11 |
12 | import java.util.HashMap;
13 | import java.util.List;
14 |
15 | @Data
16 | public class RecordDetailVo {
17 | /**
18 | * 含有考试记录原始信息的对象
19 | */
20 | private ExamRecord examRecord;
21 | /**
22 | * 用户此次考试的作答信息, 键是题目的id,值是选项id的列表
23 | */
24 | private HashMap> answersMap;
25 |
26 | /**
27 | * 用户每题作答结果的Map,键是问题的id,值是用户这题是否回答正确,True or False
28 | */
29 | private HashMap resultsMap;
30 |
31 | /**
32 | * 正确答案,键是题目的id,值是正确答案的id组成的列表
33 | */
34 | private HashMap> answersRightMap;
35 | }
36 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/vo/QuestionDetailVo.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : 问题详情的实体类
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-10-20 09:51
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.vo;
8 |
9 | import lsgwr.exam.entity.QuestionOption;
10 | import lombok.Data;
11 |
12 | import java.util.ArrayList;
13 | import java.util.List;
14 |
15 | @Data
16 | public class QuestionDetailVo {
17 | /**
18 | * 问题的id
19 | */
20 | private String id;
21 |
22 | /**
23 | * 考试题目
24 | */
25 | private String name;
26 |
27 | /**
28 | * 考试描述
29 | */
30 | private String description;
31 | /**
32 | * 问题的类型
33 | */
34 | private String type;
35 | /**
36 | * 问题的选项
37 | */
38 | private List options;
39 | /**
40 | * 问题的答案,选项的id组成的数组
41 | */
42 | private List answers = new ArrayList<>();
43 | }
44 |
--------------------------------------------------------------------------------
/frontend/src/utils/utils.less:
--------------------------------------------------------------------------------
1 | .textOverflow() {
2 | overflow: hidden;
3 | white-space: nowrap;
4 | text-overflow: ellipsis;
5 | word-break: break-all;
6 | }
7 |
8 | .textOverflowMulti(@line: 3, @bg: #fff) {
9 | position: relative;
10 | max-height: @line * 1.5em;
11 | margin-right: -1em;
12 | padding-right: 1em;
13 | overflow: hidden;
14 | line-height: 1.5em;
15 | text-align: justify;
16 | &::before {
17 | position: absolute;
18 | right: 14px;
19 | bottom: 0;
20 | padding: 0 1px;
21 | background: @bg;
22 | content: '...';
23 | }
24 | &::after {
25 | position: absolute;
26 | right: 14px;
27 | width: 1em;
28 | height: 1em;
29 | margin-top: 0.2em;
30 | background: white;
31 | content: '';
32 | }
33 | }
34 |
35 | // mixins for clearfix
36 | // ------------------------
37 | .clearfix() {
38 | zoom: 1;
39 | &::before,
40 | &::after {
41 | display: table;
42 | content: ' ';
43 | }
44 | &::after {
45 | clear: both;
46 | height: 0;
47 | font-size: 0;
48 | visibility: hidden;
49 | }
50 | }
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/vo/UserVo.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : 用户信息的展示类,通过JsonProperty来简化返回
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-05-25 20:32
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.vo;
8 |
9 | import com.fasterxml.jackson.annotation.JsonProperty;
10 | import lombok.Data;
11 |
12 | @Data
13 | public class UserVo {
14 | @JsonProperty("id")
15 | private String userId;
16 |
17 | @JsonProperty("username")
18 | private String userUsername;
19 |
20 | @JsonProperty("nickname")
21 | private String userNickname;
22 |
23 | @JsonProperty("role")
24 | private Integer userRoleId;
25 |
26 | @JsonProperty("avatar")
27 | private String userAvatar;
28 |
29 | @JsonProperty("description")
30 | private String userDescription;
31 |
32 | @JsonProperty("email")
33 | private String userEmail;
34 |
35 | @JsonProperty("phone")
36 | private String userPhone;
37 | }
38 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/config/IntercepterConfig.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : 拦截器配置
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-05-22 08:21
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.config;
8 |
9 | import lsgwr.exam.interceptor.LoginInterceptor;
10 | import org.springframework.beans.factory.annotation.Autowired;
11 | import org.springframework.context.annotation.Configuration;
12 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
13 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
14 |
15 | @Configuration
16 | public class IntercepterConfig implements WebMvcConfigurer {
17 |
18 | @Autowired
19 | private LoginInterceptor loginInterceptor;
20 |
21 | @Override
22 | public void addInterceptors(InterceptorRegistry registry) {
23 | // 拦截user下的api
24 | registry.addInterceptor(loginInterceptor).addPathPatterns("/api/**");
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/doc/deploy/nginx.conf:
--------------------------------------------------------------------------------
1 | events {
2 | worker_connections 1024;
3 | }
4 |
5 |
6 | http {
7 | include mime.types;
8 | default_type application/octet-stream;
9 | sendfile on;
10 |
11 | keepalive_timeout 65;
12 |
13 | server {
14 | listen 80;
15 | server_name localhost;
16 |
17 | # 404页面跳转
18 | location / {
19 | try_files $uri /index.html;
20 | }
21 |
22 | # 静态资源目录,即vue打包后的dist里的静态资源
23 | root /usr/share/nginx/html/;
24 | index index.html index.htm;
25 |
26 | # 后端服务的配置
27 | location /api/ {
28 | proxy_redirect off;
29 | proxy_set_header Host $host;
30 | proxy_set_header X-Real-IP $remote_addr;
31 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
32 | # 后端服务地址
33 | proxy_pass http://localhost:9527/api/;
34 | }
35 |
36 |
37 | error_page 500 502 503 504 /50x.html;
38 | location = /50x.html {
39 | root html;
40 | }
41 | }
42 |
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Liang Shan Guang
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/frontend/src/components/_util/util.js:
--------------------------------------------------------------------------------
1 | /**
2 | * components util
3 | */
4 |
5 | /**
6 | * 清理空值,对象
7 | * @param children
8 | * @returns {*[]}
9 | */
10 | export function filterEmpty (children = []) {
11 | return children.filter(c => c.tag || (c.text && c.text.trim() !== ''))
12 | }
13 |
14 | /**
15 | * 获取字符串长度,英文字符 长度1,中文字符长度2
16 | * @param {*} str
17 | */
18 | export const getStrFullLength = (str = '') =>
19 | str.split('').reduce((pre, cur) => {
20 | const charCode = cur.charCodeAt(0)
21 | if (charCode >= 0 && charCode <= 128) {
22 | return pre + 1
23 | }
24 | return pre + 2
25 | }, 0)
26 |
27 | /**
28 | * 截取字符串,根据 maxLength 截取后返回
29 | * @param {*} str
30 | * @param {*} maxLength
31 | */
32 | export const cutStrByFullLength = (str = '', maxLength) => {
33 | let showLength = 0
34 | return str.split('').reduce((pre, cur) => {
35 | const charCode = cur.charCodeAt(0)
36 | if (charCode >= 0 && charCode <= 128) {
37 | showLength += 1
38 | } else {
39 | showLength += 2
40 | }
41 | if (showLength <= maxLength) {
42 | return pre + cur
43 | }
44 | return pre
45 | }, '')
46 | }
47 |
--------------------------------------------------------------------------------
/frontend/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
25 |
58 |
--------------------------------------------------------------------------------
/frontend/src/views/user/RegisterResult.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 | 查看邮箱
10 | 返回首页
11 |
12 |
13 |
14 |
15 |
16 |
47 |
48 |
51 |
--------------------------------------------------------------------------------
/frontend/src/components/GlobalFooter/GlobalFooter.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
23 |
24 |
51 |
--------------------------------------------------------------------------------
/frontend/src/core/directives/action.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import store from '../../store'
3 |
4 | /**
5 | * Action 权限指令
6 | * 指令用法:
7 | * - 在需要控制 action 级别权限的组件上使用 v-action:[method] , 如下:
8 | * 添加用户
9 | * 删除用户
10 | * 修改
11 | *
12 | * - 当前用户没有权限时,组件上使用了该指令则会被隐藏
13 | * - 当后台权限跟 pro 提供的模式不同时,只需要针对这里的权限过滤进行修改即可
14 | *
15 | * @see https://github.com/sendya/ant-design-pro-vue/pull/53
16 | */
17 | const action = Vue.directive('action', {
18 | inserted: function (el, binding, vnode) {
19 | const actionName = binding.arg
20 | const roles = store.getters.roles
21 | const elVal = vnode.context.$route.meta.permission
22 | const permissionId = elVal instanceof String && [elVal] || elVal
23 | roles.permissions.forEach(p => {
24 | if (!permissionId.includes(p.permissionId)) {
25 | return
26 | }
27 | if (p.actionList && !p.actionList.includes(actionName)) {
28 | el.parentNode && el.parentNode.removeChild(el) || (el.style.display = 'none')
29 | }
30 | })
31 | }
32 | })
33 |
34 | export default action
35 |
--------------------------------------------------------------------------------
/frontend/src/components/TagSelect/TagSelectOption.jsx:
--------------------------------------------------------------------------------
1 | import { Tag } from 'ant-design-vue'
2 | const { CheckableTag } = Tag
3 |
4 | export default {
5 | name: 'TagSelectOption',
6 | props: {
7 | prefixCls: {
8 | type: String,
9 | default: 'ant-pro-tag-select-option'
10 | },
11 | value: {
12 | type: [String, Number, Object],
13 | default: ''
14 | },
15 | checked: {
16 | type: Boolean,
17 | default: false
18 | }
19 | },
20 | data () {
21 | return {
22 | localChecked: this.checked || false
23 | }
24 | },
25 | watch: {
26 | 'checked' (val) {
27 | this.localChecked = val
28 | },
29 | '$parent.items': {
30 | handler: function (val) {
31 | this.value && val.hasOwnProperty(this.value) && (this.localChecked = val[this.value])
32 | },
33 | deep: true
34 | }
35 | },
36 | render () {
37 | const { $slots, value } = this
38 | const onChange = (checked) => {
39 | this.$emit('change', { value, checked })
40 | }
41 | return (
42 | {$slots.default}
43 | )
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/frontend/src/assets/icons/question-admin.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/components/tools/Breadcrumb.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ item.meta.title }}
8 | {{ item.meta.title }}
9 |
10 |
11 |
12 |
13 |
43 |
44 |
46 |
--------------------------------------------------------------------------------
/frontend/src/api/index.js:
--------------------------------------------------------------------------------
1 | const api = {
2 | Login: '/auth/login',
3 | Logout: '/auth/logout',
4 | ForgePassword: '/auth/forge-password',
5 | Register: '/auth/register',
6 | twoStepCode: '/auth/2step-code',
7 | SendSms: '/account/sms',
8 | SendSmsErr: '/account/sms_err',
9 | // get my info
10 | UserInfo: '/user/info',
11 |
12 | // 下面是自己的用户认证的接口
13 | UserRegister: '/user/register',
14 | UserLogin: '/user/login',
15 |
16 | // 考试的接口
17 | ExamQuestionList: '/exam/question/list',
18 | ExamQuestionAll: '/exam/question/all',
19 | ExamQuestionUpdate: '/exam/question/update',
20 | ExamQuestionSelection: '/exam/question/selection',
21 | ExamQuestionCreate: '/exam/question/create',
22 | ExamList: '/exam/list',
23 | ExamAll: '/exam/all',
24 | // 获取问题列表,按照单选、多选和判断进行分类
25 | ExamQuestionTypeList: '/exam/question/type/list',
26 | ExamCreate: '/exam/create',
27 | ExamUpdate: '/exam/update',
28 | ExamCardList: '/exam/card/list',
29 | // 获取考试详情
30 | ExamDetail: '/exam/detail/',
31 | // 获取考试详情
32 | QuestionDetail: '/exam/question/detail/',
33 | // 交卷
34 | FinishExam: '/exam/finish/',
35 | ExamRecordList: '/exam/record/list',
36 | recordDetail: '/exam/record/detail/'
37 | }
38 | export default api
39 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/service/UserService.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : 用户接口
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-05-17 08:02
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.service;
8 |
9 | import lsgwr.exam.dto.RegisterDTO;
10 | import lsgwr.exam.entity.User;
11 | import lsgwr.exam.qo.LoginQo;
12 | import lsgwr.exam.vo.UserInfoVo;
13 | import lsgwr.exam.vo.UserVo;
14 |
15 | public interface UserService {
16 | /**
17 | * 注册
18 | *
19 | * @param registerDTO 注册参数
20 | * @return 注册成功后的用户信息
21 | */
22 | User register(RegisterDTO registerDTO);
23 |
24 | /**
25 | * 登录接口,登录成功返回token
26 | *
27 | * @param loginQo 登录参数
28 | * @return 成功返回token,失败返回null
29 | */
30 | String login(LoginQo loginQo);
31 |
32 | /**
33 | * 根据用户id获取用户信息
34 | *
35 | * @return 用户实体
36 | */
37 | UserVo getUserInfo(String userId);
38 |
39 | /**
40 | * 获取用户详细信息(主要是权限相关的)
41 | * @param userId 用户的id
42 | * @return 用户信息组装的实体
43 | */
44 | UserInfoVo getInfo(String userId);
45 | }
46 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/vo/UserInfoVo.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : 用户信息的前端展示类
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-05-26 12:59
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.vo;
8 |
9 | import com.fasterxml.jackson.annotation.JsonProperty;
10 | import lombok.Data;
11 |
12 | @Data
13 | public class UserInfoVo {
14 |
15 | @JsonProperty("id")
16 | private String userId;
17 |
18 | @JsonProperty("avatar")
19 | private String userAvatar;
20 |
21 | @JsonProperty("name")
22 | private String userNickname;
23 |
24 | @JsonProperty("username")
25 | private String userUsername;
26 |
27 | /**
28 | * 密码不用拷贝,免得泄露信息
29 | */
30 | private String password = "";
31 |
32 | @JsonProperty("email")
33 | private String userEmail;
34 |
35 | @JsonProperty("telephone")
36 | private String userPhone;
37 |
38 | @JsonProperty("roleId")
39 | private String roleName;
40 |
41 | @JsonProperty("welcome")
42 | private String userDescription;
43 |
44 | @JsonProperty("role")
45 | private RoleVo roleVo;
46 | }
47 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/config/CORSConf.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : 允许跨域访问
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-05-17 00:11
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.config;
8 |
9 | import lombok.extern.slf4j.Slf4j;
10 | import org.springframework.context.annotation.Bean;
11 | import org.springframework.context.annotation.Configuration;
12 | import org.springframework.web.servlet.config.annotation.CorsRegistry;
13 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
14 |
15 | @Configuration
16 | @Slf4j
17 | public class CORSConf {
18 |
19 | @Bean
20 | public WebMvcConfigurer corsConfigurer() {
21 | return new WebMvcConfigurer() {
22 | @Override
23 | public void addCorsMappings(CorsRegistry registry) {
24 | log.info("初始化 CORSConfiguration 配置");
25 | registry.addMapping("/**")
26 | .allowedHeaders("*")
27 | .allowedMethods("*")
28 | .allowedOrigins("*");
29 | }
30 | };
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/backend/README.md:
--------------------------------------------------------------------------------
1 | # 在线考试系统的后端实现
2 |
3 | ## 拦截器注入application.yml中的属性
4 |
5 | > 拦截器执行在自动bean初始化之前会导致拦截器中无法注入配置属性,按照下面的步骤即可正常注入
6 |
7 | + 给拦截器加`@Component`注解
8 |
9 | 
10 |
11 | + 在配置类中用`@Autowired`注入
12 |
13 | 
14 |
15 | ## 代码调试的时候如何打印完整的SQL语句和参数
16 |
17 | > 主要是指application.yml的配置
18 |
19 | 参考博客[JPA打印SQL参数](https://mp.weixin.qq.com/s/zyTOdTwFhi2CwxCI9P1kQw)
20 |
21 |
22 | + 打印SQL语句
23 | ```yaml
24 | spring:
25 | jpa:
26 | # 调试的时候用,用于打印完成SQL语句(但是不打印参数),联合下面的logging.level一同打印最完整的SQL信息(语句+参数)
27 | show-sql: true
28 | ```
29 | + 不打印SQL语句
30 | ```yaml
31 | spring:
32 | jpa:
33 | # 调试的时候用,用于打印完成SQL语句(但是不打印参数),联合下面的logging.level一同打印最完整的SQL信息(语句+参数)
34 | show-sql: false
35 | ```
36 | + 打印SQL参数
37 | ```yaml
38 | # SQL语句打印(能打印参数,设置为trace是打印完整语句,默认我们就关掉吧)
39 | logging:
40 | level:
41 | org.hibernate.type.descriptor.sql.BasicBinder: trace
42 | ```
43 | + 不打印SQL参数
44 | ```yaml
45 | # SQL语句打印(能打印参数,设置为trace是打印完整语句,默认我们就关掉吧)
46 | logging:
47 | level:
48 | org.hibernate.type.descriptor.sql.BasicBinder: off
49 | ```
50 |
51 | ## SpringBoot整合vue工程到static目录中遇到的问题
52 | + [Spring Boot整合Vue,解决静态资源映射,页面刷新失效,路径配置等问题](https://blog.csdn.net/godelgnis/article/details/89683760)
--------------------------------------------------------------------------------
/frontend/src/config/defaultSettings.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 项目默认配置项
3 | * primaryColor - 默认主题色
4 | * navTheme - sidebar theme ['dark', 'light'] 两种主题
5 | * colorWeak - 色盲模式
6 | * layout - 整体布局方式 ['sidemenu', 'topmenu'] 两种布局
7 | * fixedHeader - 固定 Header : boolean
8 | * fixSiderbar - 固定左侧菜单栏 : boolean
9 | * autoHideHeader - 向下滚动时,隐藏 Header : boolean
10 | * contentWidth - 内容区布局: 流式 | 固定
11 | *
12 | * storageOptions: {} - Vue-ls 插件配置项 (localStorage/sessionStorage)
13 | *
14 | */
15 |
16 | export default {
17 | primaryColor: '#1890FF', // primary color of ant design
18 | navTheme: 'dark', // theme for nav menu
19 | layout: 'topmenu', // nav menu position: sidemenu or topmenu
20 | contentWidth: 'Fixed', // layout of content: Fluid or Fixed, only works when layout is topmenu
21 | fixedHeader: true, // sticky header
22 | fixSiderbar: false, // sticky siderbar
23 | autoHideHeader: false, // auto hide header
24 | colorWeak: false,
25 | multiTab: false,
26 | production: process.env.NODE_ENV === 'production' && process.env.VUE_APP_PREVIEW !== 'true',
27 | // vue-ls options, localStorage中默认的存储前缀
28 | storageOptions: {
29 | namespace: 'pro__', // key prefix
30 | name: 'ls', // name variable Vue.[ls] or this.[$ls],
31 | storage: 'local' // storage name session, local, memory
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/frontend/src/views/home/Banner.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
Online Exam
8 |
9 | 基于SpringBoot+Vue技术栈开发的在线考试系统
10 |
11 |
19 |
20 |
21 |
22 |
23 |
![]()
24 |
25 |
26 |
27 |
28 |
29 |
30 |
35 |
36 |
39 |
--------------------------------------------------------------------------------
/frontend/src/api/login.js:
--------------------------------------------------------------------------------
1 | import api from './index'
2 | import { axios } from '../utils/request'
3 |
4 | /**
5 | * login func
6 | * parameter: {
7 | * username: '',
8 | * password: '',
9 | * remember_me: true,
10 | * captcha: '12345'
11 | * }
12 | * @param parameter
13 | * @returns {*}
14 | */
15 | export function login (parameter) {
16 | return axios({
17 | // 用户登录接口改成自己的
18 | url: api.UserLogin,
19 | method: 'post',
20 | data: parameter
21 | })
22 | }
23 |
24 | export function getSmsCaptcha (parameter) {
25 | return axios({
26 | url: api.SendSms,
27 | method: 'post',
28 | data: parameter
29 | })
30 | }
31 |
32 | export function getInfo () {
33 | return axios({
34 | url: api.UserInfo,
35 | method: 'get',
36 | headers: {
37 | 'Content-Type': 'application/json;charset=UTF-8'
38 | }
39 | })
40 | }
41 |
42 | export function logout () {
43 | return axios({
44 | url: api.Logout,
45 | method: 'post',
46 | headers: {
47 | 'Content-Type': 'application/json;charset=UTF-8'
48 | }
49 | })
50 | }
51 |
52 | /**
53 | * get user 2step code open?
54 | * @param parameter {*}
55 | */
56 | export function get2step (parameter) {
57 | return axios({
58 | url: api.twoStepCode,
59 | method: 'post',
60 | data: parameter
61 | })
62 | }
63 |
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | # 在线考试系统前端介绍
2 |
3 | ## 开发环境
4 | > 以下为我的本地环境,仅供参考
5 |
6 | - node:10.16.3
7 | - npm:6.9.0
8 | - webstorm:2021.2.3
9 |
10 | ## 参考材料
11 | - 基础框架:[ant-design-vue](https://github.com/vueComponent/ant-design-vue)
12 | - 脚手架参考:[ant-design-vue-pro](https://github.com/vueComponent/ant-design-vue-pro)
13 | - 表格组件:[vxe-table](https://gitee.com/xuliangzhan_admin/vxe-table/)
14 | - 富文本组件:[wangEditor](https://github.com/wangeditor-team/wangEditor)
15 |
16 | ## 项目初始化
17 |
18 | - Clone repo
19 | ```bash
20 | git clone git@github.com:lsgwr/spring-boot-online-exam.git
21 | cd spring-boot-online-exam/frontend/exam
22 | ```
23 |
24 | - Install dependencies
25 | ```
26 | npm install
27 | ```
28 |
29 | - Compiles and hot-reloads for development
30 | ```
31 | npm run serve
32 | ```
33 |
34 | - Compiles and minifies for production
35 | ```
36 | npm run build
37 | ```
38 |
39 | - Lints and fixes files
40 | ```
41 | npm run lint
42 | ```
43 |
44 | ## 关键点
45 | ### 本地调试配置后端转发
46 | > vue.config.js中的proxy
47 | ```js
48 | devServer: {
49 | // development server port 8000
50 | port: 8000,
51 | proxy: 'http://localhost:80' // 这里是后端服务的ip和端口,对应调整即可
52 | },
53 | ```
54 |
55 | ### 配置通用的后端接口前缀
56 | > 在`src/utils/request.js`中配置,比如后端接口一般都以/api开头,那么后端接口就配置成
57 |
58 | ```js
59 | // 创建 axios 实例
60 | const service = axios.create({
61 | baseURL: '/api', // api base_url
62 | timeout: 6000 // 请求超时时间
63 | })
64 | ```
65 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/entity/User.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : 用户表
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019/5/14 07:49
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.entity;
8 |
9 |
10 | import com.fasterxml.jackson.annotation.JsonFormat;
11 | import lombok.Data;
12 | import org.hibernate.annotations.DynamicUpdate;
13 |
14 | import javax.persistence.Entity;
15 | import javax.persistence.Id;
16 | import java.util.Date;
17 |
18 | @Data
19 | @Entity
20 | @DynamicUpdate
21 | public class User {
22 | @Id
23 | private String userId;
24 | private String userUsername;
25 | private String userNickname;
26 | private String userPassword;
27 | private Integer userRoleId;
28 | private String userAvatar;
29 | private String userDescription;
30 | private String userEmail;
31 | private String userPhone;
32 | /**
33 | * 创建时间, 设计表时设置了自动插入当前时间,无需在Java代码中设置了
34 | */
35 |
36 | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
37 | private Date createTime;
38 |
39 | /**
40 | * 更新时间,设计表时设置了自动插入当前时间,无需在Java代码中设置了。
41 | * 同时@DynamicUpdate注解可以时间当数据库数据变化时自动更新,无需人工维护
42 | */
43 | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
44 | private Date updateTime;
45 | }
46 |
--------------------------------------------------------------------------------
/frontend/src/components/tools/HeadInfo.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ title }}
4 |
{{ content }}
5 |
6 |
7 |
8 |
9 |
32 |
33 |
68 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/vo/QuestionCreateSimplifyVo.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : 问题创建的实体类,简化了下
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-06-02 13:26
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.vo;
8 |
9 | import com.fasterxml.jackson.annotation.JsonProperty;
10 | import lombok.Data;
11 |
12 | import java.util.List;
13 |
14 | @Data
15 | public class QuestionCreateSimplifyVo {
16 | /**
17 | * 题目名称
18 | */
19 | @JsonProperty("name")
20 | private String questionName;
21 |
22 | @JsonProperty("desc")
23 | private String questionDescription;
24 |
25 | /**
26 | * 问题的难度的id
27 | */
28 | @JsonProperty("level")
29 | private Integer questionLevelId;
30 |
31 | /**
32 | * 问题的类型(单选、多选、判断等)
33 | */
34 | @JsonProperty("type")
35 | private Integer questionTypeId;
36 |
37 | /**
38 | * 题目的类别表,从内容角度划分,比如数学、语文、英语等
39 | */
40 | @JsonProperty("category")
41 | private Integer questionCategoryId;
42 |
43 | /**
44 | * 创建选项 里添加的内容
45 | */
46 | @JsonProperty("option")
47 | private String option;
48 |
49 | /**
50 | * 问题的选项列表,带上了是否是答案的true和false
51 | */
52 | @JsonProperty("options")
53 | private List questionOptionCreateVoList;
54 | }
55 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/entity/ExamRecord.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : 参加考试的记录,要有考试记录的id、参与者、参与时间、耗时、得分、得分级别(另建表)
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019/5/14 07:43
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.entity;
8 |
9 |
10 | import com.fasterxml.jackson.annotation.JsonFormat;
11 | import lombok.Data;
12 |
13 | import javax.persistence.Entity;
14 | import javax.persistence.Id;
15 | import java.util.Date;
16 |
17 | @Data
18 | @Entity
19 | public class ExamRecord {
20 | /**
21 | * 主键
22 | */
23 | @Id
24 | private String examRecordId;
25 | /**
26 | * 参与的考试的id
27 | */
28 | private String examId;
29 |
30 | /**
31 | * 考生作答地每个题目的选项(题目和题目之间用_分隔,题目有多个选项地话用-分隔),用于查看考试详情
32 | */
33 | private String answerOptionIds;
34 |
35 | /**
36 | * 参与者,即user的id
37 | */
38 | private String examJoinerId;
39 | /**
40 | * 参加考试的日期
41 | */
42 | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
43 | private Date examJoinDate;
44 | /**
45 | * 考试耗时(秒)
46 | */
47 | private Integer examTimeCost;
48 | /**
49 | * 考试得分
50 | */
51 | private Integer examJoinScore;
52 | /**
53 | * 考试得分水平
54 | */
55 | private Integer examResultLevel;
56 | }
57 |
--------------------------------------------------------------------------------
/frontend/src/components/Menu/SideMenu.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
16 |
17 |
18 |
19 |
20 |
62 |
--------------------------------------------------------------------------------
/frontend/src/core/bootstrap.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import store from '../store/'
3 | import {
4 | ACCESS_TOKEN,
5 | DEFAULT_COLOR,
6 | DEFAULT_THEME,
7 | DEFAULT_LAYOUT_MODE,
8 | DEFAULT_COLOR_WEAK,
9 | SIDEBAR_TYPE,
10 | DEFAULT_FIXED_HEADER,
11 | DEFAULT_FIXED_HEADER_HIDDEN,
12 | DEFAULT_FIXED_SIDEMENU,
13 | DEFAULT_CONTENT_WIDTH_TYPE,
14 | DEFAULT_MULTI_TAB
15 | } from '../store/mutation-types'
16 | import config from '../config/defaultSettings'
17 |
18 | export default function Initializer () {
19 | store.commit('SET_SIDEBAR_TYPE', Vue.ls.get(SIDEBAR_TYPE, true))
20 | store.commit('TOGGLE_THEME', Vue.ls.get(DEFAULT_THEME, config.navTheme))
21 | store.commit('TOGGLE_LAYOUT_MODE', Vue.ls.get(DEFAULT_LAYOUT_MODE, config.layout))
22 | store.commit('TOGGLE_FIXED_HEADER', Vue.ls.get(DEFAULT_FIXED_HEADER, config.fixedHeader))
23 | store.commit('TOGGLE_FIXED_SIDERBAR', Vue.ls.get(DEFAULT_FIXED_SIDEMENU, config.fixSiderbar))
24 | store.commit('TOGGLE_CONTENT_WIDTH', Vue.ls.get(DEFAULT_CONTENT_WIDTH_TYPE, config.contentWidth))
25 | store.commit('TOGGLE_FIXED_HEADER_HIDDEN', Vue.ls.get(DEFAULT_FIXED_HEADER_HIDDEN, config.autoHideHeader))
26 | store.commit('TOGGLE_WEAK', Vue.ls.get(DEFAULT_COLOR_WEAK, config.colorWeak))
27 | store.commit('TOGGLE_COLOR', Vue.ls.get(DEFAULT_COLOR, config.primaryColor))
28 | store.commit('TOGGLE_MULTI_TAB', Vue.ls.get(DEFAULT_MULTI_TAB, config.multiTab))
29 | store.commit('SET_TOKEN', Vue.ls.get(ACCESS_TOKEN))
30 |
31 | // last step
32 | }
33 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/entity/Question.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : 考试题目表
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019/5/14 07:46
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.entity;
8 |
9 |
10 | import com.fasterxml.jackson.annotation.JsonFormat;
11 | import lombok.Data;
12 | import org.hibernate.annotations.DynamicUpdate;
13 |
14 | import javax.persistence.Entity;
15 | import javax.persistence.Id;
16 | import java.util.Date;
17 |
18 | @Data
19 | @Entity
20 | @DynamicUpdate
21 | public class Question {
22 | @Id
23 | private String questionId;
24 | private String questionName;
25 | private Integer questionScore;
26 | private String questionCreatorId;
27 | private Integer questionLevelId;
28 | private Integer questionTypeId;
29 | private Integer questionCategoryId;
30 | private String questionDescription;
31 | private String questionOptionIds;
32 | private String questionAnswerOptionIds;
33 | /**
34 | * 创建时间, 设计表时设置了自动插入当前时间,无需在Java代码中设置了
35 | */
36 |
37 | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
38 | private Date createTime;
39 |
40 | /**
41 | * 更新时间,设计表时设置了自动插入当前时间,无需在Java代码中设置了。
42 | * 同时@DynamicUpdate注解可以时间当数据库数据变化时自动更新,无需人工维护
43 | */
44 | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
45 | private Date updateTime;
46 | }
47 |
--------------------------------------------------------------------------------
/frontend/src/assets/icons/exam-list.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/vo/ExamCreateVo.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : 考试的前端展示类。examCreatorId可从token中获取
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-06-17 08:14
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.vo;
8 |
9 | import com.fasterxml.jackson.annotation.JsonProperty;
10 | import lombok.Data;
11 |
12 | import java.util.List;
13 |
14 | @Data
15 | public class ExamCreateVo {
16 |
17 | @JsonProperty("name")
18 | private String examName;
19 |
20 | @JsonProperty("avatar")
21 | private String examAvatar;
22 |
23 | @JsonProperty("desc")
24 | private String examDescription;
25 |
26 | /**
27 | * 考试时长,单位分钟
28 | */
29 | @JsonProperty("elapse")
30 | private Integer examTimeLimit;
31 |
32 |
33 | /**
34 | * 单选题
35 | */
36 | private List radios;
37 |
38 | /**
39 | * 多选题
40 | */
41 | private List checks;
42 |
43 | /**
44 | * 判断题
45 | */
46 | private List judges;
47 |
48 | /**
49 | * 单选题的分数
50 | */
51 | @JsonProperty("radioScore")
52 | private Integer examScoreRadio;
53 |
54 | /**
55 | * 多选题的分数
56 | */
57 | @JsonProperty("checkScore")
58 | private Integer examScoreCheck;
59 |
60 | /**
61 | * 判断题每题的分数
62 | */
63 | @JsonProperty("judgeScore")
64 | private Integer examScoreJudge;
65 | }
66 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/vo/QuestionCreateVo.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : 问题创建的实体类
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-06-02 13:26
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.vo;
8 |
9 | import com.fasterxml.jackson.annotation.JsonProperty;
10 | import lombok.Data;
11 |
12 | import java.util.List;
13 |
14 | @Data
15 | public class QuestionCreateVo {
16 | /**
17 | * 题目名称
18 | */
19 | @JsonProperty("name")
20 | private String questionName;
21 |
22 | @JsonProperty("desc")
23 | private String questionDescription;
24 |
25 | /**
26 | * 题目的分数,默认值是5
27 | */
28 | @JsonProperty("score")
29 | private Integer questionScore = 5;
30 |
31 | /**
32 | * 题目的创建者的id,从token中解析得到
33 | */
34 | @JsonProperty("creator")
35 | private String questionCreatorId;
36 |
37 | /**
38 | * 问题的难度的id
39 | */
40 | @JsonProperty("level")
41 | private Integer questionLevelId;
42 |
43 | /**
44 | * 问题的类型(单选、多选、判断等)
45 | */
46 | @JsonProperty("type")
47 | private Integer questionTypeId;
48 |
49 | /**
50 | * 题目的类别表,从内容角度划分,比如数学、语文、英语等
51 | */
52 | @JsonProperty("category")
53 | private Integer questionCategoryId;
54 |
55 | /**
56 | * 问题的选项列表,带上了是否是答案的true和false
57 | */
58 | @JsonProperty("options")
59 | private List questionOptionCreateVoList;
60 | }
61 |
--------------------------------------------------------------------------------
/frontend/src/utils/helper/permission.js:
--------------------------------------------------------------------------------
1 | const PERMISSION_ENUM = {
2 | 'add': { key: 'add', label: '新增' },
3 | 'delete': { key: 'delete', label: '删除' },
4 | 'edit': { key: 'edit', label: '修改' },
5 | 'query': { key: 'query', label: '查询' },
6 | 'get': { key: 'get', label: '详情' },
7 | 'enable': { key: 'enable', label: '启用' },
8 | 'disable': { key: 'disable', label: '禁用' },
9 | 'import': { key: 'import', label: '导入' },
10 | 'export': { key: 'export', label: '导出' }
11 | }
12 |
13 | function plugin (Vue) {
14 | if (plugin.installed) {
15 | return
16 | }
17 |
18 | !Vue.prototype.$auth && Object.defineProperties(Vue.prototype, {
19 | $auth: {
20 | get () {
21 | const _this = this
22 | return (permissions) => {
23 | const [permission, action] = permissions.split('.')
24 | const permissionList = _this.$store.getters.roles.permissions
25 | return permissionList.find((val) => {
26 | return val.permissionId === permission
27 | }).actionList.findIndex((val) => {
28 | return val === action
29 | }) > -1
30 | }
31 | }
32 | }
33 | })
34 |
35 | !Vue.prototype.$enum && Object.defineProperties(Vue.prototype, {
36 | $enum: {
37 | get () {
38 | // const _this = this;
39 | return (val) => {
40 | let result = PERMISSION_ENUM
41 | val && val.split('.').forEach(v => {
42 | result = result && result[v] || null
43 | })
44 | return result
45 | }
46 | }
47 | }
48 | })
49 | }
50 |
51 | export default plugin
52 |
--------------------------------------------------------------------------------
/frontend/public/loading/loading.css:
--------------------------------------------------------------------------------
1 | #preloadingAnimation{position:fixed;left:0;top:0;height:100%;width:100%;background:#ffffff;user-select:none;z-index: 9999;overflow: hidden}.lds-roller{display:inline-block;position:relative;left:50%;top:50%;transform:translate(-50%,-50%);width:64px;height:64px;}.lds-roller div{animation:lds-roller 1.2s cubic-bezier(0.5,0,0.5,1) infinite;transform-origin:32px 32px;}.lds-roller div:after{content:" ";display:block;position:absolute;width:6px;height:6px;border-radius:50%;background:#13c2c2;margin:-3px 0 0 -3px;}.lds-roller div:nth-child(1){animation-delay:-0.036s;}.lds-roller div:nth-child(1):after{top:50px;left:50px;}.lds-roller div:nth-child(2){animation-delay:-0.072s;}.lds-roller div:nth-child(2):after{top:54px;left:45px;}.lds-roller div:nth-child(3){animation-delay:-0.108s;}.lds-roller div:nth-child(3):after{top:57px;left:39px;}.lds-roller div:nth-child(4){animation-delay:-0.144s;}.lds-roller div:nth-child(4):after{top:58px;left:32px;}.lds-roller div:nth-child(5){animation-delay:-0.18s;}.lds-roller div:nth-child(5):after{top:57px;left:25px;}.lds-roller div:nth-child(6){animation-delay:-0.216s;}.lds-roller div:nth-child(6):after{top:54px;left:19px;}.lds-roller div:nth-child(7){animation-delay:-0.252s;}.lds-roller div:nth-child(7):after{top:50px;left:14px;}.lds-roller div:nth-child(8){animation-delay:-0.288s;}.lds-roller div:nth-child(8):after{top:45px;left:10px;}#preloadingAnimation .load-tips{color: #13c2c2;font-size:2rem;position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);margin-top:80px;text-align:center;width:400px;height:64px;} @keyframes lds-roller{0%{transform:rotate(0deg);} 100%{transform:rotate(360deg);}}
--------------------------------------------------------------------------------
/frontend/src/views/list/modules/QuestionViewModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 题干:
6 |
7 |
8 | 选项:
9 |
12 |
13 | 答案:
14 |
17 |
18 | 解析:
19 |
20 |
21 |
22 |
23 | 关闭
24 |
25 |
26 |
27 |
28 |
59 |
--------------------------------------------------------------------------------
/frontend/src/utils/storage.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Set storage
3 | *
4 | * @param name
5 | * @param content
6 | * @param maxAge
7 | */
8 | export const setStore = (name, content, maxAge = null) => {
9 | if (!global.window || !name) {
10 | return
11 | }
12 |
13 | if (typeof content !== 'string') {
14 | content = JSON.stringify(content)
15 | }
16 |
17 | const storage = global.window.localStorage
18 |
19 | storage.setItem(name, content)
20 | if (maxAge && !isNaN(parseInt(maxAge))) {
21 | const timeout = parseInt(new Date().getTime() / 1000)
22 | storage.setItem(`${name}_expire`, timeout + maxAge)
23 | }
24 | }
25 |
26 | /**
27 | * Get storage
28 | *
29 | * @param name
30 | * @returns {*}
31 | */
32 | export const getStore = name => {
33 | if (!global.window || !name) {
34 | return
35 | }
36 |
37 | const content = window.localStorage.getItem(name)
38 | const _expire = window.localStorage.getItem(`${name}_expire`)
39 |
40 | if (_expire) {
41 | const now = parseInt(new Date().getTime() / 1000)
42 | if (now > _expire) {
43 | return
44 | }
45 | }
46 |
47 | try {
48 | return JSON.parse(content)
49 | } catch (e) {
50 | return content
51 | }
52 | }
53 |
54 | /**
55 | * Clear storage
56 | *
57 | * @param name
58 | */
59 | export const clearStore = name => {
60 | if (!global.window || !name) {
61 | return
62 | }
63 |
64 | window.localStorage.removeItem(name)
65 | window.localStorage.removeItem(`${name}_expire`)
66 | }
67 |
68 | /**
69 | * Clear all storage
70 | */
71 | export const clearAll = () => {
72 | if (!global.window || !name) {
73 | return
74 | }
75 |
76 | window.localStorage.clear()
77 | }
78 |
--------------------------------------------------------------------------------
/frontend/src/views/test/SummerNoteDemo.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
59 |
--------------------------------------------------------------------------------
/frontend/src/utils/util.js:
--------------------------------------------------------------------------------
1 | export function timeFix () {
2 | const time = new Date()
3 | const hour = time.getHours()
4 | return hour < 9 ? '早上好' : hour <= 11 ? '上午好' : hour <= 13 ? '中午好' : hour < 20 ? '下午好' : '晚上好'
5 | }
6 |
7 | export function welcome () {
8 | const arr = ['休息一会儿吧', '准备吃什么呢?', '要不要打一把 DOTA', '我猜你可能累了']
9 | const index = Math.floor(Math.random() * arr.length)
10 | return arr[index]
11 | }
12 |
13 | /**
14 | * 触发 window.resize
15 | */
16 | export function triggerWindowResizeEvent () {
17 | const event = document.createEvent('HTMLEvents')
18 | event.initEvent('resize', true, true)
19 | event.eventType = 'message'
20 | window.dispatchEvent(event)
21 | }
22 |
23 | export function handleScrollHeader (callback) {
24 | let timer = 0
25 |
26 | let beforeScrollTop = window.pageYOffset
27 | callback = callback || function () {}
28 | window.addEventListener(
29 | 'scroll',
30 | event => {
31 | clearTimeout(timer)
32 | timer = setTimeout(() => {
33 | let direction = 'up'
34 | const afterScrollTop = window.pageYOffset
35 | const delta = afterScrollTop - beforeScrollTop
36 | if (delta === 0) {
37 | return false
38 | }
39 | direction = delta > 0 ? 'down' : 'up'
40 | callback(direction)
41 | beforeScrollTop = afterScrollTop
42 | }, 50)
43 | },
44 | false
45 | )
46 | }
47 |
48 | /**
49 | * Remove loading animate
50 | * @param id parent element id or class
51 | * @param timeout
52 | */
53 | export function removeLoadingAnimate (id = '', timeout = 1500) {
54 | if (id === '') {
55 | return
56 | }
57 | setTimeout(() => {
58 | document.body.removeChild(document.getElementById(id))
59 | }, timeout)
60 | }
61 |
--------------------------------------------------------------------------------
/frontend/src/views/list/modules/CreateForm.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
68 |
--------------------------------------------------------------------------------
/frontend/src/components/tools/UserMenu.vue:
--------------------------------------------------------------------------------
1 |
2 |
27 |
28 |
29 |
60 |
--------------------------------------------------------------------------------
/frontend/src/assets/icons/bx-analyse.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/utils/request.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import axios from 'axios'
3 | import store from '../store'
4 | import {
5 | VueAxios
6 | } from './axios'
7 | import notification from 'ant-design-vue/es/notification'
8 | import {
9 | ACCESS_TOKEN
10 | } from '../store/mutation-types'
11 |
12 | // 创建 axios 实例
13 | const service = axios.create({
14 | baseURL: '/api', // api base_url
15 | timeout: 6000 // 请求超时时间
16 | })
17 |
18 | const err = (error) => {
19 | if (error.response) {
20 | const data = error.response.data
21 | const token = Vue.ls.get(ACCESS_TOKEN)
22 | if (error.response.status === 403) {
23 | notification.error({
24 | message: 'Forbidden',
25 | description: data.message
26 | })
27 | }
28 | if (error.response.status === 401 && !(data.result && data.result.isLogin)) {
29 | notification.error({
30 | message: 'Unauthorized',
31 | description: 'Authorization verification failed'
32 | })
33 | if (token) {
34 | store.dispatch('Logout').then(() => {
35 | setTimeout(() => {
36 | window.location.reload()
37 | }, 1500)
38 | })
39 | }
40 | }
41 | }
42 | return Promise.reject(error)
43 | }
44 |
45 | // request interceptor
46 | service.interceptors.request.use(config => {
47 | const token = Vue.ls.get(ACCESS_TOKEN)
48 | if (token) { // 如果localStorage中有"Access-Token"属性,就在请求头里加上
49 | config.headers['Access-Token'] = token // 让每个请求携带自定义 token 请根据实际情况自行修改
50 | }
51 | return config
52 | }, err)
53 |
54 | // response interceptor
55 | service.interceptors.response.use((response) => {
56 | return response.data
57 | }, err)
58 |
59 | const installer = {
60 | vm: {},
61 | install (Vue) {
62 | Vue.use(VueAxios, service)
63 | }
64 | }
65 |
66 | export {
67 | installer as VueAxios,
68 | service as axios
69 | }
70 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/vo/JsonData.java:
--------------------------------------------------------------------------------
1 | package lsgwr.exam.vo;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.Data;
5 | import lombok.NoArgsConstructor;
6 |
7 | import java.io.Serializable;
8 |
9 | /**
10 | * 功能描述:工具类
11 | *
12 | * @author liangshanguang
13 | * @date 2019-05-22 08:11
14 | */
15 | @Data
16 | @AllArgsConstructor
17 | @NoArgsConstructor
18 | public class JsonData implements Serializable {
19 |
20 | private static final long serialVersionUID = 1L;
21 |
22 | /**
23 | * 状态码 0 表示成功,1表示处理中,-1表示失败
24 | */
25 | private Integer code;
26 | /**
27 | * 数据
28 | */
29 | private Object data;
30 | /**
31 | * 描述
32 | */
33 | private String msg;
34 |
35 | /**
36 | * 成功,传入数据
37 | */
38 | public static JsonData buildSuccess() {
39 | return new JsonData(0, null, null);
40 | }
41 |
42 | /**
43 | * 成功,传入数据
44 | */
45 | public static JsonData buildSuccess(Object data) {
46 | return new JsonData(0, data, null);
47 | }
48 |
49 | /**
50 | * 失败,传入描述信息
51 | */
52 | public static JsonData buildError(String msg) {
53 | return new JsonData(-1, null, msg);
54 | }
55 |
56 | /**
57 | * 失败,传入描述信息,状态码
58 | */
59 | public static JsonData buildError(String msg, Integer code) {
60 | return new JsonData(code, null, msg);
61 | }
62 |
63 | /**
64 | * 成功,传入数据,及描述信息
65 | *
66 | * @param data 自定义的数据
67 | * @param msg 返回给前端的消息
68 | * @return 给前端的消息体
69 | */
70 | public static JsonData buildSuccess(Object data, String msg) {
71 | return new JsonData(0, data, msg);
72 | }
73 |
74 | /**
75 | * 成功,传入数据,及状态码
76 | */
77 | public static JsonData buildSuccess(Object data, int code) {
78 | return new JsonData(code, data, null);
79 | }
80 |
81 | }
82 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/entity/Exam.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : 考试表,要有题目、总分数、时间限制、有效日期、创建者等字段
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019/5/14 07:42
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 |
8 | package lsgwr.exam.entity;
9 |
10 |
11 | import com.fasterxml.jackson.annotation.JsonFormat;
12 | import lombok.Data;
13 | import org.hibernate.annotations.DynamicUpdate;
14 |
15 | import javax.persistence.Entity;
16 | import javax.persistence.Id;
17 | import java.util.Date;
18 |
19 | @Entity
20 | @Data
21 | @DynamicUpdate
22 | public class Exam {
23 | @Id
24 | private String examId;
25 | private String examName;
26 | private String examAvatar;
27 | private String examDescription;
28 | private String examQuestionIds;
29 | private String examQuestionIdsRadio;
30 | private String examQuestionIdsCheck;
31 | private String examQuestionIdsJudge;
32 | private Integer examScore;
33 | private Integer examScoreRadio;
34 | private Integer examScoreCheck;
35 | private Integer examScoreJudge;
36 | private String examCreatorId;
37 | private Integer examTimeLimit;
38 | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
39 | private Date examStartDate;
40 | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
41 | private Date examEndDate;
42 | /**
43 | * 创建时间, 设计表时设置了自动插入当前时间,无需在Java代码中设置了
44 | */
45 |
46 | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
47 | private Date createTime;
48 |
49 | /**
50 | * 更新时间,设计表时设置了自动插入当前时间,无需在Java代码中设置了。
51 | * 同时@DynamicUpdate注解可以时间当数据库数据变化时自动更新,无需人工维护
52 | */
53 | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
54 | private Date updateTime;
55 | }
56 |
--------------------------------------------------------------------------------
/frontend/src/core/lazy_lib/components_use.js:
--------------------------------------------------------------------------------
1 |
2 | /* eslint-disable */
3 | /**
4 | * 该文件是为了按需加载,剔除掉了一些不需要的框架组件。
5 | * 减少了编译支持库包大小
6 | *
7 | * 当需要更多组件依赖时,在该文件加入即可
8 | */
9 | import Vue from 'vue'
10 | import {
11 | LocaleProvider,
12 | Layout,
13 | Input,
14 | InputNumber,
15 | Button,
16 | Switch,
17 | Radio,
18 | Checkbox,
19 | Select,
20 | Card,
21 | Form,
22 | Row,
23 | Col,
24 | Modal,
25 | Table,
26 | Tabs,
27 | Icon,
28 | Badge,
29 | Popover,
30 | Dropdown,
31 | List,
32 | Avatar,
33 | Breadcrumb,
34 | Steps,
35 | Spin,
36 | Menu,
37 | Drawer,
38 | Tooltip,
39 | Alert,
40 | Tag,
41 | Divider,
42 | DatePicker,
43 | TimePicker,
44 | Upload,
45 | Progress,
46 | Skeleton,
47 | Popconfirm,
48 | message,
49 | notification
50 | } from 'ant-design-vue'
51 | // import VueCropper from 'vue-cropper'
52 |
53 | Vue.use(LocaleProvider)
54 | Vue.use(Layout)
55 | Vue.use(Input)
56 | Vue.use(InputNumber)
57 | Vue.use(Button)
58 | Vue.use(Switch)
59 | Vue.use(Radio)
60 | Vue.use(Checkbox)
61 | Vue.use(Select)
62 | Vue.use(Card)
63 | Vue.use(Form)
64 | Vue.use(Row)
65 | Vue.use(Col)
66 | Vue.use(Modal)
67 | Vue.use(Table)
68 | Vue.use(Tabs)
69 | Vue.use(Icon)
70 | Vue.use(Badge)
71 | Vue.use(Popover)
72 | Vue.use(Dropdown)
73 | Vue.use(List)
74 | Vue.use(Avatar)
75 | Vue.use(Breadcrumb)
76 | Vue.use(Steps)
77 | Vue.use(Spin)
78 | Vue.use(Menu)
79 | Vue.use(Drawer)
80 | Vue.use(Tooltip)
81 | Vue.use(Alert)
82 | Vue.use(Tag)
83 | Vue.use(Divider)
84 | Vue.use(DatePicker)
85 | Vue.use(TimePicker)
86 | Vue.use(Upload)
87 | Vue.use(Progress)
88 | Vue.use(Skeleton)
89 | Vue.use(Popconfirm)
90 | // Vue.use(VueCropper)
91 | Vue.use(notification)
92 |
93 | Vue.prototype.$confirm = Modal.confirm
94 | Vue.prototype.$message = message
95 | Vue.prototype.$notification = notification
96 | Vue.prototype.$info = Modal.info
97 | Vue.prototype.$success = Modal.success
98 | Vue.prototype.$error = Modal.error
99 | Vue.prototype.$warning = Modal.warning
--------------------------------------------------------------------------------
/frontend/src/store/modules/permission.js:
--------------------------------------------------------------------------------
1 | import { asyncRouterMap, constantRouterMap } from '../../config/router.config'
2 |
3 | /**
4 | * 过滤账户是否拥有某一个权限,并将菜单从加载列表移除
5 | *
6 | * @param permission
7 | * @param route
8 | * @returns {boolean}
9 | */
10 | function hasPermission (permission, route) {
11 | if (route.meta && route.meta.permission) {
12 | let flag = false
13 | for (let i = 0, len = permission.length; i < len; i++) {
14 | flag = route.meta.permission.includes(permission[i])
15 | if (flag) {
16 | return true
17 | }
18 | }
19 | return false
20 | }
21 | return true
22 | }
23 |
24 | /**
25 | * 单账户多角色时,使用该方法可过滤角色不存在的菜单
26 | *
27 | * @param roles
28 | * @param route
29 | * @returns {*}
30 | */
31 | // eslint-disable-next-line
32 | function hasRole(roles, route) {
33 | if (route.meta && route.meta.roles) {
34 | return route.meta.roles.includes(roles.id)
35 | } else {
36 | return true
37 | }
38 | }
39 |
40 | function filterAsyncRouter (routerMap, roles) {
41 | const accessedRouters = routerMap.filter(route => {
42 | if (hasPermission(roles.permissionList, route)) {
43 | if (route.children && route.children.length) {
44 | route.children = filterAsyncRouter(route.children, roles)
45 | }
46 | return true
47 | }
48 | return false
49 | })
50 | return accessedRouters
51 | }
52 |
53 | const permission = {
54 | state: {
55 | routers: constantRouterMap,
56 | addRouters: []
57 | },
58 | mutations: {
59 | SET_ROUTERS: (state, routers) => {
60 | state.addRouters = routers
61 | state.routers = constantRouterMap.concat(routers)
62 | }
63 | },
64 | actions: {
65 | GenerateRoutes ({ commit }, data) {
66 | return new Promise(resolve => {
67 | const { roles } = data
68 | const accessedRouters = filterAsyncRouter(asyncRouterMap, roles)
69 | commit('SET_ROUTERS', accessedRouters)
70 | resolve()
71 | })
72 | }
73 | }
74 | }
75 |
76 | export default permission
77 |
--------------------------------------------------------------------------------
/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | 在线考试系统
9 |
10 |
11 |
12 |
15 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/frontend/src/views/account/settings/Custom.vue:
--------------------------------------------------------------------------------
1 |
72 |
73 |
76 |
--------------------------------------------------------------------------------
/frontend/public/loading/option2/loading.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/utils/JwtUtils.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : JWT工具类:JWT生产token和校验token的方法
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-05-21 08:15
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.utils;
8 |
9 | import lsgwr.exam.entity.User;
10 | import io.jsonwebtoken.Claims;
11 | import io.jsonwebtoken.Jwts;
12 | import io.jsonwebtoken.SignatureAlgorithm;
13 |
14 | import java.util.Date;
15 |
16 | public class JwtUtils {
17 | /**
18 | * 构建token的主题
19 | */
20 | private static final String SUBJECT = "lsg_exam";
21 | /**
22 | * 过期时间为1天
23 | */
24 | private static final long EXPIRE = 1000 * 60 * 60 * 24;
25 |
26 | private static final String APP_SECRET = "liangshanguang";
27 |
28 | public static String genJsonWebToken(User user) {
29 | if (user == null || user.getUserId() == null || user.getUserUsername() == null || user.getUserAvatar() == null) {
30 | return null;
31 | }
32 | return Jwts.builder().setSubject(SUBJECT)
33 | // 下面3行设置token中间字段,携带用户的信息
34 | .claim("id", user.getUserId())
35 | .claim("username", user.getUserUsername())
36 | .claim("avatar", user.getUserAvatar())
37 | .setIssuedAt(new Date())
38 | // 设置过期时间
39 | .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
40 | .signWith(SignatureAlgorithm.HS256, APP_SECRET)
41 | // 生成的结果字符串太长,这里压缩下
42 | .compact();
43 | }
44 |
45 | /**
46 | * 校验token
47 | *
48 | * @param token 生成的额token
49 | * @return 解析出的信息
50 | */
51 | public static Claims checkJWT(String token) {
52 | try {
53 | return Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(token).getBody();
54 | } catch (Exception e) {
55 | // 篡改token会导致校验失败,走到异常分支,这里返回null
56 | return null;
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/frontend/public/loading/option2/html_code_segment.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/vo/QuestionVo.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : 考试问题的对外封装类
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-05-28 08:17
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.vo;
8 |
9 | import com.fasterxml.jackson.annotation.JsonFormat;
10 | import com.fasterxml.jackson.annotation.JsonProperty;
11 | import lombok.Data;
12 |
13 | import java.util.Date;
14 | import java.util.List;
15 |
16 | @Data
17 | public class QuestionVo {
18 | @JsonProperty("id")
19 | private String questionId;
20 |
21 | @JsonProperty("name")
22 | private String questionName;
23 |
24 | @JsonProperty("score")
25 | private Integer questionScore;
26 |
27 | /**
28 | * 根据questionCreatorId查询创建人
29 | */
30 | @JsonProperty("creator")
31 | private String questionCreator;
32 |
33 | /**
34 | * 根据questionLevelId查询问题难度
35 | */
36 | @JsonProperty("level")
37 | private String questionLevel;
38 |
39 | /**
40 | * 问题难度级别在数据库表question_level中的id
41 | */
42 | @JsonProperty("levelId")
43 | private int questionLevelId;
44 |
45 | /**
46 | * 问题类型,根据questionTypeId获取
47 | */
48 | @JsonProperty("type")
49 | private String questionType;
50 |
51 | /**
52 | * 问题类型在数据库表question_type中的id
53 | */
54 | @JsonProperty("typeId")
55 | private int questionTypeId;
56 |
57 | /**
58 | * 问题分类,根据questionCategoryId获得
59 | */
60 | @JsonProperty("category")
61 | private String questionCategory;
62 |
63 | /**
64 | * 问题分类在数据库表question_category中的id
65 | */
66 | @JsonProperty("categoryId")
67 | private int questionCategoryId;
68 |
69 |
70 | @JsonProperty("description")
71 | private String questionDescription;
72 |
73 | /**
74 | * 问题选项列表,从questionOptionIds获得,需要自己额外给isAnswer赋值
75 | */
76 | @JsonProperty("options")
77 | private List questionOptionVoList;
78 |
79 |
80 | /**
81 | * 更新时间,设计表时设置了自动插入当前时间,无需在Java代码中设置了。
82 | * 同时@DynamicUpdate注解可以时间当数据库数据变化时自动更新,无需人工维护
83 | */
84 | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
85 | private Date updateTime;
86 | }
87 |
--------------------------------------------------------------------------------
/frontend/docs/load-on-demand.md:
--------------------------------------------------------------------------------
1 | 按需加载 减小打包
2 | ====
3 |
4 |
5 |
6 | ## 按需引入组件依赖
7 |
8 | `Ant Design Pro Vue` 默认编码工作并不支持按需引入,不过可以通过以下操作结合 [Ant Design Of Vue](https://vuecomponent.github.io/ant-design-vue/docs/vue/introduce-cn/) 官方文档来进行按需引入。
9 |
10 | - 增加项目按需引入依赖
11 | - 修改引入组件方式
12 |
13 |
14 |
15 | 1. 增加按需引入所需依赖 `babel-plugin-import`
16 | 并且修改文件 `babel.config.js`
17 | ```ecmascript 6
18 | module.exports = {
19 | presets: [
20 | '@vue/app'
21 | ],
22 | plugins: [
23 | [ "import", {
24 | "libraryName": "ant-design-vue",
25 | "libraryDirectory": "es",
26 | "style": "css"
27 | } ]
28 | ]
29 | }
30 | ```
31 |
32 |
33 | 2. 修改引入组件方式 (注意,这只是一个例子,请完整引入你所需要的组件)
34 |
35 | 文件 `../components/use.js`
36 |
37 | ```javascript
38 | import Vue from 'vue'
39 | import {
40 | Input,
41 | Button,
42 | Select,
43 | Card,
44 | Form,
45 | Row,
46 | Col,
47 | Modal,
48 | Table,
49 | notification
50 | } from 'ant-design-vue'
51 |
52 | Vue.use(Input)
53 | Vue.use(Button)
54 | Vue.use(Select)
55 | Vue.use(Card)
56 | Vue.use(Form)
57 | Vue.use(Row)
58 | Vue.use(Col)
59 | Vue.use(Modal)
60 | Vue.use(Table)
61 | Vue.use(notification)
62 |
63 | Vue.prototype.$notification = notification;
64 | ```
65 |
66 |
67 | 3. 最后在 `main.js` 中引入 `../components/use.js` 文件即可,如下
68 |
69 | ```javascript
70 |
71 | import Vue from 'vue'
72 | import App from './App'
73 |
74 | // 引入 按需组件的统一引入文件
75 | import './components/use'
76 |
77 | import './style/index.less'
78 |
79 |
80 | Vue.config.productionTip = false
81 |
82 | new Vue({
83 | render: h => h(App),
84 | }).$mount('#app')
85 |
86 | ```
87 |
88 | **具体完整实现可参考分支 [feature/demand_load](https://github.com/sendya/ant-design-pro-vue/tree/feature/demand_load)**
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 | ## 其他 减少打包大小
97 |
98 |
99 |
100 | 1. Ant Design Vue 1.2.x 版本起,采用的 ant-design 官方方案 svg Icon 组件,整个项目打包会变大很多,图标进行按需加载可参考 https://github.com/HeskeyBaozi/reduce-antd-icons-bundle-demo
101 | 2. moment 按需加载 可参考 https://github.com/jmblog/how-to-optimize-momentjs-with-webpack
--------------------------------------------------------------------------------
/frontend/src/utils/mixin.js:
--------------------------------------------------------------------------------
1 | // import Vue from 'vue'
2 | import { deviceEnquire, DEVICE_TYPE } from '../utils/device'
3 | import { mapState } from 'vuex'
4 |
5 | // const mixinsComputed = Vue.config.optionMergeStrategies.computed
6 | // const mixinsMethods = Vue.config.optionMergeStrategies.methods
7 |
8 | const mixin = {
9 | computed: {
10 | ...mapState({
11 | layoutMode: state => state.app.layout,
12 | navTheme: state => state.app.theme,
13 | primaryColor: state => state.app.color,
14 | colorWeak: state => state.app.weak,
15 | fixedHeader: state => state.app.fixedHeader,
16 | fixSiderbar: state => state.app.fixSiderbar,
17 | fixSidebar: state => state.app.fixSiderbar,
18 | contentWidth: state => state.app.contentWidth,
19 | autoHideHeader: state => state.app.autoHideHeader,
20 | sidebarOpened: state => state.app.sidebar,
21 | multiTab: state => state.app.multiTab
22 | })
23 | },
24 | methods: {
25 | isTopMenu () {
26 | return this.layoutMode === 'topmenu'
27 | },
28 | isSideMenu () {
29 | return !this.isTopMenu()
30 | }
31 | }
32 | }
33 |
34 | const mixinDevice = {
35 | computed: {
36 | ...mapState({
37 | device: state => state.app.device
38 | })
39 | },
40 | methods: {
41 | isMobile () {
42 | return this.device === DEVICE_TYPE.MOBILE
43 | },
44 | isDesktop () {
45 | return this.device === DEVICE_TYPE.DESKTOP
46 | },
47 | isTablet () {
48 | return this.device === DEVICE_TYPE.TABLET
49 | }
50 | }
51 | }
52 |
53 | const AppDeviceEnquire = {
54 | mounted () {
55 | const { $store } = this
56 | deviceEnquire(deviceType => {
57 | switch (deviceType) {
58 | case DEVICE_TYPE.DESKTOP:
59 | $store.commit('TOGGLE_DEVICE', 'desktop')
60 | $store.dispatch('setSidebar', true)
61 | break
62 | case DEVICE_TYPE.TABLET:
63 | $store.commit('TOGGLE_DEVICE', 'tablet')
64 | $store.dispatch('setSidebar', false)
65 | break
66 | case DEVICE_TYPE.MOBILE:
67 | default:
68 | $store.commit('TOGGLE_DEVICE', 'mobile')
69 | $store.dispatch('setSidebar', true)
70 | break
71 | }
72 | })
73 | }
74 | }
75 |
76 | export { mixin, AppDeviceEnquire, mixinDevice }
77 |
--------------------------------------------------------------------------------
/frontend/src/views/home/Page1.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Feature
5 |
What can Online System do for you
6 |
7 |
8 |
9 |
10 |
11 |
12 |
84 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/vo/ExamVo.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : 考试表,要有题目、总分数、时间限制、有效日期、创建者等字段
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019/5/14 07:42
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 |
8 | package lsgwr.exam.vo;
9 |
10 |
11 | import com.fasterxml.jackson.annotation.JsonFormat;
12 | import com.fasterxml.jackson.annotation.JsonProperty;
13 | import lombok.Data;
14 |
15 | import java.util.Date;
16 | import java.util.List;
17 |
18 | @Data
19 | public class ExamVo {
20 | @JsonProperty("id")
21 | private String examId;
22 | @JsonProperty("name")
23 | private String examName;
24 | @JsonProperty("avatar")
25 | private String examAvatar;
26 | @JsonProperty("desc")
27 | private String examDescription;
28 |
29 | @JsonProperty("radios")
30 | private List examQuestionSelectVoRadioList;
31 |
32 | @JsonProperty("checks")
33 | private List examQuestionSelectVoCheckList;
34 |
35 | @JsonProperty("judges")
36 | private List examQuestionSelectVoJudgeList;
37 |
38 | @JsonProperty("score")
39 | private Integer examScore;
40 |
41 | @JsonProperty("radioScore")
42 | private Integer examScoreRadio;
43 |
44 | @JsonProperty("checkScore")
45 | private Integer examScoreCheck;
46 |
47 | @JsonProperty("judgeScore")
48 | private Integer examScoreJudge;
49 |
50 | /**
51 | * 考试的创建人,根据id从用户表中查取姓名
52 | */
53 | @JsonProperty("creator")
54 | private String examCreator;
55 |
56 | /**
57 | * 考试限制的时间,单位为分钟
58 | */
59 | @JsonProperty("elapse")
60 | private Integer examTimeLimit;
61 |
62 | /**
63 | * 开始时间
64 | */
65 | @JsonProperty("startDate")
66 | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
67 | private Date examStartDate;
68 |
69 | /**
70 | * 结束时间
71 | */
72 | @JsonProperty("endDate")
73 | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
74 | private Date examEndDate;
75 |
76 | /**
77 | * 创建时间
78 | */
79 | @JsonProperty("createTime")
80 | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
81 | private Date createTime;
82 |
83 | /**
84 | * 更新时间
85 | */
86 | @JsonProperty("updateTime")
87 | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
88 | private Date updateTime;
89 | }
90 |
--------------------------------------------------------------------------------
/frontend/src/components/Result/Result.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 | {{ title }}
9 |
10 |
11 |
12 |
13 | {{ description }}
14 |
15 |
16 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
59 |
60 |
110 |
--------------------------------------------------------------------------------
/frontend/src/views/list/modules/UpdateAvatarModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | 截图直接粘贴到下面即可,建议图片不要大于80*80
4 |
5 |
6 | 完成
7 | 关闭
8 |
9 |
10 |
11 |
12 |
86 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/config/Swagger2Config.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : Swagger2的配置
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-05-15 07:39
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.config;
8 |
9 | import org.springframework.context.annotation.Bean;
10 | import org.springframework.context.annotation.Configuration;
11 | import springfox.documentation.builders.ApiInfoBuilder;
12 | import springfox.documentation.builders.ParameterBuilder;
13 | import springfox.documentation.builders.PathSelectors;
14 | import springfox.documentation.builders.RequestHandlerSelectors;
15 | import springfox.documentation.schema.ModelRef;
16 | import springfox.documentation.service.ApiInfo;
17 | import springfox.documentation.service.Contact;
18 | import springfox.documentation.service.Parameter;
19 | import springfox.documentation.spi.DocumentationType;
20 | import springfox.documentation.spring.web.plugins.Docket;
21 | import springfox.documentation.swagger2.annotations.EnableSwagger2;
22 |
23 | import java.util.ArrayList;
24 | import java.util.List;
25 |
26 | @Configuration
27 | @EnableSwagger2
28 | public class Swagger2Config {
29 | @Bean
30 | public Docket api() {
31 |
32 | ParameterBuilder ticketPar = new ParameterBuilder();
33 | List pars = new ArrayList<>();
34 | ticketPar.name("Access-Token").description("Rest接口权限认证token,无需鉴权可为空")
35 | .modelRef(new ModelRef("string")).parameterType("header")
36 | //header中的ticket参数非必填,传空也可以
37 | .required(false).build();
38 | //根据每个方法名也知道当前方法在设置什么参数
39 | pars.add(ticketPar.build());
40 |
41 | return new Docket(DocumentationType.SWAGGER_2)
42 | .apiInfo(apiInfo())
43 | .select()
44 | // 自行修改为自己的包路径
45 | .apis(RequestHandlerSelectors.basePackage("lsgwr"))
46 | .paths(PathSelectors.any())
47 | .build()
48 | .globalOperationParameters(pars);
49 | }
50 |
51 | private ApiInfo apiInfo() {
52 | return new ApiInfoBuilder()
53 | .title("online exam by springboot")
54 | .description("在线考试系统 by 梁山广 at 2021")
55 | .termsOfServiceUrl("https://github.com/19920625lsg/spring-boot-online-exam")
56 | .version("2.0")
57 | .contact(new Contact("liangshanguang", "https://github.com/lsgwr/spring-boot-online-exam", "liangshanguang2@gmail.com"))
58 | .build();
59 | }
60 | }
--------------------------------------------------------------------------------
/frontend/src/components/tools/TwoStepCaptcha.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 | 两步验证
10 |
11 |
12 |
返回
13 |
14 | 继续
15 |
16 |
17 |
18 |
19 |
20 |
21 |
36 |
37 |
38 |
39 |
40 |
41 |
83 |
90 |
--------------------------------------------------------------------------------
/frontend/src/components/SettingDrawer/settingConfig.js:
--------------------------------------------------------------------------------
1 | import { message } from 'ant-design-vue/es'
2 | // import defaultSettings from '../defaultSettings';
3 |
4 | let lessNodesAppended
5 |
6 | const colorList = [
7 | {
8 | key: '薄暮', color: '#F5222D'
9 | },
10 | {
11 | key: '火山', color: '#FA541C'
12 | },
13 | {
14 | key: '日暮', color: '#FAAD14'
15 | },
16 | {
17 | key: '明青', color: '#13C2C2'
18 | },
19 | {
20 | key: '极光绿', color: '#52C41A'
21 | },
22 | {
23 | key: '拂晓蓝(默认)', color: '#1890FF'
24 | },
25 | {
26 | key: '极客蓝', color: '#2F54EB'
27 | },
28 | {
29 | key: '酱紫', color: '#722ED1'
30 | }
31 | ]
32 |
33 | const updateTheme = primaryColor => {
34 | // Don't compile less in production!
35 | /* if (process.env.NODE_ENV === 'production') {
36 | return;
37 | } */
38 | // Determine if the component is remounted
39 | if (!primaryColor) {
40 | return
41 | }
42 | const hideMessage = message.loading('正在编译主题!', 0)
43 | function buildIt () {
44 | if (!window.less) {
45 | return
46 | }
47 | setTimeout(() => {
48 | window.less
49 | .modifyVars({
50 | '@primary-color': primaryColor
51 | })
52 | .then(() => {
53 | hideMessage()
54 | })
55 | .catch(() => {
56 | message.error('Failed to update theme')
57 | hideMessage()
58 | })
59 | }, 200)
60 | }
61 | if (!lessNodesAppended) {
62 | // insert less.js and color.less
63 | const lessStyleNode = document.createElement('link')
64 | const lessConfigNode = document.createElement('script')
65 | const lessScriptNode = document.createElement('script')
66 | lessStyleNode.setAttribute('rel', 'stylesheet/less')
67 | lessStyleNode.setAttribute('href', '/color.less')
68 | lessConfigNode.innerHTML = `
69 | window.less = {
70 | async: true,
71 | env: 'production',
72 | javascriptEnabled: true
73 | };
74 | `
75 | lessScriptNode.src = 'https://gw.alipayobjects.com/os/lib/less.js/3.8.1/less.min.js'
76 | lessScriptNode.async = true
77 | lessScriptNode.onload = () => {
78 | buildIt()
79 | lessScriptNode.onload = null
80 | }
81 | document.body.appendChild(lessStyleNode)
82 | document.body.appendChild(lessConfigNode)
83 | document.body.appendChild(lessScriptNode)
84 | lessNodesAppended = true
85 | } else {
86 | buildIt()
87 | }
88 | }
89 |
90 | const updateColorWeak = colorWeak => {
91 | // document.body.className = colorWeak ? 'colorWeak' : '';
92 | colorWeak ? document.body.classList.add('colorWeak') : document.body.classList.remove('colorWeak')
93 | }
94 |
95 | export { updateTheme, colorList, updateColorWeak }
96 |
--------------------------------------------------------------------------------
/frontend/src/views/account/settings/AvatarModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
21 |
22 |
23 |
24 |
25 |
![]()
26 |
27 |
28 |
29 |
30 |
31 | 取消
32 | 保存
33 |
34 |
35 |
36 |
91 |
92 |
110 |
--------------------------------------------------------------------------------
/frontend/src/components/NoticeIcon/NoticeIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | 123
35 |
36 |
37 | 123
38 |
39 |
40 |
41 |
42 |
47 |
48 |
49 |
50 |
74 |
75 |
80 |
90 |
--------------------------------------------------------------------------------
/frontend/src/permission.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import router from './router'
3 | import store from './store'
4 |
5 | import NProgress from 'nprogress' // progress bar
6 | import 'nprogress/nprogress.css' // progress bar style
7 | import notification from 'ant-design-vue/es/notification'
8 | import { setDocumentTitle, domTitle } from './utils/domUtil'
9 | import { ACCESS_TOKEN } from './store/mutation-types'
10 |
11 | NProgress.configure({ showSpinner: false }) // NProgress Configuration
12 |
13 | const whiteList = ['login', 'register', 'registerResult'] // no redirect whitelist, 免登录白名单
14 |
15 | router.beforeEach((to, from, next) => {
16 | NProgress.start() // start progress bar
17 | to.meta && (typeof to.meta.title !== 'undefined' && setDocumentTitle(`${to.meta.title} - ${domTitle}`))
18 | if (Vue.ls.get(ACCESS_TOKEN)) {
19 | /* has token, 有token,即用户已经登录 */
20 | if (to.path === '/user/login') {
21 | // 如果用户想访问登录界面就直接跳到首页
22 | next({ path: '/dashboard/workplace' })
23 | NProgress.done()
24 | } else {
25 | // 用户已经登录,且访问地不是登录页面,那么就进行权限校验,看看用户有没有权限访问这个页面
26 | if (store.getters.roles.length === 0) { // 如果store还没有用户的角色属性(可以在Vue的Chrome组件中看Vuex),那就请求
27 | store
28 | .dispatch('GetInfo') // 登陆后访问除登录以外的任何一个页面都会先调用GetInfo方法,看看用户的权限是否有资格访问
29 | .then(res => {
30 | const roles = res.data && res.data.role // 响应不为空,而且含有role属性
31 | store.dispatch('GenerateRoutes', { roles }).then(() => { // 遍历roles,生成路由
32 | // 根据roles权限生成可访问的路由表
33 | // 动态添加可访问路由表
34 | router.addRoutes(store.getters.addRouters)
35 | const redirect = decodeURIComponent(from.query.redirect || to.path)
36 | if (to.path === redirect) {
37 | // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
38 | next({ ...to, replace: true })
39 | } else {
40 | // 跳转到目的路由
41 | next({ path: redirect })
42 | }
43 | })
44 | })
45 | .catch(() => {
46 | notification.error({
47 | message: '错误',
48 | description: '请求用户信息失败,请重试'
49 | })
50 | store.dispatch('Logout').then(() => {
51 | next({ path: '/user/login', query: { redirect: to.fullPath } })
52 | })
53 | })
54 | } else {
55 | next()
56 | }
57 | }
58 | } else {
59 | if (whiteList.includes(to.name)) {
60 | // 在免登录白名单,直接进入
61 | next()
62 | } else {
63 | next({ path: '/user/login', query: { redirect: to.fullPath } })
64 | NProgress.done() // if current page is login will not trigger afterEach hook, so manually handle it
65 | }
66 | }
67 | })
68 |
69 | router.afterEach(() => {
70 | NProgress.done() // finish progress bar
71 | })
72 |
--------------------------------------------------------------------------------
/frontend/vue.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const webpack = require('webpack')
3 |
4 | function resolve (dir) {
5 | return path.join(__dirname, dir)
6 | }
7 |
8 | // vue.config.js
9 | module.exports = {
10 | /*
11 | Vue-cli3:
12 | Crashed when using Webpack `import()` #2463
13 | https://github.com/vuejs/vue-cli/issues/2463
14 |
15 | */
16 | /*
17 | pages: {
18 | index: {
19 | entry: 'src/main.js',
20 | chunks: ['chunk-vendors', 'chunk-common', 'index']
21 | }
22 | },
23 | */
24 | configureWebpack: {
25 | plugins: [
26 | // Ignore all locale files of moment.js
27 | new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
28 | new webpack.ProvidePlugin({
29 | $: 'jquery',
30 | jQuery: 'jquery'
31 | })
32 | ]
33 | },
34 |
35 | chainWebpack: (config) => {
36 | config.resolve.alias
37 | .set('@$', resolve('src'))
38 | .set('@api', resolve('src/api'))
39 | .set('@assets', resolve('src/assets'))
40 | .set('@comp', resolve('src/components'))
41 | .set('@views', resolve('src/views'))
42 | .set('@layout', resolve('src/layout'))
43 | .set('@static', resolve('src/static'))
44 | .set('jquery', resolve('node_modules/jquery/src/jquery'))
45 |
46 | const svgRule = config.module.rule('svg')
47 | svgRule.uses.clear()
48 | svgRule
49 | .oneOf('inline')
50 | .resourceQuery(/inline/)
51 | .use('vue-svg-icon-loader')
52 | .loader('vue-svg-icon-loader')
53 | .end()
54 | .end()
55 | .oneOf('external')
56 | .use('file-loader')
57 | .loader('file-loader')
58 | .options({
59 | name: 'assets/[name].[hash:8].[ext]'
60 | })
61 | /* svgRule.oneOf('inline')
62 | .resourceQuery(/inline/)
63 | .use('vue-svg-loader')
64 | .loader('vue-svg-loader')
65 | .end()
66 | .end()
67 | .oneOf('external')
68 | .use('file-loader')
69 | .loader('file-loader')
70 | .options({
71 | name: 'assets/[name].[hash:8].[ext]'
72 | })
73 | */
74 | },
75 |
76 | css: {
77 | loaderOptions: {
78 | less: {
79 | modifyVars: {
80 | /* less 变量覆盖,用于自定义 ant design 主题 */
81 |
82 | /*
83 | 'primary-color': '#F5222D',
84 | 'link-color': '#F5222D',
85 | 'border-radius-base': '4px',
86 | */
87 | },
88 | javascriptEnabled: true
89 | }
90 | }
91 | },
92 |
93 | devServer: {
94 | // development server port 8000
95 | port: 8000,
96 | proxy: 'http://localhost:9527'
97 | },
98 |
99 | // disable source map in production
100 | productionSourceMap: false,
101 | lintOnSave: undefined,
102 | // babel-loader no-ignore node_modules/*
103 | transpileDependencies: []
104 | }
105 |
--------------------------------------------------------------------------------
/frontend/src/components/Exception/ExceptionPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
{{ config[type].title }}
9 |
{{ config[type].desc }}
10 |
13 |
14 |
15 |
16 |
17 |
40 |
131 |
--------------------------------------------------------------------------------
/frontend/src/components/TagSelect/index.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'ant-design-vue/es/_util/vue-types'
2 | import Option from './TagSelectOption.jsx'
3 | import { filterEmpty } from '../../components/_util/util'
4 |
5 | export default {
6 | Option,
7 | name: 'TagSelect',
8 | model: {
9 | prop: 'checked',
10 | event: 'change'
11 | },
12 | props: {
13 | prefixCls: {
14 | type: String,
15 | default: 'ant-pro-tag-select'
16 | },
17 | defaultValue: {
18 | type: PropTypes.array,
19 | default: null
20 | },
21 | value: {
22 | type: PropTypes.array,
23 | default: null
24 | },
25 | expandable: {
26 | type: Boolean,
27 | default: false
28 | },
29 | hideCheckAll: {
30 | type: Boolean,
31 | default: false
32 | }
33 | },
34 | data () {
35 | return {
36 | expand: false,
37 | localCheckAll: false,
38 | items: this.getItemsKey(filterEmpty(this.$slots.default)),
39 | val: this.value || this.defaultValue || []
40 | }
41 | },
42 | methods: {
43 | onChange (checked) {
44 | const key = Object.keys(this.items).filter(key => key === checked.value)
45 | this.items[key] = checked.checked
46 | const bool = Object.values(this.items).lastIndexOf(false)
47 | if (bool === -1) {
48 | this.localCheckAll = true
49 | } else {
50 | this.localCheckAll = false
51 | }
52 | },
53 | onCheckAll (checked) {
54 | Object.keys(this.items).forEach(v => {
55 | this.items[v] = checked.checked
56 | })
57 | this.localCheckAll = checked.checked
58 | },
59 | getItemsKey (items) {
60 | const totalItem = {}
61 | items.forEach(item => {
62 | totalItem[item.componentOptions.propsData && item.componentOptions.propsData.value] = false
63 | })
64 | return totalItem
65 | },
66 | // CheckAll Button
67 | renderCheckAll () {
68 | return !this.hideCheckAll && () || null
69 | },
70 | // expandable
71 | renderExpandable () {
72 |
73 | },
74 | // render option
75 | renderTags (items) {
76 | const listeners = {
77 | change: (checked) => {
78 | this.onChange(checked)
79 | this.$emit('change', checked)
80 | }
81 | }
82 |
83 | return items.map(vnode => {
84 | const options = vnode.componentOptions
85 | options.listeners = listeners
86 | return vnode
87 | })
88 | }
89 | },
90 | render () {
91 | const { $props: { prefixCls } } = this
92 | const classString = {
93 | [`${prefixCls}`]: true
94 | }
95 | const tagItems = filterEmpty(this.$slots.default)
96 | return (
97 |
98 | {this.renderCheckAll()}
99 | {this.renderTags(tagItems)}
100 |
101 | )
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/backend/src/main/java/lsgwr/exam/service/ExamService.java:
--------------------------------------------------------------------------------
1 | /***********************************************************
2 | * @Description : 考试接口
3 | * @author : 梁山广(Laing Shan Guang)
4 | * @date : 2019-05-28 08:05
5 | * @email : liangshanguang2@gmail.com
6 | ***********************************************************/
7 | package lsgwr.exam.service;
8 |
9 | import lsgwr.exam.entity.Exam;
10 | import lsgwr.exam.entity.ExamRecord;
11 | import lsgwr.exam.vo.*;
12 |
13 | import java.util.HashMap;
14 | import java.util.List;
15 |
16 | public interface ExamService {
17 | /**
18 | * 获取所有的问题列表
19 | */
20 | List getQuestionAll();
21 |
22 | /**
23 | * 根据前端传过来的问题实体更新问题和选项
24 | *
25 | * @param questionVo 问题实体
26 | */
27 | QuestionVo updateQuestion(QuestionVo questionVo);
28 |
29 | /**
30 | * 问题创建
31 | *
32 | * @param questionCreateVo 问题创建实体类
33 | */
34 | void questionCreate(QuestionCreateVo questionCreateVo);
35 |
36 | /**
37 | * 获取问题的选项、分类和难度的下拉列表
38 | *
39 | * @return 选项、分类和难度的封装对象
40 | */
41 | QuestionSelectionVo getSelections();
42 |
43 | /**
44 | * 获取问题详情
45 | *
46 | * @param id 问题的id
47 | * @return 问题详情的封装VO
48 | */
49 | QuestionDetailVo getQuestionDetail(String id);
50 |
51 | /**
52 | * 获取全部考试的列表
53 | */
54 | List getExamAll();
55 |
56 | /**
57 | * 获取所有问题的下拉列表,方便前端创建考试时筛选
58 | *
59 | * @return 适配前端的问题下拉列表
60 | */
61 | ExamQuestionTypeVo getExamQuestionType();
62 |
63 | /**
64 | * 根据前端组装的参数进行考试创建
65 | *
66 | * @param examCreateVo 前端组装的考试对象
67 | * @param userId 用户id
68 | * @return 创建好的考试
69 | */
70 | Exam create(ExamCreateVo examCreateVo, String userId);
71 |
72 | /**
73 | * 获取考试卡片列表
74 | *
75 | * @return 考试卡片列表
76 | */
77 | List getExamCardList();
78 |
79 | /**
80 | * 根据考试的id获取考试的详情
81 | *
82 | * @param id exam表的主键
83 | * @return 考试详情的封装的VO对象
84 | */
85 | ExamDetailVo getExamDetail(String id);
86 |
87 | /**
88 | * 根据用户提交的作答信息进行判分
89 | *
90 | * @param userId 考试人
91 | * @param examId 参与的考试
92 | * @param answersMap 作答情况
93 | * @return 本次考试记录
94 | */
95 | ExamRecord judge(String userId, String examId, HashMap> answersMap);
96 |
97 | /**
98 | * 根据用户id获取此用户的所有考试信息
99 | *
100 | * @param userId 用户id
101 | * @return 该用户的所有考试记录
102 | */
103 | List getExamRecordList(String userId);
104 |
105 | /**
106 | * 获取指定某次考试记录的详情
107 | *
108 | * @param recordId 考试记录的id
109 | * @return 考试详情
110 | */
111 | RecordDetailVo getRecordDetail(String recordId);
112 |
113 |
114 | /**
115 | * 更新考试
116 | *
117 | * @param examVo 获取所有考试的接口中返回的考试信息结构
118 | * @param userId 当前的用户
119 | * @return 更新后的考试详情
120 | */
121 | Exam update(ExamVo examVo, String userId);
122 | }
123 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # spring-boot-online-exam
2 |
3 | > 在线Demo预览,http://129.211.88.191 ,账户分别是admin、teacher、student,密码是admin123。视频讲解代码:https://www.bilibili.com/video/BV1FP4y1L7xt/
4 |
5 | > 好消息!!!一个小伙伴做了Python实现,欢迎大家star:https://github.com/xingxingzaixian/django-drf-online-exam
6 |
7 | ## 1.快速体验
8 | ### 1.1 事先准备
9 | > clone代码并进入代码路径
10 |
11 | ```shell
12 | git clone git@github.com:lsgwr/spring-boot-online-exam.git
13 | cd spring-boot-online-exam
14 | ```
15 |
16 | 下面按照Linux和windows说明快速搭建的方法
17 | ### 1.2 Linux
18 | 执行代码下的脚本start.sh即可
19 |
20 | 然后访问 http://ip:80 即可访问自己搭建的考试系统
21 |
22 | ### 1.3 windows
23 | + 1.安装JDK,推荐JDK8
24 | + 2.从官方仓库下载发布的jar包,建议选择最新版:https://github.com/lsgwr/spring-boot-online-exam/releases
25 | + 3.安装MySQL,创建数据库exam,并初始化密码为aA111111,导入doc/sql/exam.sql文件来创建数据库
26 | + 4.启动jar包:`java -jar exam.jar`
27 | + 5.访问:http://ip:9527 即可访问自己搭建的考试系统
28 |
29 | ## 2.介绍
30 | 基于springboot的在线考试系统
31 |
32 | ### 2.1 功能简介
33 |
34 | + 支持单选题、多选题、判断题
35 | + 支持学生(student)、教师(teacher)、管理员(admin)三种角色
36 | + 学生:参加考试和查看我的考试
37 | + 教师:学生的所有权限+创建/编辑题目+创建/编辑考试
38 | + 管理员:教师的所有权限+管理用户
39 |
40 | ### 2.3 软件架构
41 |
42 | > 前后端分离,前段组件化,方便二次开发;后端
43 |
44 | + 后端采用SpringBoot+JPA++Swagger2+JWT校验,根据不同用户的权限返回给用户不同的数据
45 | + 后端采用Vue+AntDesign,组件化拆分,封装了很多年公共组件,方便维护和二次开发
46 |
47 | ### 2.3 使用教程
48 |
49 | + 1.下载代码
50 | ```shell
51 | git clone https://github.com/19920625lsg/spring-boot-online-exam.git
52 | ```
53 | + 2.初始化数据库
54 | > 安装mysql的步骤这里省略,网上的教程很多。安装好mysql后,新建exam数据库,密码和`spring-boot-online-exam/backend/exam/src/main/resources/application.yml`的`password: xxxxxx`保持一致,然后导入`spring-boot-online-exam/doc/sql/exam.sql`
55 | + 3.启动后端
56 | > 打开`spring-boot-online-exam/backend/exam`这个Maven项目,可以在IDE里启动或者执行`mvn install`生成jar包启动
57 | + 4.启动前端
58 | + 进入到前端代码路径 `cd spring-boot-online-exam/frontend/exam/`
59 | + 安装依赖 `npm install`
60 | + 启动前端 `npm run serve`
61 | + 5.部署完毕,查看效果
62 | > 打开 http://localhost:8000 或者 http://本机ip:8000 即可查看演示效果
63 |
64 | ## 3.功能图示
65 |
66 | + 1.管理题目
67 | + 1.1 题目列表
68 | > 
69 | + 1.2 题目创建
70 | > 
71 | + 1.3 题目更新
72 | > 
73 | + 2.考试管理
74 | + 2.1 考试列表
75 | > 
76 | + 2.2 考试创建
77 | > 
78 | + 2.3 考试更新(`还有点小bug,开发中`)
79 | > 
80 | + 3.我的考试
81 | + 3.1 参加考试
82 | > 在"考试列表"模块点击自己想参加的考试卡片即可
83 | > 
84 | > 
85 | + 3.2 考试记录查看
86 | > 
87 |
88 | ## 4.参与贡献
89 |
90 | 1. Fork 本仓库
91 | 2. 新建 exam_xxx 分支
92 | 3. 提交代码
93 | 4. 新建 Pull Request
94 |
95 | ## 5.Todo
96 | + `√`0.修复issue提地bug:题目创建失败
97 | + `√`1.考试详情编辑
98 | + 2.支持题目和考试的删除`删除的话比较麻烦,先不做了,最好是弄个visible字段,不实际删除,要不后面有些关联数据找不到就不好了`
99 | > 如果题目有关联的考试则必须先删除对应的考试,反过来删除考试则不用删除题目
100 | + 3.图片改成base64存到数据库中
101 | + 4.题干和选项支持富文本
102 | + 5.支持批量导入题目
103 | + 6.新增用户管理、学科管理功能
104 | + 7.老师能考到所有学生的成绩以及考试的统计信息
105 | + 8.更多的数据分析功能
106 | + 9.支持容器化一键式部署(编好Dockerfile)
107 | + 10.支持移动端,最好用uniapp做
108 | + ......抓紧做吧,争取每周末做一点......
109 |
--------------------------------------------------------------------------------