Browse Source

在线考试系统服务器

chenminghua 5 years ago
commit
46215b440b
100 changed files with 3094 additions and 0 deletions
  1. 12 0
      onlineexam-admin/.babelrc
  2. 14 0
      onlineexam-admin/.editorconfig
  3. 3 0
      onlineexam-admin/.eslintignore
  4. 196 0
      onlineexam-admin/.eslintrc.js
  5. 15 0
      onlineexam-admin/.gitignore
  6. 10 0
      onlineexam-admin/.postcssrc.js
  7. 5 0
      onlineexam-admin/.travis.yml
  8. 21 0
      onlineexam-admin/LICENSE
  9. 96 0
      onlineexam-admin/README-zh.md
  10. 68 0
      onlineexam-admin/README.md
  11. 45 0
      onlineexam-admin/build/build.js
  12. 64 0
      onlineexam-admin/build/check-versions.js
  13. BIN
      onlineexam-admin/build/logo.png
  14. 108 0
      onlineexam-admin/build/utils.js
  15. 5 0
      onlineexam-admin/build/vue-loader.conf.js
  16. 108 0
      onlineexam-admin/build/webpack.base.conf.js
  17. 95 0
      onlineexam-admin/build/webpack.dev.conf.js
  18. 177 0
      onlineexam-admin/build/webpack.prod.conf.js
  19. 9 0
      onlineexam-admin/config/dev.env.js
  20. 96 0
      onlineexam-admin/config/index.js
  21. 6 0
      onlineexam-admin/config/prod.env.js
  22. BIN
      onlineexam-admin/favicon.ico
  23. 14 0
      onlineexam-admin/index.html
  24. 26 0
      onlineexam-admin/mock/index.js
  25. 20 0
      onlineexam-admin/mock/table.js
  26. 64 0
      onlineexam-admin/mock/user.js
  27. 14 0
      onlineexam-admin/mock/utils.js
  28. 98 0
      onlineexam-admin/package.json
  29. 17 0
      onlineexam-admin/src/App.vue
  30. 54 0
      onlineexam-admin/src/api/bankManage.js
  31. 14 0
      onlineexam-admin/src/api/feedback.js
  32. 10 0
      onlineexam-admin/src/api/login.js
  33. 13 0
      onlineexam-admin/src/api/notice.js
  34. 17 0
      onlineexam-admin/src/api/paper.js
  35. 13 0
      onlineexam-admin/src/api/rotationImg.js
  36. 26 0
      onlineexam-admin/src/api/student.js
  37. 13 0
      onlineexam-admin/src/api/subject.js
  38. 13 0
      onlineexam-admin/src/api/teacher.js
  39. BIN
      onlineexam-admin/src/assets/404_images/404.png
  40. BIN
      onlineexam-admin/src/assets/404_images/404_cloud.png
  41. BIN
      onlineexam-admin/src/assets/images/admin.png
  42. BIN
      onlineexam-admin/src/assets/images/backend-bg-img-min.png
  43. BIN
      onlineexam-admin/src/assets/images/home_img.png
  44. BIN
      onlineexam-admin/src/assets/images/info_tab_img.png
  45. BIN
      onlineexam-admin/src/assets/images/no_read.png
  46. BIN
      onlineexam-admin/src/assets/images/profile.jpg
  47. BIN
      onlineexam-admin/src/assets/images/read.png
  48. BIN
      onlineexam-admin/src/assets/images/teacher.jpg
  49. 122 0
      onlineexam-admin/src/components/BackToTop/index.vue
  50. 68 0
      onlineexam-admin/src/components/Breadcrumb/index.vue
  51. 342 0
      onlineexam-admin/src/components/Charts/mixChart.vue
  52. 32 0
      onlineexam-admin/src/components/Charts/mixins/resize.js
  53. 31 0
      onlineexam-admin/src/components/Emotion/Emotion.vue
  54. 68 0
      onlineexam-admin/src/components/Emotion/index.vue
  55. 42 0
      onlineexam-admin/src/components/Hamburger/index.vue
  56. 101 0
      onlineexam-admin/src/components/Pagination/index.vue
  57. 140 0
      onlineexam-admin/src/components/PanThumb/index.vue
  58. 60 0
      onlineexam-admin/src/components/Screenfull/index.vue
  59. 43 0
      onlineexam-admin/src/components/SvgIcon/index.vue
  60. 113 0
      onlineexam-admin/src/components/TextHoverEffect/Mallki.vue
  61. 143 0
      onlineexam-admin/src/components/UploadExcel/index.vue
  62. 42 0
      onlineexam-admin/src/config/ajax.js
  63. 13 0
      onlineexam-admin/src/directive/waves/index.js
  64. 26 0
      onlineexam-admin/src/directive/waves/waves.css
  65. 72 0
      onlineexam-admin/src/directive/waves/waves.js
  66. 16 0
      onlineexam-admin/src/filters/index.js
  67. 9 0
      onlineexam-admin/src/icons/index.js
  68. 1 0
      onlineexam-admin/src/icons/svg/chart.svg
  69. 1 0
      onlineexam-admin/src/icons/svg/example.svg
  70. 1 0
      onlineexam-admin/src/icons/svg/excel.svg
  71. 1 0
      onlineexam-admin/src/icons/svg/exit-fullscreen.svg
  72. 1 0
      onlineexam-admin/src/icons/svg/explain.svg
  73. 1 0
      onlineexam-admin/src/icons/svg/eye-open.svg
  74. 1 0
      onlineexam-admin/src/icons/svg/eye.svg
  75. 1 0
      onlineexam-admin/src/icons/svg/feedback.svg
  76. 1 0
      onlineexam-admin/src/icons/svg/fill-bank.svg
  77. 1 0
      onlineexam-admin/src/icons/svg/fill-info.svg
  78. 1 0
      onlineexam-admin/src/icons/svg/form.svg
  79. 1 0
      onlineexam-admin/src/icons/svg/fullscreen.svg
  80. 1 0
      onlineexam-admin/src/icons/svg/home.svg
  81. 1 0
      onlineexam-admin/src/icons/svg/info.svg
  82. 1 0
      onlineexam-admin/src/icons/svg/judge-bank.svg
  83. 1 0
      onlineexam-admin/src/icons/svg/judge-info.svg
  84. 1 0
      onlineexam-admin/src/icons/svg/link.svg
  85. 1 0
      onlineexam-admin/src/icons/svg/multiple-bank.svg
  86. 1 0
      onlineexam-admin/src/icons/svg/multiple-info.svg
  87. 1 0
      onlineexam-admin/src/icons/svg/nested.svg
  88. 1 0
      onlineexam-admin/src/icons/svg/notice.svg
  89. 1 0
      onlineexam-admin/src/icons/svg/password.svg
  90. 1 0
      onlineexam-admin/src/icons/svg/que-bank.svg
  91. 1 0
      onlineexam-admin/src/icons/svg/rotation-img.svg
  92. 1 0
      onlineexam-admin/src/icons/svg/score.svg
  93. 1 0
      onlineexam-admin/src/icons/svg/single-bank.svg
  94. 1 0
      onlineexam-admin/src/icons/svg/single-info.svg
  95. 1 0
      onlineexam-admin/src/icons/svg/student-info.svg
  96. 1 0
      onlineexam-admin/src/icons/svg/student.svg
  97. 1 0
      onlineexam-admin/src/icons/svg/subject.svg
  98. 1 0
      onlineexam-admin/src/icons/svg/table.svg
  99. 1 0
      onlineexam-admin/src/icons/svg/teacher-info.svg
  100. 0 0
      onlineexam-admin/src/icons/svg/teacher.svg

+ 12 - 0
onlineexam-admin/.babelrc

@@ -0,0 +1,12 @@
+{
+  "presets": [
+    ["env", {
+      "modules": false,
+      "targets": {
+        "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
+      }
+    }],
+    "stage-2"
+  ],
+  "plugins":["transform-vue-jsx", "transform-runtime"]
+}

+ 14 - 0
onlineexam-admin/.editorconfig

@@ -0,0 +1,14 @@
+# http://editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.md]
+insert_final_newline = false
+trim_trailing_whitespace = false

+ 3 - 0
onlineexam-admin/.eslintignore

@@ -0,0 +1,3 @@
+build/*.js
+config/*.js
+src/assets

+ 196 - 0
onlineexam-admin/.eslintrc.js

@@ -0,0 +1,196 @@
+module.exports = {
+  root: true,
+  parserOptions: {
+    parser: 'babel-eslint',
+    sourceType: 'module'
+  },
+  env: {
+    browser: true,
+    node: true,
+    es6: true,
+  },
+  extends: ['plugin:vue/recommended', 'eslint:recommended'],
+
+  // add your custom rules here
+  //it is base on https://github.com/vuejs/eslint-config-vue
+  rules: {
+    "vue/max-attributes-per-line": [2, {
+      "singleline": 10,
+      "multiline": {
+        "max": 1,
+        "allowFirstLine": false
+      }
+    }],
+    "vue/name-property-casing": ["error", "PascalCase"],
+    'accessor-pairs': 2,
+    'arrow-spacing': [2, {
+      'before': true,
+      'after': true
+    }],
+    'block-spacing': [2, 'always'],
+    'brace-style': [2, '1tbs', {
+      'allowSingleLine': true
+    }],
+    'camelcase': [0, {
+      'properties': 'always'
+    }],
+    'comma-dangle': [2, 'never'],
+    'comma-spacing': [2, {
+      'before': false,
+      'after': true
+    }],
+    'comma-style': [2, 'last'],
+    'constructor-super': 2,
+    'curly': [2, 'multi-line'],
+    'dot-location': [2, 'property'],
+    'eol-last': 2,
+    'eqeqeq': [2, 'allow-null'],
+    'generator-star-spacing': [2, {
+      'before': true,
+      'after': true
+    }],
+    'handle-callback-err': [2, '^(err|error)$'],
+    'indent': [2, 2, {
+      'SwitchCase': 1
+    }],
+    'jsx-quotes': [2, 'prefer-single'],
+    'key-spacing': [2, {
+      'beforeColon': false,
+      'afterColon': true
+    }],
+    'keyword-spacing': [2, {
+      'before': true,
+      'after': true
+    }],
+    'new-cap': [2, {
+      'newIsCap': true,
+      'capIsNew': false
+    }],
+    'new-parens': 2,
+    'no-array-constructor': 2,
+    'no-caller': 2,
+    'no-console': 'off',
+    'no-class-assign': 2,
+    'no-cond-assign': 2,
+    'no-const-assign': 2,
+    'no-control-regex': 2,
+    'no-delete-var': 2,
+    'no-dupe-args': 2,
+    'no-dupe-class-members': 2,
+    'no-dupe-keys': 2,
+    'no-duplicate-case': 2,
+    'no-empty-character-class': 2,
+    'no-empty-pattern': 2,
+    'no-eval': 2,
+    'no-ex-assign': 2,
+    'no-extend-native': 2,
+    'no-extra-bind': 2,
+    'no-extra-boolean-cast': 2,
+    'no-extra-parens': [2, 'functions'],
+    'no-fallthrough': 2,
+    'no-floating-decimal': 2,
+    'no-func-assign': 2,
+    'no-implied-eval': 2,
+    'no-inner-declarations': [2, 'functions'],
+    'no-invalid-regexp': 2,
+    'no-irregular-whitespace': 2,
+    'no-iterator': 2,
+    'no-label-var': 2,
+    'no-labels': [2, {
+      'allowLoop': false,
+      'allowSwitch': false
+    }],
+    'no-lone-blocks': 2,
+    'no-mixed-spaces-and-tabs': 2,
+    'no-multi-spaces': 2,
+    'no-multi-str': 2,
+    'no-multiple-empty-lines': [2, {
+      'max': 1
+    }],
+    'no-native-reassign': 2,
+    'no-negated-in-lhs': 2,
+    'no-new-object': 2,
+    'no-new-require': 2,
+    'no-new-symbol': 2,
+    'no-new-wrappers': 2,
+    'no-obj-calls': 2,
+    'no-octal': 2,
+    'no-octal-escape': 2,
+    'no-path-concat': 2,
+    'no-proto': 2,
+    'no-redeclare': 2,
+    'no-regex-spaces': 2,
+    'no-return-assign': [2, 'except-parens'],
+    'no-self-assign': 2,
+    'no-self-compare': 2,
+    'no-sequences': 2,
+    'no-shadow-restricted-names': 2,
+    'no-spaced-func': 2,
+    'no-sparse-arrays': 2,
+    'no-this-before-super': 2,
+    'no-throw-literal': 2,
+    'no-trailing-spaces': 2,
+    'no-undef': 2,
+    'no-undef-init': 2,
+    'no-unexpected-multiline': 2,
+    'no-unmodified-loop-condition': 2,
+    'no-unneeded-ternary': [2, {
+      'defaultAssignment': false
+    }],
+    'no-unreachable': 2,
+    'no-unsafe-finally': 2,
+    'no-unused-vars': [2, {
+      'vars': 'all',
+      'args': 'none'
+    }],
+    'no-useless-call': 2,
+    'no-useless-computed-key': 2,
+    'no-useless-constructor': 2,
+    'no-useless-escape': 0,
+    'no-whitespace-before-property': 2,
+    'no-with': 2,
+    'one-var': [2, {
+      'initialized': 'never'
+    }],
+    'operator-linebreak': [2, 'after', {
+      'overrides': {
+        '?': 'before',
+        ':': 'before'
+      }
+    }],
+    'padded-blocks': [2, 'never'],
+    'quotes': [2, 'single', {
+      'avoidEscape': true,
+      'allowTemplateLiterals': true
+    }],
+    'semi': [2, 'never'],
+    'semi-spacing': [2, {
+      'before': false,
+      'after': true
+    }],
+    'space-before-blocks': [2, 'always'],
+    'space-before-function-paren': [2, 'never'],
+    'space-in-parens': [2, 'never'],
+    'space-infix-ops': 2,
+    'space-unary-ops': [2, {
+      'words': true,
+      'nonwords': false
+    }],
+    'spaced-comment': [2, 'always', {
+      'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ',']
+    }],
+    'template-curly-spacing': [2, 'never'],
+    'use-isnan': 2,
+    'valid-typeof': 2,
+    'wrap-iife': [2, 'any'],
+    'yield-star-spacing': [2, 'both'],
+    'yoda': [2, 'never'],
+    'prefer-const': 2,
+    'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
+    'object-curly-spacing': [2, 'always', {
+      objectsInObjects: false
+    }],
+    'array-bracket-spacing': [2, 'never']
+  }
+}
+

+ 15 - 0
onlineexam-admin/.gitignore

@@ -0,0 +1,15 @@
+.DS_Store
+node_modules/
+dist/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+package-lock.json
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln

+ 10 - 0
onlineexam-admin/.postcssrc.js

@@ -0,0 +1,10 @@
+// https://github.com/michael-ciniawsky/postcss-load-config
+
+module.exports = {
+  "plugins": {
+    "postcss-import": {},
+    "postcss-url": {},
+    // to edit target browsers: use "browserslist" field in package.json
+    "autoprefixer": {}
+  }
+}

+ 5 - 0
onlineexam-admin/.travis.yml

@@ -0,0 +1,5 @@
+language: node_js
+node_js: stable
+script: npm run test
+notifications:
+  email: false

+ 21 - 0
onlineexam-admin/LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017-present PanJiaChen
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

File diff suppressed because it is too large
+ 96 - 0
onlineexam-admin/README-zh.md


+ 68 - 0
onlineexam-admin/README.md

@@ -0,0 +1,68 @@
+# 简介(有用的话记得给个star哈,谢谢啦)
+
+## 1. 项目描述
+
+1. 此项目为一个基于Vue的前后端分离的在线考试系统项目
+2. 使用了 Vue 全家桶+ES5ES6ES7+Webpack 等前端新技术 
+3. 后端基于SpringBoot搭建SSM框架
+4. 包括学生端、教师端和管理员端 
+5. 采用模块化、组件化、工程化的模式开发
+6. 教师端和管理员端基于[vue-element-admin模板](https://github.com/PanJiaChen/vue-element-admin)
+
+## 2. 能从此项目中学到什么? 
+
+### 2.1 项目开发流程及开发方法 
+
+1. 熟悉一个项目的开发流程
+2. 学会组件化、模块化、工程化的开发模式 
+3. 掌握使用 vue-cli 脚手架初始化 Vue.js 项目
+4. 学会模拟 json 后端数据,实现前后端分离开发 
+5. 学会 ES5ES6ES7+eslint 的开发方式 
+6. 掌握一些项目优化技巧 
+7. 掌握WebSocket实时在线通信
+
+### 2.2 Vue 插件或第三方库
+
+1. 学会使用 vue-router 开发单页应用 
+2. 学会使用 axios/vue-resource 与后端进行数据交互
+3. 学会使用 vuex 管理应用组件状态
+4. 学会使用 基于Vue的插件, 如vue-seamless-scroll实现无缝滚动,v-viewer实现图片预览,ly-tab实现触摸滑动并具有回弹效果等
+5. 学会使用 mint-ui、muse-ui和element-ui 组件库构建界面
+6. 学会使用 mockjs 模拟后台数据接口
+7. 学会通过API接口与后端进行交互  
+8. 学会通过第三方平台七牛云进行图片的存取  
+
+## 3. 运行项目
+
+**onlineexam-admin文件夹:**
+
+1. `npm install`
+2. `npm run dev`
+
+**onlineexam-admin文件夹:**
+
+1. `npm install`
+2. `npm start`
+
+## 4. 其他项目地址
+
+[在线考试系统学生端](https://github.com/FrontDemon/onlineexam-student)
+
+[在线考试系统教师端](https://github.com/FrontDemon/onlineexam-teacher)
+
+[在线考试系统后端项目源码](https://github.com/FrontDemon/onlineexam-system-backend)
+
+## 5. 在线演示地址
+
+**在线考试系统管理员端:** http://maweitao.top/onlineexam-admin
+
+## 6. 部分演示截图
+
+<p align="center">
+	<img src="http://qiniu.maweitao.top/admin-home.png" alt="Sample">
+	<p align="center">
+    <em>在线考试系统管理员端首页</em>
+  </p>
+</p>
+
+

+ 45 - 0
onlineexam-admin/build/build.js

@@ -0,0 +1,45 @@
+'use strict'
+require('./check-versions')()
+
+process.env.NODE_ENV = 'production'
+
+const ora = require('ora')
+const rm = require('rimraf')
+const path = require('path')
+const chalk = require('chalk')
+const webpack = require('webpack')
+const config = require('../config')
+const webpackConfig = require('./webpack.prod.conf')
+
+const spinner = ora('building for production...')
+spinner.start()
+
+rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
+  if (err) throw err
+  webpack(webpackConfig, (err, stats) => {
+    spinner.stop()
+    if (err) throw err
+    process.stdout.write(
+      stats.toString({
+        colors: true,
+        modules: false,
+        children: false,
+        chunks: false,
+        chunkModules: false
+      }) + '\n\n'
+    )
+
+    if (stats.hasErrors()) {
+      console.log(chalk.red('  Build failed with errors.\n'))
+      process.exit(1)
+    }
+
+    console.log(chalk.cyan('  Build complete.\n'))
+    console.log(
+      chalk.yellow(
+        '  Tip: built files are meant to be served over an HTTP server.\n' +
+          "  Opening index.html over file:// won't work.\n"
+      )
+    )
+  })
+})

+ 64 - 0
onlineexam-admin/build/check-versions.js

@@ -0,0 +1,64 @@
+'use strict'
+const chalk = require('chalk')
+const semver = require('semver')
+const packageConfig = require('../package.json')
+const shell = require('shelljs')
+
+function exec(cmd) {
+  return require('child_process')
+    .execSync(cmd)
+    .toString()
+    .trim()
+}
+
+const versionRequirements = [
+  {
+    name: 'node',
+    currentVersion: semver.clean(process.version),
+    versionRequirement: packageConfig.engines.node
+  }
+]
+
+if (shell.which('npm')) {
+  versionRequirements.push({
+    name: 'npm',
+    currentVersion: exec('npm --version'),
+    versionRequirement: packageConfig.engines.npm
+  })
+}
+
+module.exports = function() {
+  const warnings = []
+
+  for (let i = 0; i < versionRequirements.length; i++) {
+    const mod = versionRequirements[i]
+
+    if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
+      warnings.push(
+        mod.name +
+          ': ' +
+          chalk.red(mod.currentVersion) +
+          ' should be ' +
+          chalk.green(mod.versionRequirement)
+      )
+    }
+  }
+
+  if (warnings.length) {
+    console.log('')
+    console.log(
+      chalk.yellow(
+        'To use this template, you must update following to modules:'
+      )
+    )
+    console.log()
+
+    for (let i = 0; i < warnings.length; i++) {
+      const warning = warnings[i]
+      console.log('  ' + warning)
+    }
+
+    console.log()
+    process.exit(1)
+  }
+}

BIN
onlineexam-admin/build/logo.png


+ 108 - 0
onlineexam-admin/build/utils.js

@@ -0,0 +1,108 @@
+'use strict'
+const path = require('path')
+const config = require('../config')
+const MiniCssExtractPlugin = require('mini-css-extract-plugin')
+const packageConfig = require('../package.json')
+
+exports.assetsPath = function(_path) {
+  const assetsSubDirectory =
+    process.env.NODE_ENV === 'production'
+      ? config.build.assetsSubDirectory
+      : config.dev.assetsSubDirectory
+
+  return path.posix.join(assetsSubDirectory, _path)
+}
+
+exports.cssLoaders = function(options) {
+  options = options || {}
+
+  const cssLoader = {
+    loader: 'css-loader',
+    options: {
+      sourceMap: options.sourceMap
+    }
+  }
+
+  const postcssLoader = {
+    loader: 'postcss-loader',
+    options: {
+      sourceMap: options.sourceMap
+    }
+  }
+
+  // generate loader string to be used with extract text plugin
+  function generateLoaders(loader, loaderOptions) {
+    const loaders = []
+
+    // Extract CSS when that option is specified
+    // (which is the case during production build)
+    if (options.extract) {
+      loaders.push(MiniCssExtractPlugin.loader)
+    } else {
+      loaders.push('vue-style-loader')
+    }
+
+    loaders.push(cssLoader)
+
+    if (options.usePostCSS) {
+      loaders.push(postcssLoader)
+    }
+
+    if (loader) {
+      loaders.push({
+        loader: loader + '-loader',
+        options: Object.assign({}, loaderOptions, {
+          sourceMap: options.sourceMap
+        })
+      })
+    }
+
+    return loaders
+  }
+  // https://vue-loader.vuejs.org/en/configurations/extract-css.html
+  return {
+    css: generateLoaders(),
+    postcss: generateLoaders(),
+    less: generateLoaders('less'),
+    sass: generateLoaders('sass', {
+      indentedSyntax: true
+    }),
+    scss: generateLoaders('sass'),
+    stylus: generateLoaders('stylus'),
+    styl: generateLoaders('stylus')
+  }
+}
+
+// Generate loaders for standalone style files (outside of .vue)
+exports.styleLoaders = function(options) {
+  const output = []
+  const loaders = exports.cssLoaders(options)
+
+  for (const extension in loaders) {
+    const loader = loaders[extension]
+    output.push({
+      test: new RegExp('\\.' + extension + '$'),
+      use: loader
+    })
+  }
+
+  return output
+}
+
+exports.createNotifierCallback = () => {
+  const notifier = require('node-notifier')
+
+  return (severity, errors) => {
+    if (severity !== 'error') return
+
+    const error = errors[0]
+    const filename = error.file && error.file.split('!').pop()
+
+    notifier.notify({
+      title: packageConfig.name,
+      message: severity + ': ' + error.name,
+      subtitle: filename || '',
+      icon: path.join(__dirname, 'logo.png')
+    })
+  }
+}

+ 5 - 0
onlineexam-admin/build/vue-loader.conf.js

@@ -0,0 +1,5 @@
+'use strict'
+
+module.exports = {
+  //You can set the vue-loader configuration by yourself.
+}

+ 108 - 0
onlineexam-admin/build/webpack.base.conf.js

@@ -0,0 +1,108 @@
+'use strict'
+const path = require('path')
+const utils = require('./utils')
+const config = require('../config')
+const { VueLoaderPlugin } = require('vue-loader')
+const vueLoaderConfig = require('./vue-loader.conf')
+
+function resolve(dir) {
+  return path.join(__dirname, '..', dir)
+}
+
+const createLintingRule = () => ({
+  test: /\.(js|vue)$/,
+  loader: 'eslint-loader',
+  enforce: 'pre',
+  include: [resolve('src'), resolve('test')],
+  options: {
+    formatter: require('eslint-friendly-formatter'),
+    emitWarning: !config.dev.showEslintErrorsInOverlay
+  }
+})
+
+module.exports = {
+  context: path.resolve(__dirname, '../'),
+  entry: {
+    app: './src/main.js'
+  },
+  output: {
+    path: config.build.assetsRoot,
+    filename: '[name].js',
+    publicPath:
+      process.env.NODE_ENV === 'production'
+        ? config.build.assetsPublicPath
+        : config.dev.assetsPublicPath
+  },
+  resolve: {
+    extensions: ['.js', '.vue', '.json'],
+    alias: {
+      '@': resolve('src')
+    }
+  },
+  module: {
+    rules: [
+      ...(config.dev.useEslint ? [createLintingRule()] : []),
+      {
+        test: /\.vue$/,
+        loader: 'vue-loader',
+        options: vueLoaderConfig
+      },
+      {
+        test: /\.js$/,
+        loader: 'babel-loader',
+        include: [
+          resolve('src'),
+          resolve('test'),
+          resolve('mock'),
+          resolve('node_modules/webpack-dev-server/client')
+        ]
+      },
+      {
+        test: /\.svg$/,
+        loader: 'svg-sprite-loader',
+        include: [resolve('src/icons')],
+        options: {
+          symbolId: 'icon-[name]'
+        }
+      },
+      {
+        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
+        loader: 'url-loader',
+        exclude: [resolve('src/icons')],
+        options: {
+          limit: 10000,
+          name: utils.assetsPath('img/[name].[hash:7].[ext]')
+        }
+      },
+      {
+        test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
+        loader: 'url-loader',
+        options: {
+          limit: 10000,
+          name: utils.assetsPath('media/[name].[hash:7].[ext]')
+        }
+      },
+      {
+        test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
+        loader: 'url-loader',
+        options: {
+          limit: 10000,
+          name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
+        }
+      }
+    ]
+  },
+  plugins: [new VueLoaderPlugin()],
+  node: {
+    // prevent webpack from injecting useless setImmediate polyfill because Vue
+    // source contains it (although only uses it if it's native).
+    setImmediate: false,
+    // prevent webpack from injecting mocks to Node native modules
+    // that does not make sense for the client
+    dgram: 'empty',
+    fs: 'empty',
+    net: 'empty',
+    tls: 'empty',
+    child_process: 'empty'
+  }
+}

+ 95 - 0
onlineexam-admin/build/webpack.dev.conf.js

@@ -0,0 +1,95 @@
+'use strict'
+const path = require('path')
+const utils = require('./utils')
+const webpack = require('webpack')
+const config = require('../config')
+const merge = require('webpack-merge')
+const baseWebpackConfig = require('./webpack.base.conf')
+const HtmlWebpackPlugin = require('html-webpack-plugin')
+const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
+const portfinder = require('portfinder')
+
+function resolve(dir) {
+  return path.join(__dirname, '..', dir)
+}
+
+const HOST = process.env.HOST
+const PORT = process.env.PORT && Number(process.env.PORT)
+
+const devWebpackConfig = merge(baseWebpackConfig, {
+  mode: 'development',
+  module: {
+    rules: utils.styleLoaders({
+      sourceMap: config.dev.cssSourceMap,
+      usePostCSS: true
+    })
+  },
+  // cheap-module-eval-source-map is faster for development
+  devtool: config.dev.devtool,
+
+  // these devServer options should be customized in /config/index.js
+  devServer: {
+    clientLogLevel: 'warning',
+    historyApiFallback: true,
+    hot: true,
+    compress: true,
+    host: HOST || config.dev.host,
+    port: PORT || config.dev.port,
+    open: config.dev.autoOpenBrowser,
+    overlay: config.dev.errorOverlay
+      ? { warnings: false, errors: true }
+      : false,
+    publicPath: config.dev.assetsPublicPath,
+    proxy: config.dev.proxyTable,
+    quiet: true, // necessary for FriendlyErrorsPlugin
+    watchOptions: {
+      poll: config.dev.poll
+    }
+  },
+  plugins: [
+    new webpack.DefinePlugin({
+      'process.env': require('../config/dev.env')
+    }),
+    new webpack.HotModuleReplacementPlugin(),
+    // https://github.com/ampedandwired/html-webpack-plugin
+    new HtmlWebpackPlugin({
+      filename: 'index.html',
+      template: 'index.html',
+      inject: true,
+      favicon: resolve('favicon.ico'),
+      title: '在线考试系统管理员端'
+    })
+  ]
+})
+
+module.exports = new Promise((resolve, reject) => {
+  portfinder.basePort = process.env.PORT || config.dev.port
+  portfinder.getPort((err, port) => {
+    if (err) {
+      reject(err)
+    } else {
+      // publish the new Port, necessary for e2e tests
+      process.env.PORT = port
+      // add port to devServer config
+      devWebpackConfig.devServer.port = port
+
+      // Add FriendlyErrorsPlugin
+      devWebpackConfig.plugins.push(
+        new FriendlyErrorsPlugin({
+          compilationSuccessInfo: {
+            messages: [
+              `Your application is running here: http://${
+                devWebpackConfig.devServer.host
+              }:${port}`
+            ]
+          },
+          onErrors: config.dev.notifyOnErrors
+            ? utils.createNotifierCallback()
+            : undefined
+        })
+      )
+
+      resolve(devWebpackConfig)
+    }
+  })
+})

+ 177 - 0
onlineexam-admin/build/webpack.prod.conf.js

@@ -0,0 +1,177 @@
+'use strict'
+const path = require('path')
+const utils = require('./utils')
+const webpack = require('webpack')
+const config = require('../config')
+const merge = require('webpack-merge')
+const baseWebpackConfig = require('./webpack.base.conf')
+const CopyWebpackPlugin = require('copy-webpack-plugin')
+const HtmlWebpackPlugin = require('html-webpack-plugin')
+const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin')
+const MiniCssExtractPlugin = require('mini-css-extract-plugin')
+const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
+const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
+
+function resolve(dir) {
+  return path.join(__dirname, '..', dir)
+}
+
+const env = require('../config/prod.env')
+
+// For NamedChunksPlugin
+const seen = new Set()
+const nameLength = 4
+
+const webpackConfig = merge(baseWebpackConfig, {
+  mode: 'production',
+  module: {
+    rules: utils.styleLoaders({
+      sourceMap: config.build.productionSourceMap,
+      extract: true,
+      usePostCSS: true
+    })
+  },
+  devtool: config.build.productionSourceMap ? config.build.devtool : false,
+  output: {
+    path: config.build.assetsRoot,
+    filename: utils.assetsPath('js/[name].[chunkhash:8].js'),
+    chunkFilename: utils.assetsPath('js/[name].[chunkhash:8].js')
+  },
+  plugins: [
+    // http://vuejs.github.io/vue-loader/en/workflow/production.html
+    new webpack.DefinePlugin({
+      'process.env': env
+    }),
+    // extract css into its own file
+    new MiniCssExtractPlugin({
+      filename: utils.assetsPath('css/[name].[contenthash:8].css'),
+      chunkFilename: utils.assetsPath('css/[name].[contenthash:8].css')
+    }),
+    // generate dist index.html with correct asset hash for caching.
+    // you can customize output by editing /index.html
+    // see https://github.com/ampedandwired/html-webpack-plugin
+    new HtmlWebpackPlugin({
+      filename: config.build.index,
+      template: 'index.html',
+      inject: true,
+      favicon: resolve('favicon.ico'),
+      title: '在线考试系统管理员端',
+      minify: {
+        removeComments: true,
+        collapseWhitespace: true,
+        removeAttributeQuotes: true
+        // more options:
+        // https://github.com/kangax/html-minifier#options-quick-reference
+      }
+      // default sort mode uses toposort which cannot handle cyclic deps
+      // in certain cases, and in webpack 4, chunk order in HTML doesn't
+      // matter anyway
+    }),
+    new ScriptExtHtmlWebpackPlugin({
+      //`runtime` must same as runtimeChunk name. default is `runtime`
+      inline: /runtime\..*\.js$/
+    }),
+    // keep chunk.id stable when chunk has no name
+    new webpack.NamedChunksPlugin(chunk => {
+      if (chunk.name) {
+        return chunk.name
+      }
+      const modules = Array.from(chunk.modulesIterable)
+      if (modules.length > 1) {
+        const hash = require('hash-sum')
+        const joinedHash = hash(modules.map(m => m.id).join('_'))
+        let len = nameLength
+        while (seen.has(joinedHash.substr(0, len))) len++
+        seen.add(joinedHash.substr(0, len))
+        return `chunk-${joinedHash.substr(0, len)}`
+      } else {
+        return modules[0].id
+      }
+    }),
+    // keep module.id stable when vender modules does not change
+    new webpack.HashedModuleIdsPlugin(),
+    // copy custom static assets
+    new CopyWebpackPlugin([
+      {
+        from: path.resolve(__dirname, '../static'),
+        to: config.build.assetsSubDirectory,
+        ignore: ['.*']
+      }
+    ])
+  ],
+  optimization: {
+    splitChunks: {
+      chunks: 'all',
+      cacheGroups: {
+        libs: {
+          name: 'chunk-libs',
+          test: /[\\/]node_modules[\\/]/,
+          priority: 10,
+          chunks: 'initial' // 只打包初始时依赖的第三方
+        },
+        elementUI: {
+          name: 'chunk-elementUI', // 单独将 elementUI 拆包
+          priority: 20, // 权重要大于 libs 和 app 不然会被打包进 libs 或者 app
+          test: /[\\/]node_modules[\\/]element-ui[\\/]/
+        }
+      }
+    },
+    runtimeChunk: 'single',
+    minimizer: [
+      new UglifyJsPlugin({
+        uglifyOptions: {
+          mangle: {
+            safari10: true
+          }
+        },
+        sourceMap: config.build.productionSourceMap,
+        cache: true,
+        parallel: true
+      }),
+      // Compress extracted CSS. We are using this plugin so that possible
+      // duplicated CSS from different components can be deduped.
+      new OptimizeCSSAssetsPlugin()
+    ]
+  }
+})
+
+if (config.build.productionGzip) {
+  const CompressionWebpackPlugin = require('compression-webpack-plugin')
+
+  webpackConfig.plugins.push(
+    new CompressionWebpackPlugin({
+      algorithm: 'gzip',
+      test: new RegExp(
+        '\\.(' + config.build.productionGzipExtensions.join('|') + ')$'
+      ),
+      threshold: 10240,
+      minRatio: 0.8
+    })
+  )
+}
+
+if (config.build.generateAnalyzerReport || config.build.bundleAnalyzerReport) {
+  const BundleAnalyzerPlugin = require('webpack-bundle-analyzer')
+    .BundleAnalyzerPlugin
+
+  if (config.build.bundleAnalyzerReport) {
+    webpackConfig.plugins.push(
+      new BundleAnalyzerPlugin({
+        analyzerPort: 8080,
+        generateStatsFile: false
+      })
+    )
+  }
+
+  if (config.build.generateAnalyzerReport) {
+    webpackConfig.plugins.push(
+      new BundleAnalyzerPlugin({
+        analyzerMode: 'static',
+        reportFilename: 'bundle-report.html',
+        openAnalyzer: false
+      })
+    )
+  }
+}
+
+module.exports = webpackConfig

+ 9 - 0
onlineexam-admin/config/dev.env.js

@@ -0,0 +1,9 @@
+'use strict'
+const merge = require('webpack-merge')
+const prodEnv = require('./prod.env')
+
+module.exports = merge(prodEnv, {
+  NODE_ENV: '"development"',
+  BASE_API: '"/api"',
+  BASE_WEBSOCKET: '"ws://localhost:8080/api/websocket/"'
+})

+ 96 - 0
onlineexam-admin/config/index.js

@@ -0,0 +1,96 @@
+'use strict'
+// Template version: 1.2.6
+// see http://vuejs-templates.github.io/webpack for documentation.
+
+const path = require('path')
+
+module.exports = {
+  dev: {
+    // Paths
+    assetsSubDirectory: 'static',
+    assetsPublicPath: '/',
+    // 代理配置表,在这里可以配置特定的请求代理到对应的API接口
+    proxyTable: {
+      '/api': { // 匹配所有以 '/api'开头的请求路径
+        target: 'http://localhost:8080', // 代理目标的基础路径
+        // secure: false,  // 如果是https接口,需要配置这个参数
+        changeOrigin: true // 支持跨域
+        // pathRewrite: { // 重写路径: 去掉路径中开头的'/api'
+        //   '^/api': '/api'
+        // }
+      }
+    },
+
+    // Various Dev Server settings
+    host: 'localhost', // can be overwritten by process.env.HOST
+    port: 9538, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
+    autoOpenBrowser: true,
+    errorOverlay: true,
+    notifyOnErrors: false,
+    poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-
+
+    // Use Eslint Loader?
+    // If true, your code will be linted during bundling and
+    // linting errors and warnings will be shown in the console.
+    useEslint: true,
+    // If true, eslint errors and warnings will also be shown in the error overlay
+    // in the browser.
+    showEslintErrorsInOverlay: false,
+
+    /**
+     * Source Maps
+     */
+
+    // https://webpack.js.org/configuration/devtool/#development
+    devtool: 'cheap-source-map',
+
+    // CSS Sourcemaps off by default because relative paths are "buggy"
+    // with this option, according to the CSS-Loader README
+    // (https://github.com/webpack/css-loader#sourcemaps)
+    // In our experience, they generally work as expected,
+    // just be aware of this issue when enabling this option.
+    cssSourceMap: false
+  },
+
+  build: {
+    // Template for index.html
+    index: path.resolve(__dirname, '../dist/index.html'),
+
+    // Paths
+    assetsRoot: path.resolve(__dirname, '../dist'),
+    assetsSubDirectory: 'static',
+
+    /**
+     * You can set by youself according to actual condition
+     * You will need to set this if you plan to deploy your site under a sub path,
+     * for example GitHub pages. If you plan to deploy your site to https://foo.github.io/bar/,
+     * then assetsPublicPath should be set to "/bar/".
+     * In most cases please use '/' !!!
+     */
+    assetsPublicPath: './',
+
+    /**
+     * Source Maps
+     */
+
+    productionSourceMap: false,
+    // https://webpack.js.org/configuration/devtool/#production
+    devtool: 'source-map',
+
+    // Gzip off by default as many popular static hosts such as
+    // Surge or Netlify already gzip all static assets for you.
+    // Before setting to `true`, make sure to:
+    // npm install --save-dev compression-webpack-plugin
+    productionGzip: false,
+    productionGzipExtensions: ['js', 'css'],
+
+    // Run the build command with an extra argument to
+    // View the bundle analyzer report after build finishes:
+    // `npm run build --report`
+    // Set to `true` or `false` to always turn it on or off
+    bundleAnalyzerReport: process.env.npm_config_report || false,
+
+    // `npm run build:prod --generate_report`
+    generateAnalyzerReport: process.env.npm_config_generate_report || false
+  }
+}

+ 6 - 0
onlineexam-admin/config/prod.env.js

@@ -0,0 +1,6 @@
+'use strict'
+module.exports = {
+  NODE_ENV: '"production"',
+  BASE_API: '"http://maweitao.top/oes/api"',
+  BASE_WEBSOCKET: '"ws://maweitao.top:8080/oes/api/websocket/"'
+}

BIN
onlineexam-admin/favicon.ico


+ 14 - 0
onlineexam-admin/index.html

@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width,initial-scale=1.0">
+    <title>在线考试系统管理员端</title>
+    <!-- 引入阿里巴巴图标矢量库Iconfont -->
+    <link rel="stylesheet" href="http://at.alicdn.com/t/font_1150421_yzg7b4u7s2m.css">
+  </head>
+  <body>
+    <div id="app"></div>
+    <!-- built files will be auto injected -->
+  </body>
+</html>

+ 26 - 0
onlineexam-admin/mock/index.js

@@ -0,0 +1,26 @@
+import Mock from 'mockjs'
+import userAPI from './user'
+import tableAPI from './table'
+
+// Fix an issue with setting withCredentials = true, cross-domain request lost cookies
+// https://github.com/nuysoft/Mock/issues/300
+Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send
+Mock.XHR.prototype.send = function() {
+  if (this.custom.xhr) {
+    this.custom.xhr.withCredentials = this.withCredentials || false
+  }
+  this.proxy_send(...arguments)
+}
+// Mock.setup({
+//   timeout: '350-600'
+// })
+
+// User
+Mock.mock(/\/user\/login/, 'post', userAPI.login)
+Mock.mock(/\/user\/info/, 'get', userAPI.getInfo)
+Mock.mock(/\/user\/logout/, 'post', userAPI.logout)
+
+// Table
+Mock.mock(/\/table\/list/, 'get', tableAPI.list)
+
+export default Mock

+ 20 - 0
onlineexam-admin/mock/table.js

@@ -0,0 +1,20 @@
+import Mock from 'mockjs'
+
+export default {
+  list: () => {
+    const items = Mock.mock({
+      'items|30': [{
+        id: '@id',
+        title: '@sentence(10, 20)',
+        'status|1': ['published', 'draft', 'deleted'],
+        author: 'name',
+        display_time: '@datetime',
+        pageviews: '@integer(300, 5000)'
+      }]
+    })
+    return {
+      code: 20000,
+      data: items
+    }
+  }
+}

+ 64 - 0
onlineexam-admin/mock/user.js

@@ -0,0 +1,64 @@
+import { param2Obj } from './utils'
+
+const tokens = {
+  admin: {
+    token: 'admin-token'
+  },
+  editor: {
+    token: 'editor-token'
+  }
+}
+
+const users = {
+  'admin-token': {
+    roles: ['admin'],
+    introduction: 'I am a super administrator',
+    avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
+    name: 'Super Admin'
+  },
+  'editor-token': {
+    roles: ['editor'],
+    introduction: 'I am an editor',
+    avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
+    name: 'Normal Editor'
+  }
+}
+
+export default {
+  login: res => {
+    const { username } = JSON.parse(res.body)
+    const data = tokens[username]
+
+    if (data) {
+      return {
+        code: 20000,
+        data
+      }
+    }
+    return {
+      code: 60204,
+      message: 'Account and password are incorrect.'
+    }
+  },
+  getInfo: res => {
+    const { token } = param2Obj(res.url)
+    const info = users[token]
+
+    if (info) {
+      return {
+        code: 20000,
+        data: info
+      }
+    }
+    return {
+      code: 50008,
+      message: 'Login failed, unable to get user details.'
+    }
+  },
+  logout: () => {
+    return {
+      code: 20000,
+      data: 'success'
+    }
+  }
+}

+ 14 - 0
onlineexam-admin/mock/utils.js

@@ -0,0 +1,14 @@
+export function param2Obj(url) {
+  const search = url.split('?')[1]
+  if (!search) {
+    return {}
+  }
+  return JSON.parse(
+    '{"' +
+      decodeURIComponent(search)
+        .replace(/"/g, '\\"')
+        .replace(/&/g, '","')
+        .replace(/=/g, '":"') +
+      '"}'
+  )
+}

+ 98 - 0
onlineexam-admin/package.json

@@ -0,0 +1,98 @@
+{
+  "name": "vue-admin-template",
+  "version": "3.9.0",
+  "license": "MIT",
+  "description": "A vue admin template with Element UI & axios & iconfont & permission control & lint",
+  "author": "Pan <panfree23@gmail.com>",
+  "scripts": {
+    "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
+    "start": "npm run dev",
+    "build": "node build/build.js",
+    "build:report": "npm_config_report=true npm run build",
+    "lint": "eslint --ext .js,.vue src",
+    "test": "npm run lint",
+    "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml"
+  },
+  "dependencies": {
+    "axios": "0.18.0",
+    "date-fns": "^1.30.1",
+    "echarts": "^4.2.1",
+    "element-ui": "^2.7.2",
+    "file-saver": "^2.0.1",
+    "jquery": "^3.4.0",
+    "js-cookie": "2.2.0",
+    "mockjs": "1.0.1-beta3",
+    "normalize.css": "7.0.0",
+    "nprogress": "0.2.0",
+    "screenfull": "^4.2.0",
+    "script-loader": "^0.7.2",
+    "stylus": "^0.54.5",
+    "stylus-loader": "^3.0.2",
+    "v-viewer": "^1.4.0",
+    "vue": "2.5.17",
+    "vue-native-websocket": "^2.0.13",
+    "vue-router": "3.0.1",
+    "vuex": "3.0.1",
+    "xlsx": "^0.14.2"
+  },
+  "devDependencies": {
+    "autoprefixer": "8.5.0",
+    "babel-core": "6.26.0",
+    "babel-eslint": "8.2.6",
+    "babel-helper-vue-jsx-merge-props": "2.0.3",
+    "babel-loader": "7.1.5",
+    "babel-plugin-syntax-jsx": "6.18.0",
+    "babel-plugin-transform-runtime": "6.23.0",
+    "babel-plugin-transform-vue-jsx": "3.7.0",
+    "babel-preset-env": "1.7.0",
+    "babel-preset-stage-2": "6.24.1",
+    "chalk": "2.4.1",
+    "compression-webpack-plugin": "2.0.0",
+    "copy-webpack-plugin": "4.5.2",
+    "css-loader": "1.0.0",
+    "eslint": "4.19.1",
+    "eslint-friendly-formatter": "4.0.1",
+    "eslint-loader": "2.0.0",
+    "eslint-plugin-vue": "4.7.1",
+    "eventsource-polyfill": "0.9.6",
+    "file-loader": "1.1.11",
+    "friendly-errors-webpack-plugin": "1.7.0",
+    "html-webpack-plugin": "4.0.0-alpha",
+    "mini-css-extract-plugin": "0.4.1",
+    "node-notifier": "5.2.1",
+    "node-sass": "^4.7.2",
+    "optimize-css-assets-webpack-plugin": "5.0.0",
+    "ora": "3.0.0",
+    "path-to-regexp": "2.4.0",
+    "portfinder": "1.0.16",
+    "postcss-import": "12.0.0",
+    "postcss-loader": "2.1.6",
+    "postcss-url": "7.3.2",
+    "rimraf": "2.6.2",
+    "sass-loader": "7.0.3",
+    "script-ext-html-webpack-plugin": "2.0.1",
+    "semver": "5.5.0",
+    "shelljs": "0.8.2",
+    "svg-sprite-loader": "3.8.0",
+    "svgo": "1.0.5",
+    "uglifyjs-webpack-plugin": "1.2.7",
+    "url-loader": "1.0.1",
+    "vue-loader": "15.3.0",
+    "vue-style-loader": "4.1.2",
+    "vue-template-compiler": "2.5.17",
+    "webpack": "4.16.5",
+    "webpack-bundle-analyzer": "2.13.1",
+    "webpack-cli": "3.1.0",
+    "webpack-dev-server": "3.1.14",
+    "webpack-merge": "4.1.4"
+  },
+  "engines": {
+    "node": ">= 6.0.0",
+    "npm": ">= 3.0.0"
+  },
+  "browserslist": [
+    "> 1%",
+    "last 2 versions",
+    "not ie <= 8"
+  ]
+}

+ 17 - 0
onlineexam-admin/src/App.vue

@@ -0,0 +1,17 @@
+<template>
+  <div id="app">
+    <router-view/>
+  </div>
+</template>
+
+<script>
+
+export default {
+  name: 'App'
+}
+</script>
+<style>
+  path {
+    fill: inherit !important
+  }
+</style>

+ 54 - 0
onlineexam-admin/src/api/bankManage.js

@@ -0,0 +1,54 @@
+import ajax from '@/config/ajax'
+const BASE_URL = process.env.BASE_API + '/admin'
+
+// 获取全部单选题信息
+export const reqGetSingleList = () => ajax(BASE_URL + '/getSingleList')
+// 获取搜素单选题信息
+export const reqSearchSingleList = (content, langId, composeFlag) => ajax(BASE_URL + '/searchSingleList', { content, langId, composeFlag })
+// 删除单选题
+export const reqDeleteSingle = (singleId) => ajax(BASE_URL + '/deleteSingle', { singleId }, 'POST')
+// 添加单选题题目
+export const reqInsertSingleInfo = (temp) => ajax(BASE_URL + '/insertSingleInfo', temp, 'POST')
+// 更新单选题题目
+export const reqUpdateSingleInfo = (temp) => ajax(BASE_URL + '/updateSingleInfo', temp, 'POST')
+// 添加导入单选题Excel文件
+export const reqInsertSingleList = (singleList) => ajax(BASE_URL + '/insertSingleList', { singleList }, 'POST')
+// ---------------------------------------------------------------------------------------------------------------
+// 获取全部多选题信息
+export const reqGetMultipleList = () => ajax(BASE_URL + '/getMultipleList')
+// 获取搜素多选题信息
+export const reqSearchMultipleList = (content, langId, composeFlag) => ajax(BASE_URL + '/searchMultipleList', { content, langId, composeFlag })
+// 删除多选题
+export const reqDeleteMultiple = (multipleId) => ajax(BASE_URL + '/deleteMultiple', { multipleId }, 'POST')
+// 添加多选题题目
+export const reqInsertMultipleInfo = (temp) => ajax(BASE_URL + '/insertMultipleInfo', temp, 'POST')
+// 更新多选题题目
+export const reqUpdateMultipleInfo = (temp) => ajax(BASE_URL + '/updateMultipleInfo', temp, 'POST')
+// 添加导入多选题Excel文件
+export const reqInsertMultipleList = (multipleList) => ajax(BASE_URL + '/insertMultipleList', { multipleList }, 'POST')
+// ---------------------------------------------------------------------------------------------------------------
+// 获取全部判断题信息
+export const reqGetJudgeList = () => ajax(BASE_URL + '/getJudgeList')
+// 获取搜素判断题信息
+export const reqSearchJudgeList = (content, langId, composeFlag) => ajax(BASE_URL + '/searchJudgeList', { content, langId, composeFlag })
+// 删除判断题
+export const reqDeleteJudge = (judgeId) => ajax(BASE_URL + '/deleteJudge', { judgeId }, 'POST')
+// 添加判断题题目
+export const reqInsertJudgeInfo = (temp) => ajax(BASE_URL + '/insertJudgeInfo', temp, 'POST')
+// 更新判断题题目
+export const reqUpdateJudgeInfo = (temp) => ajax(BASE_URL + '/updateJudgeInfo', temp, 'POST')
+// 添加导入判断题Excel文件
+export const reqInsertJudgeList = (judgeList) => ajax(BASE_URL + '/insertJudgeList', { judgeList }, 'POST')
+// ---------------------------------------------------------------------------------------------------------------
+// 获取全部填空题信息
+export const reqGetFillList = () => ajax(BASE_URL + '/getFillList')
+// 获取搜素填空题信息
+export const reqSearchFillList = (content, langId, composeFlag) => ajax(BASE_URL + '/searchFillList', { content, langId, composeFlag })
+// 删除填空题
+export const reqDeleteFill = (fillId) => ajax(BASE_URL + '/deleteFill', { fillId }, 'POST')
+// 添加填空题题目
+export const reqInsertFillInfo = (temp) => ajax(BASE_URL + '/insertFillInfo', temp, 'POST')
+// 更新填空题题目
+export const reqUpdateFillInfo = (temp) => ajax(BASE_URL + '/updateFillInfo', temp, 'POST')
+// 添加导入填空题Excel文件
+export const reqInsertFillList = (fillList) => ajax(BASE_URL + '/inserFillList', { fillList }, 'POST')

+ 14 - 0
onlineexam-admin/src/api/feedback.js

@@ -0,0 +1,14 @@
+import ajax from '@/config/ajax'
+const BASE_URL = process.env.BASE_API + '/admin'
+
+// 获取未回复留言条数
+export const reqGetUnReplyCount = () => ajax(BASE_URL + '/getUnReplyCount')
+// 获取全部留言信息
+export const reqGetFeedbacksList = () => ajax(BASE_URL + '/getFeedbacksList')
+// 获取搜索留言信息
+export const reqSearchFeedbacksList = (feedbackContent, stuName, admAnswer, admName, feedbackStatus) => ajax(BASE_URL + '/searchFeedbacksList',
+  { feedbackContent, stuName, admAnswer, admName, feedbackStatus })
+// 删除留言
+export const reqDeleteFeedback = (feedbackId) => ajax(BASE_URL + '/deleteFeedback', { feedbackId }, 'POST')
+// 回复留言
+export const reqReplyFeedback = (temp) => ajax(BASE_URL + '/replyFeedback', temp, 'POST')

+ 10 - 0
onlineexam-admin/src/api/login.js

@@ -0,0 +1,10 @@
+/*
+  与后台交互模块 (依赖已封装的ajax函数)
+ */
+import ajax from '@/config/ajax'
+const BASE_URL = process.env.BASE_API + '/admin'
+
+// 校验管理员登录
+export const reqLogin = (ano, admPsw) => ajax(BASE_URL + '/adminLogin', { ano, admPsw }, 'POST')
+// 请求退出登录
+export const reqLogOut = () => ajax(BASE_URL + '/adminLogOut')

+ 13 - 0
onlineexam-admin/src/api/notice.js

@@ -0,0 +1,13 @@
+import ajax from '@/config/ajax'
+const BASE_URL = process.env.BASE_API + '/admin'
+
+// 获取全部公告信息
+export const reqGetNoticesList = () => ajax(BASE_URL + '/getNoticesList')
+// 获取搜索公告信息
+export const reqSearchNoticesList = (noticeContent, teaName) => ajax(BASE_URL + '/searchNoticesList', { noticeContent, teaName })
+// 请求添加公告信息
+export const reqInsertNoticeInfo = (temp) => ajax(BASE_URL + '/insertNoticeInfo', temp, 'POST')
+// 请求更新公告内容
+export const reqUpdateNoticeInfo = (row) => ajax(BASE_URL + '/updateNoticeInfo', row, 'POST')
+// 请求删除公告
+export const reqDeleteNotice = (noticeId) => ajax(BASE_URL + '/deleteNotice', { noticeId }, 'POST')

+ 17 - 0
onlineexam-admin/src/api/paper.js

@@ -0,0 +1,17 @@
+import ajax from '@/config/ajax'
+const BASE_URL = process.env.BASE_API + '/admin'
+
+// 获取全部公告信息
+export const reqGetPapersList = () => ajax(BASE_URL + '/getTeacherPapersList')
+// 获取搜素公告信息
+export const reqSearchPapersList = (paperName, langId, paperType) => ajax(BASE_URL + '/searchPapersList', { paperName, langId, paperType })
+// 请求删除试卷
+export const reqDeletePaper = (paperId) => ajax(BASE_URL + '/deletePaper', { paperId }, 'POST')
+// 请求获取选中试卷问题详情
+export const reqPaperQueDetailByPaperId = (paperId, totalNum) => ajax(BASE_URL + '/getPaperQueDetailByPaperId', { paperId, totalNum })
+// 请求随机组卷,插入试卷数据,即发布试卷
+export const reqRandomInsertPaperInfo = (temp) => ajax(BASE_URL + '/randomInsertPaperInfo', temp, 'POST')
+// 请求固定组卷,插入试卷数据,即发布试卷
+export const reqFixedInsertPaperInfo = (temp) => ajax(BASE_URL + '/fixedInsertPaperInfo', temp, 'POST')
+// 请求通过langId获取科目下的所有问题
+export const reqPaperQueDetailByLangId = (langId) => ajax(BASE_URL + '/getPaperQueDetailByLangId', { langId })

+ 13 - 0
onlineexam-admin/src/api/rotationImg.js

@@ -0,0 +1,13 @@
+import ajax from '@/config/ajax'
+const BASE_URL = process.env.BASE_API + '/admin'
+
+// 获取全部轮播图信息
+export const reqGetRotationImgsList = () => ajax(BASE_URL + '/getRotationImgsList')
+// 获取搜索轮播图信息
+export const reqSearchRotationImgsList = (imgTitle, admName) => ajax(BASE_URL + '/searchRotationImgsList', { imgTitle, admName })
+// 请求添加轮播图信息
+export const reqInsertRotationImgInfo = (temp) => ajax(BASE_URL + '/insertRotationImgInfo', temp, 'POST')
+// 请求更新轮播图信息
+export const reqUpdateRotationImgInfo = (row) => ajax(BASE_URL + '/updateRotationImgInfo', row, 'POST')
+// 请求删除轮播图
+export const reqDeleteRotationImg = (imgId) => ajax(BASE_URL + '/deleteRotationImgInfo', { imgId }, 'POST')

+ 26 - 0
onlineexam-admin/src/api/student.js

@@ -0,0 +1,26 @@
+import ajax from '@/config/ajax'
+const BASE_URL = process.env.BASE_API + '/teacher'
+
+// 获取全部学生信息
+export const reqGetStudentsList = () => ajax(BASE_URL + '/getStudentsList')
+// 请求更新学生登录状态
+export const reqUpdateStudentInfo = (row) => ajax(BASE_URL + '/updateStudentInfo', row, 'POST')
+// 请求搜索学生信息
+export const reqSearchStudentsList = (sno, stuName, stuSex) => ajax(BASE_URL + '/searchStudentInfo', { sno, stuName, stuSex })
+// 请求添加学生信息
+export const reqInsertStudentInfo = (temp) => ajax(BASE_URL + '/insertStudentInfo', temp, 'POST')
+
+// 获取全部成绩信息
+export const reqGetScoresList = () => ajax(BASE_URL + '/getScoresList')
+// 请求删除成绩
+export const reqDeleteScore = (row) => ajax(BASE_URL + '/deleteScore', row, 'POST')
+// 请求搜索成绩信息
+export const reqSearchScoresList = (sno, paperId) => ajax(BASE_URL + '/searchScoresList', { sno, paperId })
+
+// 获取全部已发布试卷信息
+export const reqGetPapersList = () => ajax(BASE_URL + '/getPapersList')
+// 请求获取成绩图标数据
+export const reqGetChartData = (paperId) => ajax(BASE_URL + '/getChartData', { paperId })
+
+// 插入上传学生信息数据
+export const reqInsertStudentInfoList = (studentList) => ajax(BASE_URL + '/insertStudentInfoList', { studentList }, 'POST')

+ 13 - 0
onlineexam-admin/src/api/subject.js

@@ -0,0 +1,13 @@
+import ajax from '@/config/ajax'
+const BASE_URL = process.env.BASE_API + '/admin'
+
+// 获取全部科目信息
+export const reqGetSubjectsList = () => ajax(BASE_URL + '/getSubjectsList')
+// 获取搜索科目信息
+export const reqSearchSubjectsList = (langName, langDesc, langCreatedBy, isRecommend) => ajax(BASE_URL + '/searchSubjectsList', { langName, langDesc, langCreatedBy, isRecommend })
+// 请求添加轮播图信息
+export const reqInsertSubjectInfo = (temp) => ajax(BASE_URL + '/insertSubjectInfo', temp, 'POST')
+// 请求更新轮播图信息
+export const reqUpdateSubjectInfo = (row) => ajax(BASE_URL + '/updateSubjectInfo', row, 'POST')
+// 请求删除科目
+export const reqDeleteSubject = (langId) => ajax(BASE_URL + '/deleteSubject', { langId }, 'POST')

+ 13 - 0
onlineexam-admin/src/api/teacher.js

@@ -0,0 +1,13 @@
+import ajax from '@/config/ajax'
+const BASE_URL = process.env.BASE_API + '/admin'
+
+// 获取全部教师信息
+export const reqGetTeachersList = () => ajax(BASE_URL + '/getTeachersList')
+// 请求搜索教师信息
+export const reqSearchTeachersList = (tno, teaName, teaSex) => ajax(BASE_URL + '/searchTeacherInfo', { tno, teaName, teaSex })
+// 请求更新教师信息
+export const reqUpdateTeacherInfo = (row) => ajax(BASE_URL + '/updateTeacherInfo', row, 'POST')
+// 请求添加教师信息
+export const reqInsertTeacherInfo = (temp) => ajax(BASE_URL + '/insertTeacherInfo', temp, 'POST')
+// 插入上传教师信息数据
+export const reqInsertTeacherInfoList = (teacherList) => ajax(BASE_URL + '/insertTeacherInfoList', { teacherList }, 'POST')

BIN
onlineexam-admin/src/assets/404_images/404.png


BIN
onlineexam-admin/src/assets/404_images/404_cloud.png


BIN
onlineexam-admin/src/assets/images/admin.png


BIN
onlineexam-admin/src/assets/images/backend-bg-img-min.png


BIN
onlineexam-admin/src/assets/images/home_img.png


BIN
onlineexam-admin/src/assets/images/info_tab_img.png


BIN
onlineexam-admin/src/assets/images/no_read.png


BIN
onlineexam-admin/src/assets/images/profile.jpg


BIN
onlineexam-admin/src/assets/images/read.png


BIN
onlineexam-admin/src/assets/images/teacher.jpg


+ 122 - 0
onlineexam-admin/src/components/BackToTop/index.vue

@@ -0,0 +1,122 @@
+<template>
+  <transition :name="transitionName">
+    <div v-show="visible && device === 'desktop'" :style="customStyle" class="back-to-ceiling" @click="backToTop">
+      <svg width="16" height="16" viewBox="0 0 17 17" xmlns="http://www.w3.org/2000/svg" class="Icon Icon--backToTopArrow" aria-hidden="true" style="height: 16px; width: 16px;">
+        <title>回到顶部</title>
+        <g>
+          <path d="M12.036 15.59c0 .55-.453.995-.997.995H5.032c-.55 0-.997-.445-.997-.996V8.584H1.03c-1.1 0-1.36-.633-.578-1.416L7.33.29c.39-.39 1.026-.385 1.412 0l6.878 6.88c.782.78.523 1.415-.58 1.415h-3.004v7.004z" fill-rule="evenodd" />
+        </g>
+      </svg>
+    </div>
+  </transition>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+export default {
+  name: 'BackToTop',
+  props: {
+    visibilityHeight: {
+      type: Number,
+      default: 400
+    },
+    backPosition: {
+      type: Number,
+      default: 0
+    },
+    customStyle: {
+      type: Object,
+      default: function() {
+        return {
+          right: '50px',
+          bottom: '50px',
+          width: '40px',
+          height: '40px',
+          'border-radius': '4px',
+          'line-height': '45px',
+          background: '#e7eaf1'
+        }
+      }
+    },
+    transitionName: {
+      type: String,
+      default: 'fade'
+    }
+  },
+  data() {
+    return {
+      visible: false,
+      interval: null,
+      isMoving: false
+    }
+  },
+  computed: {
+    ...mapGetters([
+      'device'
+    ])
+  },
+  mounted() {
+    window.addEventListener('scroll', this.handleScroll)
+  },
+  beforeDestroy() {
+    window.removeEventListener('scroll', this.handleScroll)
+    if (this.interval) {
+      clearInterval(this.interval)
+    }
+  },
+  methods: {
+    handleScroll() {
+      this.visible = window.pageYOffset > this.visibilityHeight
+    },
+    backToTop() {
+      if (this.isMoving) return
+      const start = window.pageYOffset
+      let i = 0
+      this.isMoving = true
+      this.interval = setInterval(() => {
+        const next = Math.floor(this.easeInOutQuad(10 * i, start, -start, 500))
+        if (next <= this.backPosition) {
+          window.scrollTo(0, this.backPosition)
+          clearInterval(this.interval)
+          this.isMoving = false
+        } else {
+          window.scrollTo(0, next)
+        }
+        i++
+      }, 16.7)
+    },
+    easeInOutQuad(t, b, c, d) {
+      if ((t /= d / 2) < 1) return c / 2 * t * t + b
+      return -c / 2 * (--t * (t - 2) - 1) + b
+    }
+  }
+}
+</script>
+
+<style scoped>
+.back-to-ceiling {
+  position: fixed;
+  display: inline-block;
+  text-align: center;
+  cursor: pointer;
+}
+
+.back-to-ceiling:hover {
+  background: #d5dbe7;
+}
+
+.fade-enter-active,
+.fade-leave-active {
+  transition: opacity .5s;
+}
+
+.fade-enter,
+.fade-leave-to {
+  opacity: 0
+}
+
+.back-to-ceiling .Icon {
+  fill: #9aaabf;
+  background: none;
+}
+</style>

+ 68 - 0
onlineexam-admin/src/components/Breadcrumb/index.vue

@@ -0,0 +1,68 @@
+<template>
+  <el-breadcrumb class="app-breadcrumb" separator="/">
+    <transition-group name="breadcrumb">
+      <el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
+        <span v-if="item.redirect==='noredirect'||index==levelList.length-1" class="no-redirect">{{ item.meta.title }}</span>
+        <a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
+      </el-breadcrumb-item>
+    </transition-group>
+  </el-breadcrumb>
+</template>
+
+<script>
+import pathToRegexp from 'path-to-regexp'
+export default {
+  data() {
+    return {
+      levelList: null
+    }
+  },
+  watch: {
+    $route() {
+      this.getBreadcrumb()
+    }
+  },
+  created() {
+    this.getBreadcrumb()
+  },
+  methods: {
+    getBreadcrumb() {
+      let matched = this.$route.matched.filter(item => item.name)
+
+      const first = matched[0]
+      if (first && first.name !== 'dashboard') {
+        matched = [{ path: '/dashboard', meta: { title: '首页' }}].concat(matched)
+      }
+
+      this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
+    },
+    pathCompile(path) {
+      // To solve this problem https://github.com/PanJiaChen/vue-element-admin/issues/561
+      const { params } = this.$route
+      var toPath = pathToRegexp.compile(path)
+      return toPath(params)
+    },
+    handleLink(item) {
+      const { redirect, path } = item
+      if (redirect) {
+        this.$router.push(redirect)
+        return
+      }
+      this.$router.push(this.pathCompile(path))
+    }
+  }
+}
+</script>
+
+<style type="text/scss" rel="stylesheet/scss" lang="scss" scoped>
+  .app-breadcrumb.el-breadcrumb {
+    display: inline-block;
+    font-size: 14px;
+    line-height: 50px;
+    margin-left: 10px;
+    .no-redirect {
+      color: #97a8be;
+      cursor: text;
+    }
+  }
+</style>

+ 342 - 0
onlineexam-admin/src/components/Charts/mixChart.vue

@@ -0,0 +1,342 @@
+<template>
+  <div :id="id" :class="className" :style="{height:height,width:width}" />
+</template>
+
+<script>
+import echarts from 'echarts'
+import resize from './mixins/resize'
+import { mapGetters } from 'vuex'
+import { reqGetChartData } from '@/api/student'
+
+export default {
+  mixins: [resize],
+  props: {
+    className: {
+      type: String,
+      default: 'chart'
+    },
+    id: {
+      type: String,
+      default: 'chart'
+    },
+    width: {
+      type: String,
+      default: '200px'
+    },
+    height: {
+      type: String,
+      default: '200px'
+    }
+  },
+  data() {
+    return {
+      titleText: '学生成绩信息图表',
+      chart: null,
+      snoData: [],
+      singleData: [],
+      multipleData: [],
+      judgeData: [],
+      fillData: [],
+      scoreData: [],
+      timeUsedData: [],
+      paperInfo: {}
+    }
+  },
+  computed: {
+    ...mapGetters([
+      'paperId',
+      'device'
+    ])
+  },
+  watch: {
+    paperId(newValue) {
+      this.getChartData(() => {
+        if (!this.snoData.length) {
+          this.titleText = '该试卷暂无人参加考试'
+        } else {
+          this.titleText = `${this.paperInfo.paperName}(总分:${this.paperInfo.totalScore}分,考试时长:${this.paperInfo.paperDuration / 60}分钟,难度系数:${this.paperInfo.paperDifficulty})`
+        }
+        this.chart.clear()
+        this.initChart()
+      })
+    }
+  },
+  mounted() {
+    this.getChartData(() => {
+      this.titleText = `${this.paperInfo.paperName}(总分:${this.paperInfo.totalScore}分,考试时长:${this.paperInfo.paperDuration / 60}分钟,难度系数:${this.paperInfo.paperDifficulty})`
+      this.initChart()
+    })
+  },
+  beforeDestroy() {
+    if (!this.chart) {
+      return
+    }
+    this.chart.dispose()
+    this.chart = null
+  },
+  methods: {
+    async getChartData(callback) {
+      const result = await reqGetChartData(this.paperId)
+      if (result.statu === 0) {
+        this.snoData = result.data.snoData
+        this.singleData = result.data.singleData
+        this.multipleData = result.data.multipleData
+        this.judgeData = result.data.judgeData
+        this.fillData = result.data.fillData
+        this.scoreData = result.data.scoreData
+        this.timeUsedData = result.data.timeUsedData
+        this.paperInfo = result.data.paperInfo
+        callback && callback()
+      } else {
+        this.$message({
+          message: result.msg,
+          type: 'error'
+        })
+      }
+    },
+    initChart() {
+      this.chart = echarts.init(document.getElementById(this.id))
+      this.chart.setOption({
+        backgroundColor: '#344b58',
+        title: {
+          text: this.titleText,
+          x: '20',
+          top: '20',
+          textStyle: {
+            color: '#fff',
+            fontSize: this.device === 'desktop' ? '22' : '9'
+          },
+          subtextStyle: {
+            color: '#90979c',
+            fontSize: '16'
+          }
+        },
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: {
+            textStyle: {
+              color: '#fff'
+            }
+          }
+        },
+        grid: {
+          left: this.device === 'desktop' ? '5%' : '10%',
+          right: '5%',
+          borderWidth: 0,
+          top: 150,
+          bottom: 95,
+          textStyle: {
+            color: '#fff'
+          }
+        },
+        legend: {
+          x: '5%',
+          top: '10%',
+          textStyle: {
+            color: '#90979c'
+          },
+          data: ['单选题得分', '多选题得分', '判断题得分', '填空题得分', '总分', '花费时间(秒)']
+        },
+        calculable: true,
+        xAxis: [{
+          type: 'category',
+          axisLine: {
+            lineStyle: {
+              color: '#90979c'
+            }
+          },
+          splitLine: {
+            show: false
+          },
+          axisTick: {
+            show: false
+          },
+          splitArea: {
+            show: false
+          },
+          axisLabel: {
+            interval: 0,
+            rotate: this.device === 'desktop' ? -8 : -16 // 让坐标值旋转一定的角度
+          },
+          data: this.snoData
+        }],
+        yAxis: [{
+          type: 'value',
+          splitLine: {
+            show: false
+          },
+          axisLine: {
+            lineStyle: {
+              color: '#90979c'
+            }
+          },
+          axisTick: {
+            show: false
+          },
+          axisLabel: {
+            interval: 0
+          },
+          splitArea: {
+            show: false
+          }
+        }],
+        dataZoom: [{
+          show: true,
+          height: 30,
+          xAxisIndex: [
+            0
+          ],
+          bottom: 30,
+          start: 10,
+          end: 80,
+          handleIcon: 'path://M306.1,413c0,2.2-1.8,4-4,4h-59.8c-2.2,0-4-1.8-4-4V200.8c0-2.2,1.8-4,4-4h59.8c2.2,0,4,1.8,4,4V413z',
+          handleSize: '110%',
+          handleStyle: {
+            color: '#d3dee5'
+
+          },
+          textStyle: {
+            color: '#fff' },
+          borderColor: '#90979c'
+
+        }, {
+          type: 'inside',
+          show: true,
+          height: 15,
+          start: 1,
+          end: 35
+        }],
+        series: [
+          {
+            name: '单选题得分',
+            type: 'bar',
+            stack: 'total',
+            barMaxWidth: 35,
+            barGap: '10%',
+            itemStyle: {
+              normal: {
+                color: 'rgba(255,144,128,1)',
+                label: {
+                  show: true,
+                  textStyle: {
+                    color: '#fff'
+                  },
+                  position: 'insideTop',
+                  formatter(p) {
+                    return p.value > 0 ? p.value : ''
+                  }
+                }
+              }
+            },
+            data: this.singleData
+          },
+
+          {
+            name: '多选题得分',
+            type: 'bar',
+            stack: 'total',
+            itemStyle: {
+              normal: {
+                color: 'rgba(0,191,183,1)',
+                barBorderRadius: 0,
+                label: {
+                  show: true,
+                  position: 'top',
+                  formatter(p) {
+                    return p.value > 0 ? p.value : ''
+                  }
+                }
+              }
+            },
+            data: this.multipleData
+          },
+
+          {
+            name: '判断题得分',
+            type: 'bar',
+            stack: 'total',
+            itemStyle: {
+              normal: {
+                color: 'rgba(100,91,183,1)',
+                barBorderRadius: 0,
+                label: {
+                  show: true,
+                  position: 'top',
+                  formatter(p) {
+                    return p.value > 0 ? p.value : ''
+                  }
+                }
+              }
+            },
+            data: this.judgeData
+          },
+
+          {
+            name: '填空题得分',
+            type: 'bar',
+            stack: 'total',
+            itemStyle: {
+              normal: {
+                color: 'rgba(99, 194, 255, 1)',
+                barBorderRadius: 0,
+                label: {
+                  show: true,
+                  position: 'top',
+                  formatter(p) {
+                    return p.value > 0 ? p.value : ''
+                  }
+                }
+              }
+            },
+            data: this.fillData
+          },
+
+          {
+            name: '总分',
+            type: 'line',
+            stack: 'total',
+            symbolSize: 10,
+            symbol: 'circle',
+            itemStyle: {
+              normal: {
+                color: 'rgba(252,230,48,1)',
+                barBorderRadius: 0,
+                label: {
+                  show: true,
+                  position: 'top',
+                  formatter(p) {
+                    return p.value > 0 ? p.value : ''
+                  }
+                }
+              }
+            },
+            data: this.scoreData
+          },
+
+          {
+            name: '花费时间(秒)',
+            type: 'line',
+            stack: 'total',
+            symbolSize: 10,
+            symbol: 'circle',
+            itemStyle: {
+              normal: {
+                color: 'rgba(152,230,48,1)',
+                barBorderRadius: 0,
+                label: {
+                  show: true,
+                  position: 'top',
+                  formatter(p) {
+                    return p.value > 0 ? p.value : ''
+                  }
+                }
+              }
+            },
+            data: this.timeUsedData
+          }
+        ]
+      })
+    }
+  }
+}
+</script>

+ 32 - 0
onlineexam-admin/src/components/Charts/mixins/resize.js

@@ -0,0 +1,32 @@
+import { debounce } from '@/utils'
+
+export default {
+  data() {
+    return {
+      sidebarElm: null
+    }
+  },
+  mounted() {
+    this.__resizeHandler = debounce(() => {
+      if (this.chart) {
+        this.chart.resize()
+      }
+    }, 100)
+    window.addEventListener('resize', this.__resizeHandler)
+
+    this.sidebarElm = document.getElementsByClassName('sidebar-container')[0]
+    this.sidebarElm && this.sidebarElm.addEventListener('transitionend', this.sidebarResizeHandler)
+  },
+  beforeDestroy() {
+    window.removeEventListener('resize', this.__resizeHandler)
+
+    this.sidebarElm && this.sidebarElm.removeEventListener('transitionend', this.sidebarResizeHandler)
+  },
+  methods: {
+    sidebarResizeHandler(e) {
+      if (e.propertyName === 'width') {
+        this.__resizeHandler()
+      }
+    }
+  }
+}

File diff suppressed because it is too large
+ 31 - 0
onlineexam-admin/src/components/Emotion/Emotion.vue


+ 68 - 0
onlineexam-admin/src/components/Emotion/index.vue

@@ -0,0 +1,68 @@
+<template>
+  <div>
+    <div :style="{height: height + 'px' }" class="emotion-box">
+      <div v-for="(line, i) in list" :key="i" class="emotion-box-line">
+        <emotion v-for="(item, i) in line" :key="i" class="emotion-item" @click.native="clickHandler(item)" >{{ item }}</emotion>
+      </div>
+    </div>
+  </div>
+</template>
+<script>
+/* eslint-disable */
+import Emotion from './Emotion'
+export default {
+  props: {
+    height: {
+      type: Number,
+      default: 200,
+    }
+  },
+  data () {
+    return {
+      list: [
+        ['微笑', '撇嘴', '色', '发呆', '得意', '流泪', '害羞', '闭嘴'],
+        ['睡', '大哭', '尴尬', '发怒', '调皮', '呲牙', '惊讶', '难过']
+          ['酷', '冷汗', '抓狂', '吐', '偷笑', '可爱', '白眼', '傲慢'],
+        ['饥饿', '困', '惊恐', '流汗', '憨笑', '大兵', '奋斗', '咒骂'],
+        ['疑问', '嘘', '晕', '折磨', '衰', '骷髅', '敲打', '再见'],
+        ['擦汗', '抠鼻', '鼓掌', '糗大了', '坏笑', '左哼哼', '右哼哼', '哈欠'],
+        ['鄙视', '委屈', '快哭了', '阴险', '亲亲', '吓', '可怜', '菜刀'],
+        ['西瓜', '啤酒', '篮球', '乒乓', '咖啡', '饭', '猪头', '玫瑰',],
+        ['凋谢', '示爱', '爱心', '心碎', '蛋糕', '闪电', '炸弹', '刀'],
+        ['足球', '瓢虫', '便便', '月亮', '太阳', '礼物', '拥抱', '强'],
+        ['弱', '握手', '胜利', '抱拳', '勾引', '拳头', '差劲', '爱你'],
+        ['NO', 'OK', '爱情', '飞吻', '跳跳', '发抖', '怄火', '转圈'],
+        ['磕头', '回头', '跳绳', '挥手', '激动', '街舞', '左太极', '右太极'],
+      ]
+    }
+  },
+  methods: {
+    clickHandler (i) {
+      let emotion = `#${i};`
+      this.$emit('emotion', emotion)
+    }
+  },
+  components: {
+    Emotion
+  }
+}
+</script>
+<style scoped>
+  .emotion-box {
+    margin: 0 auto;
+    width: 100%;
+    box-sizing: border-box;
+    padding: 5px;
+    border: 1px solid #b4b4b4;
+    overflow: hidden;
+    overflow-y: auto;
+  }
+  .emotion-box-line {
+    display: flex;
+  }
+  .emotion-item {
+    flex: 1;
+    text-align: center;
+    cursor: pointer;
+  }
+</style>

+ 42 - 0
onlineexam-admin/src/components/Hamburger/index.vue

@@ -0,0 +1,42 @@
+<template>
+  <div>
+    <svg
+      :class="{'is-active':isActive}"
+      class="hamburger"
+      viewBox="0 0 1024 1024"
+      xmlns="http://www.w3.org/2000/svg"
+      width="64"
+      height="64"
+      @click="toggleClick">
+      <path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" />
+    </svg>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'Hamburger',
+  props: {
+    isActive: {
+      type: Boolean,
+      default: false
+    },
+    toggleClick: {
+      type: Function,
+      default: null
+    }
+  }
+}
+</script>
+
+<style scoped>
+.hamburger {
+  display: inline-block;
+  cursor: pointer;
+  width: 20px;
+  height: 20px;
+}
+.hamburger.is-active {
+  transform: rotate(180deg);
+}
+</style>

+ 101 - 0
onlineexam-admin/src/components/Pagination/index.vue

@@ -0,0 +1,101 @@
+<template>
+  <div :class="{'hidden':hidden}" class="pagination-container">
+    <el-pagination
+      :background="background"
+      :current-page.sync="currentPage"
+      :page-size.sync="pageSize"
+      :layout="layout"
+      :page-sizes="pageSizes"
+      :total="total"
+      v-bind="$attrs"
+      @size-change="handleSizeChange"
+      @current-change="handleCurrentChange"
+    />
+  </div>
+</template>
+
+<script>
+import { scrollTo } from '@/utils/scrollTo'
+
+export default {
+  name: 'Pagination',
+  props: {
+    total: {
+      required: true,
+      type: Number
+    },
+    page: {
+      type: Number,
+      default: 1
+    },
+    limit: {
+      type: Number,
+      default: 20
+    },
+    pageSizes: {
+      type: Array,
+      default() {
+        return [10, 20, 30, 50]
+      }
+    },
+    layout: {
+      type: String,
+      default: 'total, sizes, prev, pager, next, jumper'
+    },
+    background: {
+      type: Boolean,
+      default: true
+    },
+    autoScroll: {
+      type: Boolean,
+      default: true
+    },
+    hidden: {
+      type: Boolean,
+      default: false
+    }
+  },
+  computed: {
+    currentPage: {
+      get() {
+        return this.page
+      },
+      set(val) {
+        this.$emit('update:page', val)
+      }
+    },
+    pageSize: {
+      get() {
+        return this.limit
+      },
+      set(val) {
+        this.$emit('update:limit', val)
+      }
+    }
+  },
+  methods: {
+    handleSizeChange(val) {
+      this.$emit('pagination', { page: this.currentPage, limit: val })
+      if (this.autoScroll) {
+        scrollTo(0, 800)
+      }
+    },
+    handleCurrentChange(val) {
+      this.$emit('pagination', { page: val, limit: this.pageSize })
+      if (this.autoScroll) {
+        scrollTo(0, 800)
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.pagination-container {
+  background: #fff;
+  padding: 32px 16px;
+}
+.pagination-container.hidden {
+  display: none;
+}
+</style>

+ 140 - 0
onlineexam-admin/src/components/PanThumb/index.vue

@@ -0,0 +1,140 @@
+<template>
+  <div :style="{zIndex:zIndex,height:height,width:width}" class="pan-item">
+    <div class="pan-info">
+      <div class="pan-info-roles-container">
+        <slot />
+      </div>
+    </div>
+    <img :src="image" class="pan-thumb">
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'PanThumb',
+  props: {
+    image: {
+      type: String,
+      required: true
+    },
+    zIndex: {
+      type: Number,
+      default: 1
+    },
+    width: {
+      type: String,
+      default: '10vw'
+    },
+    height: {
+      type: String,
+      default: '10vw'
+    }
+  }
+}
+</script>
+
+<style scoped>
+.pan-item {
+  width: 200px;
+  height: 200px;
+  border-radius: 50%;
+  display: inline-block;
+  position: relative;
+  cursor: default;
+  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
+}
+
+.pan-info-roles-container {
+  padding: 20px;
+  text-align: center;
+}
+
+.pan-thumb {
+  width: 100%;
+  height: 100%;
+  background-size: 100%;
+  border-radius: 50%;
+  overflow: hidden;
+  position: absolute;
+  transform-origin: 95% 40%;
+  transition: all 0.3s ease-in-out;
+}
+
+.pan-thumb:after {
+  content: '';
+  width: 8px;
+  height: 8px;
+  position: absolute;
+  border-radius: 50%;
+  top: 40%;
+  left: 95%;
+  margin: -4px 0 0 -4px;
+  background: radial-gradient(ellipse at center, rgba(14, 14, 14, 1) 0%, rgba(125, 126, 125, 1) 100%);
+  box-shadow: 0 0 1px rgba(255, 255, 255, 0.9);
+}
+
+.pan-info {
+  position: absolute;
+  width: inherit;
+  height: inherit;
+  border-radius: 50%;
+  overflow: hidden;
+  box-shadow: inset 0 0 0 5px rgba(0, 0, 0, 0.05);
+}
+
+.pan-info h3 {
+  color: #fff;
+  text-transform: uppercase;
+  position: relative;
+  letter-spacing: 2px;
+  font-size: 18px;
+  margin: 0 60px;
+  padding: 22px 0 0 0;
+  height: 85px;
+  font-family: 'Open Sans', Arial, sans-serif;
+  text-shadow: 0 0 1px #fff, 0 1px 2px rgba(0, 0, 0, 0.3);
+}
+
+.pan-info p {
+  color: #fff;
+  padding: 10px 5px;
+  font-style: italic;
+  margin: 0 30px;
+  font-size: 12px;
+  border-top: 1px solid rgba(255, 255, 255, 0.5);
+}
+
+.pan-info p a {
+  display: block;
+  color: #333;
+  width: 80px;
+  height: 80px;
+  background: rgba(255, 255, 255, 0.3);
+  border-radius: 50%;
+  color: #fff;
+  font-style: normal;
+  font-weight: 700;
+  text-transform: uppercase;
+  font-size: 9px;
+  letter-spacing: 1px;
+  padding-top: 24px;
+  margin: 7px auto 0;
+  font-family: 'Open Sans', Arial, sans-serif;
+  opacity: 0;
+  transition: transform 0.3s ease-in-out 0.2s, opacity 0.3s ease-in-out 0.2s, background 0.2s linear 0s;
+  transform: translateX(60px) rotate(90deg);
+}
+
+.pan-info p a:hover {
+  background: rgba(255, 255, 255, 0.5);
+}
+
+.pan-item:hover .pan-thumb {
+  transform: rotate(-110deg);
+}
+
+.pan-item:hover .pan-info p a {
+  opacity: 1;
+  transform: translateX(0px) rotate(0deg);
+}
+</style>

+ 60 - 0
onlineexam-admin/src/components/Screenfull/index.vue

@@ -0,0 +1,60 @@
+<template>
+  <div>
+    <svg-icon :icon-class="isFullscreen?'exit-fullscreen':'fullscreen'" @click="click" />
+  </div>
+</template>
+
+<script>
+import screenfull from 'screenfull'
+
+export default {
+  name: 'Screenfull',
+  data() {
+    return {
+      isFullscreen: false
+    }
+  },
+  mounted() {
+    this.init()
+  },
+  beforeDestroy() {
+    this.destroy()
+  },
+  methods: {
+    click() {
+      if (!screenfull.enabled) {
+        this.$message({
+          message: '你的浏览器无法进行缩放',
+          type: 'warning'
+        })
+        return false
+      }
+      screenfull.toggle()
+    },
+    change() {
+      this.isFullscreen = screenfull.isFullscreen
+    },
+    init() {
+      if (screenfull.enabled) {
+        screenfull.on('change', this.change)
+      }
+    },
+    destroy() {
+      if (screenfull.enabled) {
+        screenfull.off('change', this.change)
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.screenfull-svg {
+  display: inline-block;
+  cursor: pointer;
+  fill: #5a5e66;;
+  width: 20px;
+  height: 20px;
+  vertical-align: 10px;
+}
+</style>

+ 43 - 0
onlineexam-admin/src/components/SvgIcon/index.vue

@@ -0,0 +1,43 @@
+<template>
+  <svg :class="svgClass" aria-hidden="true" v-on="$listeners">
+    <use :xlink:href="iconName"/>
+  </svg>
+</template>
+
+<script>
+export default {
+  name: 'SvgIcon',
+  props: {
+    iconClass: {
+      type: String,
+      required: true
+    },
+    className: {
+      type: String,
+      default: ''
+    }
+  },
+  computed: {
+    iconName() {
+      return `#icon-${this.iconClass}`
+    },
+    svgClass() {
+      if (this.className) {
+        return 'svg-icon ' + this.className
+      } else {
+        return 'svg-icon'
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.svg-icon {
+  width: 1em;
+  height: 1em;
+  vertical-align: -0.15em;
+  fill: currentColor;
+  overflow: hidden;
+}
+</style>

+ 113 - 0
onlineexam-admin/src/components/TextHoverEffect/Mallki.vue

@@ -0,0 +1,113 @@
+<template>
+  <a :class="className" class="link--mallki" href="#">
+    {{ text }}
+    <span :data-letters="text" />
+    <span :data-letters="text" />
+  </a>
+</template>
+
+<script>
+export default {
+  props: {
+    className: {
+      type: String,
+      default: ''
+    },
+    text: {
+      type: String,
+      default: 'vue-element-admin'
+    }
+  }
+}
+</script>
+
+<style>
+/* Mallki */
+
+.link--mallki {
+  font-weight: 800;
+  color: #4dd9d5;
+  font-family: 'Dosis', sans-serif;
+  -webkit-transition: color 0.5s 0.25s;
+  transition: color 0.5s 0.25s;
+  overflow: hidden;
+  position: relative;
+  display: inline-block;
+  line-height: 1;
+  outline: none;
+  text-decoration: none;
+}
+
+.link--mallki:hover {
+  -webkit-transition: none;
+  transition: none;
+  color: transparent;
+}
+
+.link--mallki::before {
+  content: '';
+  width: 100%;
+  height: 6px;
+  margin: -3px 0 0 0;
+  background: #3888fa;
+  position: absolute;
+  left: 0;
+  top: 50%;
+  -webkit-transform: translate3d(-100%, 0, 0);
+  transform: translate3d(-100%, 0, 0);
+  -webkit-transition: -webkit-transform 0.4s;
+  transition: transform 0.4s;
+  -webkit-transition-timing-function: cubic-bezier(0.7, 0, 0.3, 1);
+  transition-timing-function: cubic-bezier(0.7, 0, 0.3, 1);
+}
+
+.link--mallki:hover::before {
+  -webkit-transform: translate3d(100%, 0, 0);
+  transform: translate3d(100%, 0, 0);
+}
+
+.link--mallki span {
+  position: absolute;
+  height: 50%;
+  width: 100%;
+  left: 0;
+  top: 0;
+  overflow: hidden;
+}
+
+.link--mallki span::before {
+  content: attr(data-letters);
+  color: red;
+  position: absolute;
+  left: 0;
+  width: 100%;
+  color: #3888fa;
+  -webkit-transition: -webkit-transform 0.5s;
+  transition: transform 0.5s;
+}
+
+.link--mallki span:nth-child(2) {
+  top: 50%;
+}
+
+.link--mallki span:first-child::before {
+  top: 0;
+  -webkit-transform: translate3d(0, 100%, 0);
+  transform: translate3d(0, 100%, 0);
+}
+
+.link--mallki span:nth-child(2)::before {
+  bottom: 0;
+  -webkit-transform: translate3d(0, -100%, 0);
+  transform: translate3d(0, -100%, 0);
+}
+
+.link--mallki:hover span::before {
+  -webkit-transition-delay: 0.3s;
+  transition-delay: 0.3s;
+  -webkit-transform: translate3d(0, 0, 0);
+  transform: translate3d(0, 0, 0);
+  -webkit-transition-timing-function: cubic-bezier(0.2, 1, 0.3, 1);
+  transition-timing-function: cubic-bezier(0.2, 1, 0.3, 1);
+}
+</style>

+ 143 - 0
onlineexam-admin/src/components/UploadExcel/index.vue

@@ -0,0 +1,143 @@
+<template>
+  <div style="display: flex;justify-content: center;align-items: center">
+    <input ref="excel-upload-input" class="excel-upload-input" type="file" accept=".xlsx, .xls" @change="handleClick">
+    <div class="drop" @drop="handleDrop" @dragover="handleDragover" @dragenter="handleDragover">
+      <span v-if="device === 'desktop'">将Excel文件拖至此处或者</span>
+      <el-button :loading="loading" style="margin-left:16px;" type="primary" icon="el-icon-upload" @click="handleUpload">
+        点击上传
+      </el-button>
+    </div>
+  </div>
+</template>
+
+<script>
+import XLSX from 'xlsx'
+import { mapGetters } from 'vuex'
+export default {
+  props: {
+    beforeUpload: Function, // eslint-disable-line
+    onSuccess: Function// eslint-disable-line
+  },
+  data() {
+    return {
+      loading: false,
+      excelData: {
+        header: null,
+        results: null
+      }
+    }
+  },
+  computed: {
+    ...mapGetters([
+      'device'
+    ])
+  },
+  methods: {
+    generateData({ header, results }) {
+      this.excelData.header = header
+      this.excelData.results = results
+      this.onSuccess && this.onSuccess(this.excelData)
+    },
+    handleDrop(e) {
+      e.stopPropagation()
+      e.preventDefault()
+      if (this.loading) return
+      const files = e.dataTransfer.files
+      if (files.length !== 1) {
+        this.$message.error('只支持上传一个文件!')
+        return
+      }
+      const rawFile = files[0] // only use files[0]
+
+      if (!this.isExcel(rawFile)) {
+        this.$message.error('只支持上传.xlsx, .xls, .csv后缀的文件')
+        return false
+      }
+      this.upload(rawFile)
+      e.stopPropagation()
+      e.preventDefault()
+    },
+    handleDragover(e) {
+      e.stopPropagation()
+      e.preventDefault()
+      e.dataTransfer.dropEffect = 'copy'
+    },
+    handleUpload() {
+      this.$refs['excel-upload-input'].click()
+    },
+    handleClick(e) {
+      const files = e.target.files
+      const rawFile = files[0] // only use files[0]
+      if (!rawFile) return
+      this.upload(rawFile)
+    },
+    upload(rawFile) {
+      this.$refs['excel-upload-input'].value = null // fix can't select the same excel
+
+      if (!this.beforeUpload) {
+        this.readerData(rawFile)
+        return
+      }
+      const before = this.beforeUpload(rawFile)
+      if (before) {
+        this.readerData(rawFile)
+      }
+    },
+    readerData(rawFile) {
+      this.loading = true
+      return new Promise((resolve, reject) => {
+        const reader = new FileReader()
+        reader.onload = e => {
+          const data = e.target.result
+          const workbook = XLSX.read(data, { type: 'array' })
+          const firstSheetName = workbook.SheetNames[0]
+          const worksheet = workbook.Sheets[firstSheetName]
+          const header = this.getHeaderRow(worksheet)
+          const results = XLSX.utils.sheet_to_json(worksheet)
+          this.generateData({ header, results })
+          this.loading = false
+          resolve()
+        }
+        reader.readAsArrayBuffer(rawFile)
+      })
+    },
+    getHeaderRow(sheet) {
+      const headers = []
+      const range = XLSX.utils.decode_range(sheet['!ref'])
+      let C
+      const R = range.s.r
+      /* start in the first row */
+      for (C = range.s.c; C <= range.e.c; ++C) { /* walk every column in the range */
+        const cell = sheet[XLSX.utils.encode_cell({ c: C, r: R })]
+        /* find the cell in the first row */
+        let hdr = 'UNKNOWN ' + C // <-- replace with your desired default
+        if (cell && cell.t) hdr = XLSX.utils.format_cell(cell)
+        headers.push(hdr)
+      }
+      return headers
+    },
+    isExcel(file) {
+      return /\.(xlsx|xls|csv)$/.test(file.name)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.excel-upload-input{
+  display: none;
+  z-index: -9999;
+}
+.drop{
+  border: 2px dashed #bbb;
+  width: 600px;
+  height: 160px;
+  line-height: 160px;
+  margin: 0 auto;
+  font-size: 24px;
+  border-radius: 5px;
+  text-align: center;
+  color: #bbb;
+  position: relative;
+}
+</style>

+ 42 - 0
onlineexam-admin/src/config/ajax.js

@@ -0,0 +1,42 @@
+/*
+ajax 请求函数模块
+*/
+import axios from 'axios'
+/**
+ * 向外部暴漏一个函数 ajax
+ * @param {*} url 请求路径,默认为空
+ * @param {*} data 请求参数,默认为空对象
+ * @param {*} type 请求方法,默认为GET
+ */
+// axios.defaults.withCredentials = true
+export default function ajax(url = '', data = {}, type = 'GET') {
+  // 返回值 Promise对象 (异步返回的数据是response.data,而不是response)
+  return new Promise(function(resolve, reject) {
+    // (利用axios)异步执行ajax请求
+    let promise // 这个内部的promise用来保存axios的返回值(promise对象)
+    if (type === 'GET') {
+      // 准备 url query 参数数据
+      let dataStr = '' // 数据拼接字符串,将data连接到url
+      Object.keys(data).forEach(key => {
+        dataStr += key + '=' + data[key] + '&'
+      })
+      if (dataStr !== '') {
+        dataStr = dataStr.substring(0, dataStr.lastIndexOf('&'))
+        url = url + '?' + dataStr
+      }
+      // 发送 get 请求
+      promise = axios.get(url)
+    } else {
+      // 发送 post 请求
+      promise = axios.post(url, data)
+    }
+    promise.then(response => {
+      // 成功回调resolve()
+      resolve(response.data)
+    })
+      .catch(error => {
+        // 失败回调reject()
+        reject(error)
+      })
+  })
+}

+ 13 - 0
onlineexam-admin/src/directive/waves/index.js

@@ -0,0 +1,13 @@
+import waves from './waves'
+
+const install = function(Vue) {
+  Vue.directive('waves', waves)
+}
+
+if (window.Vue) {
+  window.waves = waves
+  Vue.use(install); // eslint-disable-line
+}
+
+waves.install = install
+export default waves

+ 26 - 0
onlineexam-admin/src/directive/waves/waves.css

@@ -0,0 +1,26 @@
+.waves-ripple {
+    position: absolute;
+    border-radius: 100%;
+    background-color: rgba(0, 0, 0, 0.15);
+    background-clip: padding-box;
+    pointer-events: none;
+    -webkit-user-select: none;
+    -moz-user-select: none;
+    -ms-user-select: none;
+    user-select: none;
+    -webkit-transform: scale(0);
+    -ms-transform: scale(0);
+    transform: scale(0);
+    opacity: 1;
+}
+
+.waves-ripple.z-active {
+    opacity: 0;
+    -webkit-transform: scale(2);
+    -ms-transform: scale(2);
+    transform: scale(2);
+    -webkit-transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;
+    transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;
+    transition: opacity 1.2s ease-out, transform 0.6s ease-out;
+    transition: opacity 1.2s ease-out, transform 0.6s ease-out, -webkit-transform 0.6s ease-out;
+}

+ 72 - 0
onlineexam-admin/src/directive/waves/waves.js

@@ -0,0 +1,72 @@
+import './waves.css'
+
+const context = '@@wavesContext'
+
+function handleClick(el, binding) {
+  function handle(e) {
+    const customOpts = Object.assign({}, binding.value)
+    const opts = Object.assign({
+      ele: el, // 波纹作用元素
+      type: 'hit', // hit 点击位置扩散 center中心点扩展
+      color: 'rgba(0, 0, 0, 0.15)' // 波纹颜色
+    },
+    customOpts
+    )
+    const target = opts.ele
+    if (target) {
+      target.style.position = 'relative'
+      target.style.overflow = 'hidden'
+      const rect = target.getBoundingClientRect()
+      let ripple = target.querySelector('.waves-ripple')
+      if (!ripple) {
+        ripple = document.createElement('span')
+        ripple.className = 'waves-ripple'
+        ripple.style.height = ripple.style.width = Math.max(rect.width, rect.height) + 'px'
+        target.appendChild(ripple)
+      } else {
+        ripple.className = 'waves-ripple'
+      }
+      switch (opts.type) {
+        case 'center':
+          ripple.style.top = rect.height / 2 - ripple.offsetHeight / 2 + 'px'
+          ripple.style.left = rect.width / 2 - ripple.offsetWidth / 2 + 'px'
+          break
+        default:
+          ripple.style.top =
+            (e.pageY - rect.top - ripple.offsetHeight / 2 - document.documentElement.scrollTop ||
+              document.body.scrollTop) + 'px'
+          ripple.style.left =
+            (e.pageX - rect.left - ripple.offsetWidth / 2 - document.documentElement.scrollLeft ||
+              document.body.scrollLeft) + 'px'
+      }
+      ripple.style.backgroundColor = opts.color
+      ripple.className = 'waves-ripple z-active'
+      return false
+    }
+  }
+
+  if (!el[context]) {
+    el[context] = {
+      removeHandle: handle
+    }
+  } else {
+    el[context].removeHandle = handle
+  }
+
+  return handle
+}
+
+export default {
+  bind(el, binding) {
+    el.addEventListener('click', handleClick(el, binding), false)
+  },
+  update(el, binding) {
+    el.removeEventListener('click', el[context].removeHandle, false)
+    el.addEventListener('click', handleClick(el, binding), false)
+  },
+  unbind(el) {
+    el.removeEventListener('click', el[context].removeHandle, false)
+    el[context] = null
+    delete el[context]
+  }
+}

+ 16 - 0
onlineexam-admin/src/filters/index.js

@@ -0,0 +1,16 @@
+import Vue from 'vue'
+import { getNumberPrefix } from '@/utils'
+// import moment from 'moment'
+import format from 'date-fns/format'
+// 自定义过滤器
+Vue.filter('date-format', function(value, formatStr = 'YYYY-MM-DD HH:mm:ss') {
+  // return moment(value).format(formatStr)
+  return format(value, formatStr)
+})
+// 自定义花费时间格式化
+Vue.filter('timeUsed-format', function(value) {
+  const hours = getNumberPrefix(parseInt(value / (1000 * 60 * 60) % 24, 10))
+  const minutes = getNumberPrefix(parseInt(value / (1000 * 60) % 60, 10))
+  const seconds = getNumberPrefix(parseInt(value / 1000 % 60, 10))
+  return `${hours}:${minutes}:${seconds}`
+})

+ 9 - 0
onlineexam-admin/src/icons/index.js

@@ -0,0 +1,9 @@
+import Vue from 'vue'
+import SvgIcon from '@/components/SvgIcon' // svg组件
+
+// register globally
+Vue.component('svg-icon', SvgIcon)
+
+const requireAll = requireContext => requireContext.keys().map(requireContext)
+const req = require.context('./svg', false, /\.svg$/)
+requireAll(req)

File diff suppressed because it is too large
+ 1 - 0
onlineexam-admin/src/icons/svg/chart.svg


+ 1 - 0
onlineexam-admin/src/icons/svg/example.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M96.258 57.462h31.421C124.794 27.323 100.426 2.956 70.287.07v31.422a32.856 32.856 0 0 1 25.971 25.97zm-38.796-25.97V.07C27.323 2.956 2.956 27.323.07 57.462h31.422a32.856 32.856 0 0 1 25.97-25.97zm12.825 64.766v31.421c30.46-2.885 54.507-27.253 57.713-57.712H96.579c-2.886 13.466-13.146 23.726-26.292 26.291zM31.492 70.287H.07c2.886 30.46 27.253 54.507 57.713 57.713V96.579c-13.466-2.886-23.726-13.146-26.291-26.292z"/></svg>

File diff suppressed because it is too large
+ 1 - 0
onlineexam-admin/src/icons/svg/excel.svg


File diff suppressed because it is too large
+ 1 - 0
onlineexam-admin/src/icons/svg/exit-fullscreen.svg


File diff suppressed because it is too large
+ 1 - 0
onlineexam-admin/src/icons/svg/explain.svg


File diff suppressed because it is too large
+ 1 - 0
onlineexam-admin/src/icons/svg/eye-open.svg


File diff suppressed because it is too large
+ 1 - 0
onlineexam-admin/src/icons/svg/eye.svg


File diff suppressed because it is too large
+ 1 - 0
onlineexam-admin/src/icons/svg/feedback.svg


File diff suppressed because it is too large
+ 1 - 0
onlineexam-admin/src/icons/svg/fill-bank.svg


File diff suppressed because it is too large
+ 1 - 0
onlineexam-admin/src/icons/svg/fill-info.svg


File diff suppressed because it is too large
+ 1 - 0
onlineexam-admin/src/icons/svg/form.svg


+ 1 - 0
onlineexam-admin/src/icons/svg/fullscreen.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M38.47 52L52 38.462l-23.648-23.67L43.209 0H.035L0 43.137l14.757-14.865L38.47 52zm74.773 47.726L89.526 76 76 89.536l23.648 23.672L84.795 128h43.174L128 84.863l-14.757 14.863zM89.538 52l23.668-23.648L128 43.207V.038L84.866 0 99.73 14.76 76 38.472 89.538 52zM38.46 76L14.792 99.651 0 84.794v43.173l43.137.033-14.865-14.757L52 89.53 38.46 76z"/></svg>

File diff suppressed because it is too large
+ 1 - 0
onlineexam-admin/src/icons/svg/home.svg


File diff suppressed because it is too large
+ 1 - 0
onlineexam-admin/src/icons/svg/info.svg


File diff suppressed because it is too large
+ 1 - 0
onlineexam-admin/src/icons/svg/judge-bank.svg


File diff suppressed because it is too large
+ 1 - 0
onlineexam-admin/src/icons/svg/judge-info.svg


+ 1 - 0
onlineexam-admin/src/icons/svg/link.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><g><path d="M115.625 127.937H.063V12.375h57.781v12.374H12.438v90.813h90.813V70.156h12.374z"/><path d="M116.426 2.821l8.753 8.753-56.734 56.734-8.753-8.745z"/><path d="M127.893 37.982h-12.375V12.375H88.706V0h39.187z"/></g></svg>

File diff suppressed because it is too large
+ 1 - 0
onlineexam-admin/src/icons/svg/multiple-bank.svg


File diff suppressed because it is too large
+ 1 - 0
onlineexam-admin/src/icons/svg/multiple-info.svg


File diff suppressed because it is too large
+ 1 - 0
onlineexam-admin/src/icons/svg/nested.svg


File diff suppressed because it is too large
+ 1 - 0
onlineexam-admin/src/icons/svg/notice.svg


File diff suppressed because it is too large
+ 1 - 0
onlineexam-admin/src/icons/svg/password.svg


File diff suppressed because it is too large
+ 1 - 0
onlineexam-admin/src/icons/svg/que-bank.svg


File diff suppressed because it is too large
+ 1 - 0
onlineexam-admin/src/icons/svg/rotation-img.svg


File diff suppressed because it is too large
+ 1 - 0
onlineexam-admin/src/icons/svg/score.svg


File diff suppressed because it is too large
+ 1 - 0
onlineexam-admin/src/icons/svg/single-bank.svg


File diff suppressed because it is too large
+ 1 - 0
onlineexam-admin/src/icons/svg/single-info.svg


File diff suppressed because it is too large
+ 1 - 0
onlineexam-admin/src/icons/svg/student-info.svg


File diff suppressed because it is too large
+ 1 - 0
onlineexam-admin/src/icons/svg/student.svg


File diff suppressed because it is too large
+ 1 - 0
onlineexam-admin/src/icons/svg/subject.svg


File diff suppressed because it is too large
+ 1 - 0
onlineexam-admin/src/icons/svg/table.svg


File diff suppressed because it is too large
+ 1 - 0
onlineexam-admin/src/icons/svg/teacher-info.svg


+ 0 - 0
onlineexam-admin/src/icons/svg/teacher.svg


Some files were not shown because too many files changed in this diff