Selaa lähdekoodia

Signed-off-by: littlegreen <littlegreen55@163.com>20220819更新

littlegreen 2 vuotta sitten
vanhempi
commit
15947069b4
100 muutettua tiedostoa jossa 3337 lisäystä ja 0 poistoa
  1. 14 0
      .editorconfig
  2. 5 0
      .env.development
  3. 6 0
      .env.production
  4. 4 0
      .eslintignore
  5. 197 0
      .eslintrc.js
  6. 23 0
      .gitignore
  7. 21 0
      LICENSE
  8. 5 0
      babel.config.js
  9. 35 0
      build/index.js
  10. 69 0
      package.json
  11. BIN
      public/favicon.ico
  12. 14 0
      public/index.html
  13. 14 0
      src/App.vue
  14. 21 0
      src/api/common.js
  15. 25 0
      src/api/exam/exam.js
  16. 57 0
      src/api/paper/exam.js
  17. 9 0
      src/api/paper/paper.js
  18. 42 0
      src/api/qu/qu.js
  19. 33 0
      src/api/qu/repo.js
  20. 10 0
      src/api/sys/config/config.js
  21. 28 0
      src/api/sys/depart/depart.js
  22. 6 0
      src/api/sys/role/role.js
  23. 13 0
      src/api/sys/user/user.js
  24. 17 0
      src/api/user.js
  25. 10 0
      src/api/user/book.js
  26. 58 0
      src/api/user/repo.js
  27. BIN
      src/assets/401_images/401.gif
  28. BIN
      src/assets/404_images/404.png
  29. BIN
      src/assets/404_images/404_cloud.png
  30. BIN
      src/assets/bg2.jpg
  31. BIN
      src/assets/custom-theme/fonts/element-icons.ttf
  32. BIN
      src/assets/custom-theme/fonts/element-icons.woff
  33. 0 0
      src/assets/custom-theme/index.css
  34. BIN
      src/assets/guide.pdf
  35. 69 0
      src/assets/login-bg.svg
  36. BIN
      src/assets/logo.png
  37. BIN
      src/assets/system.png
  38. 111 0
      src/components/BackToTop/index.vue
  39. 82 0
      src/components/Breadcrumb/index.vue
  40. 262 0
      src/components/DataTable/index.vue
  41. 230 0
      src/components/DepartTreeSelect/index.vue
  42. 78 0
      src/components/ErrorLog/index.vue
  43. 74 0
      src/components/ExamSelect/index.vue
  44. 65 0
      src/components/FileUpload/index.vue
  45. 117 0
      src/components/FileUpload/local.vue
  46. 44 0
      src/components/Hamburger/index.vue
  47. 180 0
      src/components/HeaderSearch/index.vue
  48. 72 0
      src/components/MeetRole/index.vue
  49. 101 0
      src/components/Pagination/index.vue
  50. 142 0
      src/components/PanThumb/index.vue
  51. 77 0
      src/components/RepoSelect/index.vue
  52. 145 0
      src/components/RightPanel/index.vue
  53. 60 0
      src/components/Screenfull/index.vue
  54. 56 0
      src/components/SizeSelect/index.vue
  55. 62 0
      src/components/SvgIcon/index.vue
  56. 175 0
      src/components/ThemePicker/index.vue
  57. 49 0
      src/directive/clipboard/clipboard.js
  58. 13 0
      src/directive/clipboard/index.js
  59. 13 0
      src/directive/permission/index.js
  60. 22 0
      src/directive/permission/permission.js
  61. 91 0
      src/directive/sticky.js
  62. 13 0
      src/directive/waves/index.js
  63. 26 0
      src/directive/waves/waves.css
  64. 72 0
      src/directive/waves/waves.js
  65. 60 0
      src/filters/index.js
  66. 9 0
      src/icons/index.js
  67. 1 0
      src/icons/svg/404.svg
  68. 1 0
      src/icons/svg/admin.svg
  69. 0 0
      src/icons/svg/agreement.svg
  70. 1 0
      src/icons/svg/bug.svg
  71. 1 0
      src/icons/svg/chart.svg
  72. 1 0
      src/icons/svg/clipboard.svg
  73. 1 0
      src/icons/svg/component.svg
  74. 0 0
      src/icons/svg/configure.svg
  75. 0 0
      src/icons/svg/dashboard.svg
  76. 1 0
      src/icons/svg/documentation.svg
  77. 1 0
      src/icons/svg/drag.svg
  78. 1 0
      src/icons/svg/edit.svg
  79. 1 0
      src/icons/svg/education.svg
  80. 1 0
      src/icons/svg/email.svg
  81. 1 0
      src/icons/svg/example.svg
  82. 1 0
      src/icons/svg/excel.svg
  83. 1 0
      src/icons/svg/exit-fullscreen.svg
  84. 1 0
      src/icons/svg/eye-open.svg
  85. 1 0
      src/icons/svg/eye.svg
  86. 5 0
      src/icons/svg/fire.svg
  87. 0 0
      src/icons/svg/form.svg
  88. 1 0
      src/icons/svg/fullscreen.svg
  89. 1 0
      src/icons/svg/guide.svg
  90. 0 0
      src/icons/svg/hot.svg
  91. 1 0
      src/icons/svg/icon.svg
  92. 1 0
      src/icons/svg/international.svg
  93. 1 0
      src/icons/svg/language.svg
  94. 1 0
      src/icons/svg/link.svg
  95. 1 0
      src/icons/svg/list.svg
  96. 1 0
      src/icons/svg/lock.svg
  97. 0 0
      src/icons/svg/log.svg
  98. 0 0
      src/icons/svg/map.svg
  99. 1 0
      src/icons/svg/message.svg
  100. 1 0
      src/icons/svg/money.svg

+ 14 - 0
.editorconfig

@@ -0,0 +1,14 @@
+# https://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

+ 5 - 0
.env.development

@@ -0,0 +1,5 @@
+# 环境标识
+ENV = 'development'
+
+# 开发环境,一般就设置为本地后端端口,如:http://localhost:8101
+VUE_APP_BASE_API = 'http://114.116.114.108:8101'

+ 6 - 0
.env.production

@@ -0,0 +1,6 @@
+# 环境标识
+ENV = 'production'
+
+# 接口地址,如果要放到后端的static目录下,此处不用填任何内容
+VUE_APP_BASE_API = ''
+

+ 4 - 0
.eslintignore

@@ -0,0 +1,4 @@
+build/*.js
+src/assets
+public
+dist

+ 197 - 0
.eslintrc.js

@@ -0,0 +1,197 @@
+module.exports = {
+  root: true,
+  parserOptions: {
+    parser: 'babel-eslint',
+    sourceType: 'module'
+  },
+  env: {
+    browser: true,
+    node: true,
+    es6: true,
+  },
+  extends: ['plugin:vue/recommended', 'eslint:recommended'],
+
+  rules: {
+    "vue/max-attributes-per-line": [2, {
+      "singleline": 10,
+      "multiline": {
+        "max": 1,
+        "allowFirstLine": false
+      }
+    }],
+    "vue/singleline-html-element-content-newline": "off",
+    "vue/multiline-html-element-content-newline":"off",
+    "vue/name-property-casing": ["error", "PascalCase"],
+    "vue/no-v-html": "off",
+    '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': ["error", "always", {"null": "ignore"}],
+    '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': 0,
+    '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'],
+    'eqeqeq': 'off'
+  }
+}

+ 23 - 0
.gitignore

@@ -0,0 +1,23 @@
+.DS_Store
+node_modules/
+dist/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+**/*.log
+
+tests/**/coverage/
+tests/e2e/reports
+selenium-debug.log
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.local
+
+package-lock.json
+yarn.lock

+ 21 - 0
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.

+ 5 - 0
babel.config.js

@@ -0,0 +1,5 @@
+module.exports = {
+  presets: [
+    '@vue/app'
+  ]
+}

+ 35 - 0
build/index.js

@@ -0,0 +1,35 @@
+const { run } = require('runjs')
+const chalk = require('chalk')
+const config = require('../vue.config.js')
+const rawArgv = process.argv.slice(2)
+const args = rawArgv.join(' ')
+
+if (process.env.npm_config_preview || rawArgv.includes('--preview')) {
+  const report = rawArgv.includes('--report')
+
+  run(`vue-cli-service build ${args}`)
+
+  const port = 9526
+  const publicPath = config.publicPath
+
+  var connect = require('connect')
+  var serveStatic = require('serve-static')
+  const app = connect()
+
+  app.use(
+    publicPath,
+    serveStatic('./dist', {
+      index: ['index.html', '/']
+    })
+  )
+
+  app.listen(port, function () {
+    console.log(chalk.green(`> Preview at  http://localhost:${port}${publicPath}`))
+    if (report) {
+      console.log(chalk.green(`> Report at  http://localhost:${port}${publicPath}report.html`))
+    }
+
+  })
+} else {
+  run(`vue-cli-service build ${args}`)
+}

+ 69 - 0
package.json

@@ -0,0 +1,69 @@
+{
+  "name": "yf-exam-lite",
+  "version": "1.3.5",
+  "description": "原动力题库系统",
+  "author": "18365918@qq.com",
+  "scripts": {
+    "dev": "vue-cli-service serve",
+    "build:prod": "vue-cli-service build",
+    "build:demo": "vue-cli-service build --mode demo",
+    "lint": "eslint --ext .js,.vue src",
+    "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml"
+  },
+  "dependencies": {
+    "axios": "^0.21.1",
+    "babel-plugin-dynamic-import-node": "^2.3.3",
+    "clipboard": "^2.0.4",
+    "cos-js-sdk-v5": "^1.2.16",
+    "dropzone": "5.5.1",
+    "element-ui": "^2.15.7",
+    "fuse.js": "3.4.4",
+    "js-cookie": "2.2.0",
+    "jsonlint": "1.6.3",
+    "moment": "^2.29.1",
+    "normalize.css": "7.0.0",
+    "nprogress": "0.2.0",
+    "path-to-regexp": "2.4.0",
+    "querystring": "^0.2.0",
+    "screenfull": "^4.2.0",
+    "showdown": "^1.9.1",
+    "sortablejs": "^1.8.4",
+    "tracking": "^1.1.3",
+    "vue": "2.6.10",
+    "vue-count-to": "1.0.13",
+    "vue-fullscreen": "^2.1.6",
+    "vue-router": "3.0.2",
+    "vue-splitpane": "1.0.4",
+    "vue-uuid": "^2.0.2",
+    "vue-visibility-change": "^1.2.1",
+    "vuedraggable": "2.20.0",
+    "vuex": "3.1.0"
+  },
+  "devDependencies": {
+    "@babel/core": "7.0.0",
+    "@babel/register": "7.0.0",
+    "@vue/cli-plugin-babel": "3.5.3",
+    "@vue/cli-plugin-eslint": "^3.9.1",
+    "@vue/cli-service": "^4.2.2",
+    "autoprefixer": "^9.5.1",
+    "babel-eslint": "10.0.1",
+    "babel-jest": "^25.1.0",
+    "chalk": "2.4.2",
+    "connect": "3.6.6",
+    "sass": "^1.49.9",
+    "sass-loader": "^10.2.0",
+    "script-ext-html-webpack-plugin": "^2.1.5",
+    "svg-sprite-loader": "4.1.3",
+    "svgo": "1.2.0",
+    "vue-template-compiler": "2.6.10",
+    "webpack": "^4.46.0"
+  },
+  "engines": {
+    "node": ">=8.9",
+    "npm": ">= 3.0.0"
+  },
+  "browserslist": [
+    "> 1%",
+    "last 2 versions"
+  ]
+}

BIN
public/favicon.ico


+ 14 - 0
public/index.html

@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+    <meta name="renderer" content="webkit">
+    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
+    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
+    <title><%= webpackConfig.name %></title>
+  </head>
+  <body>
+    <div id="app"></div>
+  </body>
+</html>

+ 14 - 0
src/App.vue

@@ -0,0 +1,14 @@
+<template>
+  <div id="app">
+    <router-view />
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'App',
+  mounted() {
+    console.log(this.$store)
+  }
+}
+</script>

+ 21 - 0
src/api/common.js

@@ -0,0 +1,21 @@
+import { post } from '@/utils/request'
+
+export function fetchList(url, query) {
+  return post(url, query)
+}
+
+export function fetchDetail(url, id) {
+  return post(url, { 'id': id })
+}
+
+export function saveData(url, data) {
+  return post(url, data)
+}
+
+export function deleteData(url, ids) {
+  return post(url, { 'ids': ids })
+}
+
+export function changeState(url, ids, state) {
+  return post(url, { 'ids': ids, 'state': state })
+}

+ 25 - 0
src/api/exam/exam.js

@@ -0,0 +1,25 @@
+import { post } from '@/utils/request'
+
+/**
+ * 题库详情
+ * @param data
+ */
+export function fetchDetail(id) {
+  return post('/exam/api/exam/exam/detail', { id: id })
+}
+
+/**
+ * 保存题库
+ * @param data
+ */
+export function saveData(data) {
+  return post('/exam/api/exam/exam/save', data)
+}
+
+/**
+ * 题库详情
+ * @param data
+ */
+export function fetchList() {
+  return post('/exam/api/exam/exam/paging', { current: 1, size: 100 })
+}

+ 57 - 0
src/api/paper/exam.js

@@ -0,0 +1,57 @@
+import { post } from '@/utils/request'
+
+/**
+ * 创建试卷
+ * @param data
+ */
+export function createPaper(data) {
+  return post('/exam/api/paper/paper/create-paper', data)
+}
+
+/**
+ * 试卷详情
+ * @param data
+ */
+export function paperDetail(data) {
+  return post('/exam/api/paper/paper/paper-detail', data)
+}
+
+/**
+ * 题目详情
+ * @param data
+ */
+export function quDetail(data) {
+  return post('/exam/api/paper/paper/qu-detail', data)
+}
+
+/**
+ * 填充答案
+ * @param data
+ */
+export function fillAnswer(data) {
+  return post('/exam/api/paper/paper/fill-answer', data)
+}
+
+/**
+ * 交卷
+ * @param data
+ */
+export function handExam(data) {
+  return post('/exam/api/paper/paper/hand-exam', data)
+}
+
+/**
+ * 试卷详情
+ * @param data
+ */
+export function paperResult(data) {
+  return post('/exam/api/paper/paper/paper-result', data)
+}
+
+/**
+ * 错题训练
+ * @param data
+ */
+export function training(data) {
+  return post('/exam/api/paper/paper/training', data)
+}

+ 9 - 0
src/api/paper/paper.js

@@ -0,0 +1,9 @@
+import { post } from '@/utils/request'
+
+/**
+ * 试卷列表
+ * @param data
+ */
+export function listPaper(userId, examId) {
+  return post('/exam/api/paper/paper/paging', { current: 1, size: 5, params: { userId: userId, examId: examId }})
+}

+ 42 - 0
src/api/qu/qu.js

@@ -0,0 +1,42 @@
+import { post, upload, download } from '@/utils/request'
+
+/**
+ * 题库详情
+ * @param data
+ */
+export function fetchDetail(id) {
+  return post('/exam/api/qu/qu/detail', { id: id })
+}
+
+/**
+ * 保存题库
+ * @param data
+ */
+export function saveData(data) {
+  return post('/exam/api/qu/qu/save', data)
+}
+
+/**
+ * 导出
+ * @param data
+ */
+export function exportExcel(data) {
+  return download('/exam/api/qu/qu/export', data, '导出的数据.xlsx')
+}
+
+/**
+ * 导入模板
+ * @param data
+ */
+export function importTemplate() {
+  return download('/exam/api/qu/qu/import/template', {}, 'qu-import-template.xlsx')
+}
+
+/**
+ * 导出
+ * @param data
+ */
+export function importExcel(file) {
+  return upload('/exam/api/qu/qu/import', file)
+}
+

+ 33 - 0
src/api/qu/repo.js

@@ -0,0 +1,33 @@
+import { post } from '@/utils/request'
+
+/**
+ * 题库详情
+ * @param data
+ */
+export function fetchDetail(data) {
+  return post('/exam/api/repo/detail', data)
+}
+
+/**
+ * 保存题库
+ * @param data
+ */
+export function saveData(data) {
+  return post('/exam/api/repo/save', data)
+}
+
+/**
+ * 保存题库
+ * @param data
+ */
+export function fetchList(data) {
+  return post('/exam/api/repo/list', data)
+}
+
+/**
+ * 题库批量操作
+ * @param data
+ */
+export function batchAction(data) {
+  return post('/exam/api/repo/batch-action', data)
+}

+ 10 - 0
src/api/sys/config/config.js

@@ -0,0 +1,10 @@
+import { post } from '@/utils/request'
+
+// 获得用户协议详情,固定ID为0
+export function fetchDetail() {
+  return post('/exam/api/sys/config/detail', { id: '1' })
+}
+
+export function saveData(data) {
+  return post('/exam/api/sys/config/save', data)
+}

+ 28 - 0
src/api/sys/depart/depart.js

@@ -0,0 +1,28 @@
+import { post } from '@/utils/request'
+
+export function pagingTree(data) {
+  return post('/exam/api/sys/depart/paging', data)
+}
+
+export function fetchTree(data) {
+  return post('/exam/api/sys/depart/tree', data)
+}
+
+export function fetchDetail(id) {
+  const data = { id: id }
+  return post('/exam/api/sys/depart/detail', data)
+}
+
+export function deleteData(ids) {
+  const data = { ids: ids }
+  return post('/exam/api/sys/depart/delete', data)
+}
+
+export function saveData(data) {
+  return post('/exam/api/sys/depart/save', data)
+}
+
+export function sortData(id, sort) {
+  const data = { id: id, sort: sort }
+  return post('/exam/api/sys/depart/sort', data)
+}

+ 6 - 0
src/api/sys/role/role.js

@@ -0,0 +1,6 @@
+import { post } from '@/utils/request'
+
+export function fetchList() {
+  return post('/exam/api/sys/role/list', {})
+}
+

+ 13 - 0
src/api/sys/user/user.js

@@ -0,0 +1,13 @@
+import { post } from '@/utils/request'
+
+export function updateData(data) {
+  return post('/exam/api/sys/user/update', data)
+}
+
+export function saveData(data) {
+  return post('/exam/api/sys/user/save', data)
+}
+
+export function userReg(data) {
+  return post('/exam/api/sys/user/reg', data)
+}

+ 17 - 0
src/api/user.js

@@ -0,0 +1,17 @@
+import { post } from '@/utils/request'
+
+export function login(data) {
+  return post('/exam/api/sys/user/login', data)
+}
+
+export function getInfo(token) {
+  return post('/exam/api/sys/user/info?token=' + token)
+}
+
+export function logout() {
+  return post('/exam/api/sys/user/logout', {})
+}
+
+export function reg(data) {
+  return post('/exam/api/sys/user/reg', data)
+}

+ 10 - 0
src/api/user/book.js

@@ -0,0 +1,10 @@
+import { post } from '@/utils/request'
+
+/**
+ * 题库详情
+ * @param data
+ */
+export function nextQu(examId, quId) {
+  return post('/exam/api/user/wrong-book/next', { examId: examId, quId: quId })
+}
+

+ 58 - 0
src/api/user/repo.js

@@ -0,0 +1,58 @@
+import { post } from '@/utils/request'
+
+/**
+ * 开始训练了
+ * @param mode
+ * @param repoId
+ * @param userId
+ * @returns {*}
+ */
+export function startTrain(mode, repoId, userId, clear) {
+  return post('/exam/api/user/repo/start', { mode: mode, repoId: repoId, userId: userId, clear: clear })
+}
+
+/**
+ * 开始训练了
+ * @param mode
+ * @param repoId
+ * @param userId
+ * @returns {*}
+ */
+export function fillResult(id, answers, isRight) {
+  return post('/exam/api/user/repo/fill', { id: id, answers: answers, isRight: isRight })
+}
+
+/**
+ * 是否存在训练
+ * @param mode
+ * @param repoId
+ * @param userId
+ * @returns {*}
+ */
+export function hasTrain(mode, repoId, userId) {
+  return post('/exam/api/user/repo/check', { mode: mode, repoId: repoId, userId: userId })
+}
+
+/**
+ * 查找答题卡
+ * @param mode
+ * @param repoId
+ * @param userId
+ * @returns {*}
+ */
+export function listCard(mode, repoId, userId) {
+  return post('/exam/api/user/repo/card', { mode: mode, repoId: repoId, userId: userId })
+}
+
+/**
+ * 下一题或者下一题
+ * @param mode
+ * @param repoId
+ * @param userId
+ * @param sequence
+ * @returns {*}
+ */
+export function nextQu(mode, repoId, userId, sequence) {
+  return post('/exam/api/user/repo/next', { mode: mode, repoId: repoId, userId: userId, sequence: sequence })
+}
+

BIN
src/assets/401_images/401.gif


BIN
src/assets/404_images/404.png


BIN
src/assets/404_images/404_cloud.png


BIN
src/assets/bg2.jpg


BIN
src/assets/custom-theme/fonts/element-icons.ttf


BIN
src/assets/custom-theme/fonts/element-icons.woff


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
src/assets/custom-theme/index.css


BIN
src/assets/guide.pdf


+ 69 - 0
src/assets/login-bg.svg

@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="1361px" height="609px" viewBox="0 0 1361 609" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
+    <title>Group 21</title>
+    <desc>Created with Sketch.</desc>
+    <defs></defs>
+    <g id="Ant-Design-Pro-3.0" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="账户密码登录-校验" transform="translate(-79.000000, -82.000000)">
+            <g id="Group-21" transform="translate(77.000000, 73.000000)">
+                <g id="Group-18" opacity="0.8" transform="translate(74.901416, 569.699158) rotate(-7.000000) translate(-74.901416, -569.699158) translate(4.901416, 525.199158)">
+                    <ellipse id="Oval-11" fill="#CFDAE6" opacity="0.25" cx="63.5748792" cy="32.468367" rx="21.7830479" ry="21.766008"></ellipse>
+                    <ellipse id="Oval-3" fill="#CFDAE6" opacity="0.599999964" cx="5.98746479" cy="13.8668601" rx="5.2173913" ry="5.21330997"></ellipse>
+                    <path d="M38.1354514,88.3520215 C43.8984227,88.3520215 48.570234,83.6838647 48.570234,77.9254015 C48.570234,72.1669383 43.8984227,67.4987816 38.1354514,67.4987816 C32.3724801,67.4987816 27.7006688,72.1669383 27.7006688,77.9254015 C27.7006688,83.6838647 32.3724801,88.3520215 38.1354514,88.3520215 Z" id="Oval-3-Copy" fill="#CFDAE6" opacity="0.45"></path>
+                    <path d="M64.2775582,33.1704963 L119.185836,16.5654915" id="Path-12" stroke="#CFDAE6" stroke-width="1.73913043" stroke-linecap="round" stroke-linejoin="round"></path>
+                    <path d="M42.1431708,26.5002681 L7.71190162,14.5640702" id="Path-16" stroke="#E0B4B7" stroke-width="0.702678964" opacity="0.7" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="1.405357899873153,2.108036953469981"></path>
+                    <path d="M63.9262187,33.521561 L43.6721326,69.3250951" id="Path-15" stroke="#BACAD9" stroke-width="0.702678964" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="1.405357899873153,2.108036953469981"></path>
+                    <g id="Group-17" transform="translate(126.850922, 13.543654) rotate(30.000000) translate(-126.850922, -13.543654) translate(117.285705, 4.381889)" fill="#CFDAE6">
+                        <ellipse id="Oval-4" opacity="0.45" cx="9.13482653" cy="9.12768076" rx="9.13482653" ry="9.12768076"></ellipse>
+                        <path d="M18.2696531,18.2553615 C18.2696531,13.2142826 14.1798519,9.12768076 9.13482653,9.12768076 C4.08980114,9.12768076 0,13.2142826 0,18.2553615 L18.2696531,18.2553615 Z" id="Oval-4" transform="translate(9.134827, 13.691521) scale(-1, -1) translate(-9.134827, -13.691521) "></path>
+                    </g>
+                </g>
+                <g id="Group-14" transform="translate(216.294700, 123.725600) rotate(-5.000000) translate(-216.294700, -123.725600) translate(106.294700, 35.225600)">
+                    <ellipse id="Oval-2" fill="#CFDAE6" opacity="0.25" cx="29.1176471" cy="29.1402439" rx="29.1176471" ry="29.1402439"></ellipse>
+                    <ellipse id="Oval-2" fill="#CFDAE6" opacity="0.3" cx="29.1176471" cy="29.1402439" rx="21.5686275" ry="21.5853659"></ellipse>
+                    <ellipse id="Oval-2-Copy" stroke="#CFDAE6" opacity="0.4" cx="179.019608" cy="138.146341" rx="23.7254902" ry="23.7439024"></ellipse>
+                    <ellipse id="Oval-2" fill="#BACAD9" opacity="0.5" cx="29.1176471" cy="29.1402439" rx="10.7843137" ry="10.7926829"></ellipse>
+                    <path d="M29.1176471,39.9329268 L29.1176471,18.347561 C23.1616351,18.347561 18.3333333,23.1796097 18.3333333,29.1402439 C18.3333333,35.1008781 23.1616351,39.9329268 29.1176471,39.9329268 Z" id="Oval-2" fill="#BACAD9"></path>
+                    <g id="Group-9" opacity="0.45" transform="translate(172.000000, 131.000000)" fill="#E6A1A6">
+                        <ellipse id="Oval-2-Copy-2" cx="7.01960784" cy="7.14634146" rx="6.47058824" ry="6.47560976"></ellipse>
+                        <path d="M0.549019608,13.6219512 C4.12262681,13.6219512 7.01960784,10.722722 7.01960784,7.14634146 C7.01960784,3.56996095 4.12262681,0.670731707 0.549019608,0.670731707 L0.549019608,13.6219512 Z" id="Oval-2-Copy-2" transform="translate(3.784314, 7.146341) scale(-1, 1) translate(-3.784314, -7.146341) "></path>
+                    </g>
+                    <ellipse id="Oval-10" fill="#CFDAE6" cx="218.382353" cy="138.685976" rx="1.61764706" ry="1.61890244"></ellipse>
+                    <ellipse id="Oval-10-Copy-2" fill="#E0B4B7" opacity="0.35" cx="179.558824" cy="175.381098" rx="1.61764706" ry="1.61890244"></ellipse>
+                    <ellipse id="Oval-10-Copy" fill="#E0B4B7" opacity="0.35" cx="180.098039" cy="102.530488" rx="2.15686275" ry="2.15853659"></ellipse>
+                    <path d="M28.9985381,29.9671598 L171.151018,132.876024" id="Path-11" stroke="#CFDAE6" opacity="0.8"></path>
+                </g>
+                <g id="Group-10" opacity="0.799999952" transform="translate(1054.100635, 36.659317) rotate(-11.000000) translate(-1054.100635, -36.659317) translate(1026.600635, 4.659317)">
+                    <ellipse id="Oval-7" stroke="#CFDAE6" stroke-width="0.941176471" cx="43.8135593" cy="32" rx="11.1864407" ry="11.2941176"></ellipse>
+                    <g id="Group-12" transform="translate(34.596774, 23.111111)" fill="#BACAD9">
+                        <ellipse id="Oval-7" opacity="0.45" cx="9.18534718" cy="8.88888889" rx="8.47457627" ry="8.55614973"></ellipse>
+                        <path d="M9.18534718,17.4450386 C13.8657264,17.4450386 17.6599235,13.6143199 17.6599235,8.88888889 C17.6599235,4.16345787 13.8657264,0.332739156 9.18534718,0.332739156 L9.18534718,17.4450386 Z" id="Oval-7"></path>
+                    </g>
+                    <path d="M34.6597385,24.809694 L5.71666084,4.76878945" id="Path-2" stroke="#CFDAE6" stroke-width="0.941176471"></path>
+                    <ellipse id="Oval" stroke="#CFDAE6" stroke-width="0.941176471" cx="3.26271186" cy="3.29411765" rx="3.26271186" ry="3.29411765"></ellipse>
+                    <ellipse id="Oval-Copy" fill="#F7E1AD" cx="2.79661017" cy="61.1764706" rx="2.79661017" ry="2.82352941"></ellipse>
+                    <path d="M34.6312443,39.2922712 L5.06366663,59.785082" id="Path-10" stroke="#CFDAE6" stroke-width="0.941176471"></path>
+                </g>
+                <g id="Group-19" opacity="0.33" transform="translate(1282.537219, 446.502867) rotate(-10.000000) translate(-1282.537219, -446.502867) translate(1142.537219, 327.502867)">
+                    <g id="Group-17" transform="translate(141.333539, 104.502742) rotate(275.000000) translate(-141.333539, -104.502742) translate(129.333539, 92.502742)" fill="#BACAD9">
+                        <circle id="Oval-4" opacity="0.45" cx="11.6666667" cy="11.6666667" r="11.6666667"></circle>
+                        <path d="M23.3333333,23.3333333 C23.3333333,16.8900113 18.1099887,11.6666667 11.6666667,11.6666667 C5.22334459,11.6666667 0,16.8900113 0,23.3333333 L23.3333333,23.3333333 Z" id="Oval-4" transform="translate(11.666667, 17.500000) scale(-1, -1) translate(-11.666667, -17.500000) "></path>
+                    </g>
+                    <circle id="Oval-5-Copy-6" fill="#CFDAE6" cx="201.833333" cy="87.5" r="5.83333333"></circle>
+                    <path d="M143.5,88.8126685 L155.070501,17.6038544" id="Path-17" stroke="#BACAD9" stroke-width="1.16666667"></path>
+                    <path d="M17.5,37.3333333 L127.466252,97.6449735" id="Path-18" stroke="#BACAD9" stroke-width="1.16666667"></path>
+                    <polyline id="Path-19" stroke="#CFDAE6" stroke-width="1.16666667" points="143.902597 120.302281 174.935455 231.571342 38.5 147.510847 126.366941 110.833333"></polyline>
+                    <path d="M159.833333,99.7453842 L195.416667,89.25" id="Path-20" stroke="#E0B4B7" stroke-width="1.16666667" opacity="0.6"></path>
+                    <path d="M205.333333,82.1372105 L238.719406,36.1666667" id="Path-24" stroke="#BACAD9" stroke-width="1.16666667"></path>
+                    <path d="M266.723424,132.231988 L207.083333,90.4166667" id="Path-25" stroke="#CFDAE6" stroke-width="1.16666667"></path>
+                    <circle id="Oval-5" fill="#C1D1E0" cx="156.916667" cy="8.75" r="8.75"></circle>
+                    <circle id="Oval-5-Copy-3" fill="#C1D1E0" cx="39.0833333" cy="148.75" r="5.25"></circle>
+                    <circle id="Oval-5-Copy-2" fill-opacity="0.6" fill="#D1DEED" cx="8.75" cy="33.25" r="8.75"></circle>
+                    <circle id="Oval-5-Copy-4" fill-opacity="0.6" fill="#D1DEED" cx="243.833333" cy="30.3333333" r="5.83333333"></circle>
+                    <circle id="Oval-5-Copy-5" fill="#E0B4B7" cx="175.583333" cy="232.75" r="5.25"></circle>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

BIN
src/assets/logo.png


BIN
src/assets/system.png


+ 111 - 0
src/components/BackToTop/index.vue

@@ -0,0 +1,111 @@
+<template>
+  <transition :name="transitionName">
+    <div v-show="visible" :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"><path d="M12.036 15.59a1 1 0 0 1-.997.995H5.032a.996.996 0 0 1-.997-.996V8.584H1.03c-1.1 0-1.36-.633-.578-1.416L7.33.29a1.003 1.003 0 0 1 1.412 0l6.878 6.88c.782.78.523 1.415-.58 1.415h-3.004v7.004z" /></svg>
+    </div>
+  </transition>
+</template>
+
+<script>
+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
+    }
+  },
+  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>

+ 82 - 0
src/components/Breadcrumb/index.vue

@@ -0,0 +1,82 @@
+<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(route) {
+      // if you go to the redirect page, do not update the breadcrumbs
+      if (route.path.startsWith('/redirect/')) {
+        return
+      }
+      this.getBreadcrumb()
+    }
+  },
+  created() {
+    this.getBreadcrumb()
+  },
+  methods: {
+    getBreadcrumb() {
+      // only show routes with meta.title
+      let matched = this.$route.matched.filter(item => item.meta && item.meta.title)
+      const first = matched[0]
+
+      if (!this.isDashboard(first)) {
+        matched = [{ path: '/dashboard', meta: { title: '控制台' }}].concat(matched)
+      }
+
+      this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
+    },
+    isDashboard(route) {
+      const name = route && route.name
+      if (!name) {
+        return false
+      }
+      return name.trim().toLocaleLowerCase() === '控制台'.toLocaleLowerCase()
+    },
+    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 lang="scss" scoped>
+.app-breadcrumb.el-breadcrumb {
+  display: inline-block;
+  font-size: 14px;
+  line-height: 50px;
+  margin-left: 8px;
+
+  .no-redirect {
+    color: #97a8be;
+    cursor: text;
+  }
+}
+</style>

+ 262 - 0
src/components/DataTable/index.vue

@@ -0,0 +1,262 @@
+<template>
+  <div class="app-container">
+
+    <div class="filter-container">
+
+      <slot name="filter-content" />
+
+      <el-row>
+        <el-col>
+          <el-button v-if="options.addRoute" type="primary" icon="el-icon-plus" @click="handleAdd">添加</el-button>
+        </el-col>
+      </el-row>
+
+    </div>
+
+    <div v-show="multiShow && options.multiActions" class="filter-container">
+
+      <el-select v-model="multiNow" :placeholder="selectedLabel" class="filter-item" style="width: 130px" @change="handleOption">
+        <el-option
+          v-for="item in options.multiActions"
+          :key="item.value"
+          :label="item.label"
+          :value="item.value"
+        />
+      </el-select>
+
+    </div>
+
+    <el-table
+      v-loading="listLoading"
+      :data="dataList.records"
+      :header-cell-style="{'background':'#f2f3f4', 'color':'#555', 'font-weight':'bold', 'line-height':'32px'}"
+      border
+      fit
+      highlight-current-row
+      @selection-change="handleSelection"
+    >
+
+      <el-table-column
+        v-if="options.multi"
+        align="center"
+        type="selection"
+        width="55"
+      />
+
+      <slot name="data-columns" />
+
+    </el-table>
+
+    <pagination v-show="dataList.total>0" :total="dataList.total" :page.sync="listQuery.current" :limit.sync="listQuery.size" @pagination="getList" />
+  </div>
+</template>
+
+<script>
+import { fetchList, deleteData, changeState } from '@/api/common'
+import Pagination from '@/components/Pagination'
+
+export default {
+  name: 'PagingTable',
+  components: { Pagination },
+  // 组件入参
+  props: {
+    options: {
+      type: Object,
+      default: () => {
+        return {
+          // 批量操作
+          multiActions: [],
+          // 列表请求URL
+          listUrl: '/exam/api',
+          // 删除请求URL
+          deleteUrl: '',
+          // 启用禁用
+          stateUrl: '',
+          // 可批量操作
+          multi: false
+        }
+      }
+    },
+
+    // 列表查询参数
+    listQuery: {
+      type: Object,
+      default: () => {
+        return {
+          current: 1,
+          size: 10,
+          params: {},
+          t: 0
+        }
+      }
+    }
+  },
+  data() {
+    return {
+      // 接口数据返回
+      dataList: {
+        total: 0
+      },
+      // 数据加载标识
+      listLoading: true,
+      // 选定和批量操作
+      selectedIds: [],
+      selectedObjs: [],
+      // 显示已中多少项
+      selectedLabel: '',
+      // 显示批量操作
+      multiShow: false,
+      // 批量操作的标识
+      multiNow: ''
+    }
+  },
+  watch: {
+
+    // 检测查询变化
+    listQuery: {
+      handler() {
+        this.getList()
+      },
+      deep: true
+    }
+  },
+  created() {
+    this.getList()
+  },
+  methods: {
+
+    /**
+     * 添加数据跳转
+     */
+    handleAdd() {
+      if (this.options.addRoute) {
+        this.$router.push({ name: this.options.addRoute, params: {}})
+        return
+      }
+      console.log('未设置添加数据跳转路由!')
+    },
+
+    /**
+     * 查询数据列表
+     */
+    getList() {
+      this.listLoading = true
+      this.listQuery.t = new Date().getTime()
+      fetchList(this.options.listUrl, this.listQuery).then(response => {
+        this.dataList = response.data
+        this.listLoading = false
+      })
+    },
+
+    /**
+     * 搜索
+     */
+    handleFilter() {
+      // 重新搜索
+      this.getList()
+    },
+
+    /**
+     * 批量操作回调
+     */
+    handleOption(v) {
+      this.multiNow = ''
+
+      // 内部消化的操作
+      if (v === 'delete') {
+        this.handleDelete()
+        return
+      }
+
+      if (v === 'enable') {
+        this.handleState(0)
+        return
+      }
+
+      if (v === 'disable') {
+        this.handleState(1)
+        return
+      }
+
+      // 向外回调的操作
+      this.$emit('multi-actions', { opt: v, ids: this.selectedIds })
+    },
+
+    /**
+     * 修改状态,启用禁用
+     */
+    handleState(state) {
+      // 修改状态
+      changeState(this.options.stateUrl, this.selectedIds, state).then(response => {
+        if (response.code === 0) {
+          this.$message({
+            type: 'success',
+            message: '状态修改成功!'
+          })
+
+          // 重新搜索
+          this.getList()
+        }
+      })
+    },
+
+    /**
+     * 删除数据
+     */
+    handleDelete() {
+      if (this.selectedIds.length === 0) {
+        this.$message({
+          message: '请至少选择一条数据!',
+          type: 'warning'
+        })
+        return
+      }
+
+      // 删除
+      this.$confirm('确实要删除吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        deleteData(this.options.deleteUrl, this.selectedIds).then(() => {
+          this.$message({
+            type: 'success',
+            message: '删除成功!'
+          })
+          this.getList()
+        })
+      })
+    },
+
+    /**
+     * 列表多选操作
+     * @param val
+     */
+    handleSelection(val) {
+      const ids = []
+      val.forEach(row => {
+        ids.push(row.id)
+      })
+
+      this.selectedObjs = val
+      this.selectedIds = ids
+      this.multiShow = ids.length > 0
+      this.selectedLabel = '已选' + ids.length + '项'
+
+      this.$emit('select-changed', { ids: this.selectedIds, objs: this.selectedObjs })
+    }
+
+  }
+}
+</script>
+
+<style>
+
+  .filter-container .filter-item{
+    margin-left: 5px;
+  }
+
+  .filter-container .filter-item:first-child{
+    margin-left: 0px;
+  }
+</style>

+ 230 - 0
src/components/DepartTreeSelect/index.vue

@@ -0,0 +1,230 @@
+<!-- 树状选择器 -->
+<template>
+  <el-popover
+    ref="popover"
+    placement="bottom-start"
+    trigger="click"
+    @show="onShowPopover"
+    @hide="onHidePopover"
+  >
+    <el-tree
+      ref="tree"
+      :expand-on-click-node="false"
+      :style="`min-width: ${treeWidth}`"
+      :data="data"
+      :props="props"
+      :filter-node-method="filterNode"
+      placeholder="选择部门"
+      class="select-tree"
+      check-strictly="false"
+      highlight-current
+      default-expand-all
+      @node-click="onClickNode"
+    />
+    <el-input
+      slot="reference"
+      ref="input"
+      v-model="labelModel"
+      :style="`width: ${width}px`"
+      :class="{ rotate: showStatus }"
+      :placeholder="placeholder"
+      clearable
+      suffix-icon="el-icon-arrow-down"
+    />
+  </el-popover>
+</template>
+
+<script>
+export default {
+  name: 'DepartTree',
+  // 设置绑定参数
+  model: {
+    prop: 'value',
+    event: 'selected'
+  },
+  props: {
+    // 接收绑定参数
+    // eslint-disable-next-line vue/require-default-prop
+    value: String,
+    // 输入框宽度
+    // eslint-disable-next-line vue/require-default-prop
+    width: String,
+    // 选项数据
+    options: {
+      type: Array,
+      required: true
+    },
+    // 输入框占位符
+    placeholder: {
+      type: String,
+      required: false,
+      default: '请选择'
+    },
+    // 树节点配置选项
+    props: {
+      type: Object,
+      required: false,
+      default: () => ({
+        parent: 'parentId',
+        value: 'rowGuid',
+        label: 'areaName',
+        children: 'children'
+      })
+    }
+  },
+  data() {
+    return {
+      // 树状菜单显示状态
+      showStatus: false,
+      // 菜单宽度
+      treeWidth: 'auto',
+      // 输入框显示值
+      labelModel: '',
+      // 实际请求传值
+      valueModel: '0'
+    }
+  },
+  computed: {
+    // 是否为树状结构数据
+    dataType() {
+      const jsonStr = JSON.stringify(this.options)
+      return jsonStr.indexOf(this.props.children) !== -1
+    },
+    // 若非树状结构,则转化为树状结构数据
+    data() {
+      return this.dataType ? this.options : this.switchTree()
+    }
+  },
+  watch: {
+    labelModel(val) {
+      if (!val) {
+        this.valueModel = ''
+      }
+      this.$refs.tree.filter(val)
+    },
+    value(val) {
+      this.labelModel = this.queryTree(this.data, val)
+    }
+  },
+  created() {
+    // 检测输入框原有值并显示对应 label
+    if (this.value) {
+      this.labelModel = this.queryTree(this.data, this.value)
+    }
+    // 获取输入框宽度同步至树状菜单宽度
+    this.$nextTick(() => {
+      this.treeWidth = `${
+        (this.width || this.$refs.input.$refs.input.clientWidth) - 24
+      }px`
+    })
+  },
+  methods: {
+    // 单击节点
+    onClickNode(node) {
+      this.labelModel = node[this.props.label]
+      this.valueModel = node[this.props.value]
+      this.onCloseTree()
+    },
+    // 偏平数组转化为树状层级结构
+    switchTree() {
+      return this.cleanChildren(this.buildTree(this.options, '0'))
+    },
+    // 隐藏树状菜单
+    onCloseTree() {
+      this.$refs.popover.showPopper = false
+    },
+    // 显示时触发
+    onShowPopover() {
+      this.showStatus = true
+      this.$refs.tree.filter(false)
+    },
+    // 隐藏时触发
+    onHidePopover() {
+      this.showStatus = false
+      this.$emit('selected', this.valueModel)
+    },
+    // 树节点过滤方法
+    filterNode(query, data) {
+      if (!query) return true
+      return data[this.props.label].indexOf(query) !== -1
+    },
+    // 搜索树状数据中的 ID
+    queryTree(tree, id) {
+      let stark = []
+      stark = stark.concat(tree)
+      while (stark.length) {
+        const temp = stark.shift()
+        if (temp[this.props.children]) {
+          stark = stark.concat(temp[this.props.children])
+        }
+        if (temp[this.props.value] === id) {
+          return temp[this.props.label]
+        }
+      }
+      return ''
+    },
+    // 将一维的扁平数组转换为多层级对象
+    buildTree(data, id = '0') {
+      const fa = (parentId) => {
+        const temp = []
+        for (let i = 0; i < data.length; i++) {
+          const n = data[i]
+          if (n[this.props.parent] === parentId) {
+            n.children = fa(n.rowGuid)
+            temp.push(n)
+          }
+        }
+        return temp
+      }
+      return fa(id)
+    },
+    // 清除空 children项
+    cleanChildren(data) {
+      const fa = (list) => {
+        list.map((e) => {
+          if (e.children.length) {
+            fa(e.children)
+          } else {
+            delete e.children
+          }
+          return e
+        })
+        return list
+      }
+      return fa(data)
+    }
+  }
+}
+</script>
+
+<style>
+.el-input.el-input--suffix {
+  cursor: pointer;
+  overflow: hidden;
+}
+.el-input.el-input--suffix.rotate .el-input__suffix {
+  transform: rotate(180deg);
+}
+.select-tree {
+  max-height: 350px;
+  overflow-y: scroll;
+}
+/* 菜单滚动条 */
+.select-tree::-webkit-scrollbar {
+  z-index: 11;
+  width: 6px;
+}
+.select-tree::-webkit-scrollbar-track,
+.select-tree::-webkit-scrollbar-corner {
+  background: #fff;
+}
+.select-tree::-webkit-scrollbar-thumb {
+  border-radius: 5px;
+  width: 6px;
+  background: #b4bccc;
+}
+.select-tree::-webkit-scrollbar-track-piece {
+  background: #fff;
+  width: 6px;
+}
+</style>

+ 78 - 0
src/components/ErrorLog/index.vue

@@ -0,0 +1,78 @@
+<template>
+  <div v-if="errorLogs.length>0">
+    <el-badge :is-dot="true" style="line-height: 25px;margin-top: -5px;" @click.native="dialogTableVisible=true">
+      <el-button style="padding: 8px 10px;" size="small" type="danger">
+        <svg-icon icon-class="bug" />
+      </el-button>
+    </el-badge>
+
+    <el-dialog :visible.sync="dialogTableVisible" width="80%" append-to-body>
+      <div slot="title">
+        <span style="padding-right: 10px;">Error Log</span>
+        <el-button size="mini" type="primary" icon="el-icon-delete" @click="clearAll">Clear All</el-button>
+      </div>
+      <el-table :data="errorLogs" border>
+        <el-table-column label="Message">
+          <template slot-scope="{row}">
+            <div>
+              <span class="message-title">Msg:</span>
+              <el-tag type="danger">
+                {{ row.err.message }}
+              </el-tag>
+            </div>
+            <br>
+            <div>
+              <span class="message-title" style="padding-right: 10px;">Info: </span>
+              <el-tag type="warning">
+                {{ row.vm.$vnode.tag }} error in {{ row.info }}
+              </el-tag>
+            </div>
+            <br>
+            <div>
+              <span class="message-title" style="padding-right: 16px;">Url: </span>
+              <el-tag type="success">
+                {{ row.url }}
+              </el-tag>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column label="Stack">
+          <template slot-scope="scope">
+            {{ scope.row.err.stack }}
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'ErrorLog',
+  data() {
+    return {
+      dialogTableVisible: false
+    }
+  },
+  computed: {
+    errorLogs() {
+      return this.$store.getters.errorLogs
+    }
+  },
+  methods: {
+    clearAll() {
+      this.dialogTableVisible = false
+      this.$store.dispatch('errorLog/clearErrorLog')
+    }
+  }
+}
+</script>
+
+<style scoped>
+.message-title {
+  font-size: 16px;
+  color: #333;
+  font-weight: bold;
+  padding-right: 8px;
+}
+</style>

+ 74 - 0
src/components/ExamSelect/index.vue

@@ -0,0 +1,74 @@
+<!-- eslint-disable vue/require-default-prop -->
+<template>
+
+  <el-select
+    v-model="currentValue"
+    :multiple="multi"
+    :remote-method="fetchData"
+    filterable
+    remote
+    clearable
+    placeholder="选择或搜索考试"
+    class="filter-item"
+    @change="handlerChange"
+  >
+    <el-option
+      v-for="item in dataList"
+      :key="item.id"
+      :label="item.title"
+      :value="item.id"
+    />
+  </el-select>
+
+</template>
+
+<script>
+
+import { fetchList } from '@/api/exam/exam'
+
+export default {
+  name: 'ExamSelect',
+  props: {
+    // eslint-disable-next-line vue/require-default-prop
+    multi: Boolean,
+    // eslint-disable-next-line vue/require-default-prop
+    value: Array,
+    // eslint-disable-next-line vue/require-default-prop
+    default: String
+  },
+  data() {
+    return {
+      // 下拉选项值
+      dataList: [],
+      currentValue: []
+    }
+  },
+
+  watch: {
+    // 检测查询变化
+    value: {
+      handler() {
+        this.currentValue = this.value
+      }
+    }
+  },
+  created() {
+    this.currentValue = this.value
+    this.fetchData()
+  },
+  methods: {
+
+    fetchData() {
+      fetchList().then(response => {
+        this.dataList = response.data.records
+      })
+    },
+    handlerChange(e) {
+      console.log(e)
+
+      this.$emit('change', e)
+      this.$emit('input', e)
+    }
+  }
+}
+</script>

+ 65 - 0
src/components/FileUpload/index.vue

@@ -0,0 +1,65 @@
+<template>
+  <div>
+    <file-upload-local v-model="fileUrl" :accept="accept" :tips="tips" :list-type="listType" />
+  </div>
+</template>
+
+<script>
+
+import FileUploadLocal from './local'
+
+export default {
+  name: 'FileUpload',
+  components: { FileUploadLocal },
+  props: {
+    // eslint-disable-next-line vue/require-default-prop
+    value: String,
+    accept: {
+      type: String,
+      default: '*'
+    },
+    // eslint-disable-next-line vue/require-default-prop
+    tips: String,
+    listType: {
+      type: String,
+      default: 'picture'
+    }
+  },
+  data() {
+    return {
+      fileUrl: ''
+    }
+  },
+
+  watch: {
+    // 检测查询变化
+    value: {
+      handler() {
+        this.fillValue()
+      }
+    },
+
+    // 检测查询变化
+    fileUrl: {
+      handler() {
+        this.$emit('input', this.fileUrl)
+      }
+    }
+  },
+  mounted() {
+
+  },
+
+  created() {
+    this.fillValue()
+  },
+
+  methods: {
+
+    fillValue() {
+      this.fileUrl = this.value
+    }
+
+  }
+}
+</script>

+ 117 - 0
src/components/FileUpload/local.vue

@@ -0,0 +1,117 @@
+<template>
+  <div class="content">
+    <el-upload
+      v-model="fileUrl"
+      :action="server"
+      :accept="accept"
+      :before-remove="beforeRemove"
+      :on-remove="handleRemove"
+      :on-success="handleSuccess"
+      :on-exceed="handleExceed"
+
+      :drag="listType!=='picture'"
+      :limit="limit"
+      :headers="header"
+      :file-list="fileList"
+      :list-type="listType"
+    >
+
+      <el-button v-if="listType==='picture'" size="small" type="primary">点击上传</el-button>
+
+      <i v-if="listType!=='picture'" class="el-icon-upload" />
+      <div v-if="listType!=='picture'" class="el-upload__text">
+        将文件拖到此处,或
+        <em>点击上传</em>
+      </div>
+      <div v-if="tips" slot="tip" class="el-upload__tip">{{ tips }}</div>
+    </el-upload>
+
+  </div>
+
+</template>
+
+<script>
+
+import { getToken } from '@/utils/auth'
+
+export default {
+  name: 'FileUploadLocal',
+  props: {
+    // eslint-disable-next-line vue/require-default-prop
+    value: String,
+    // eslint-disable-next-line vue/require-default-prop
+    accept: String,
+    // eslint-disable-next-line vue/require-default-prop
+    tips: String,
+    // eslint-disable-next-line vue/require-default-prop
+    listType: String,
+    limit: {
+      type: Number,
+      default: 1
+    }
+  },
+  data() {
+    return {
+      server: `${process.env.VUE_APP_BASE_API}/common/api/file/upload`,
+      fileList: [],
+      fileUrl: '',
+      header: {}
+    }
+  },
+
+  watch: {
+    // 检测查询变化
+    value: {
+      handler() {
+        this.fillValue()
+      }
+    }
+  },
+
+  created() {
+    this.fillValue()
+    this.header = { token: getToken() }
+  },
+
+  methods: {
+
+    fillValue() {
+      this.fileList = []
+      this.fileUrl = this.value
+      if (this.fileUrl) {
+        this.fileList = [{ name: this.fileUrl, url: this.fileUrl }]
+      }
+    },
+
+    // 文件超出个数限制时的钩子
+    handleExceed() {
+      this.$message.warning(`每次只能上传 ${this.limit} 个文件`)
+    },
+    // 删除文件之前的钩子
+    beforeRemove() {
+      return this.$confirm(`确定移除文件吗?`)
+    },
+
+    // 文件列表移除文件时的钩子
+    handleRemove() {
+      this.$emit('input', '')
+      this.fileList = []
+    },
+
+    // 文件上传成功时的钩子
+    handleSuccess(response) {
+      if (response.code === 1) {
+        this.$message({
+          type: 'error',
+          message: response.msg
+        })
+
+        this.fileList = []
+        return
+      }
+      this.$emit('input', response.data.url)
+    }
+
+  }
+}
+</script>

+ 44 - 0
src/components/Hamburger/index.vue

@@ -0,0 +1,44 @@
+<template>
+  <div style="padding: 0 15px;" @click="toggleClick">
+    <svg
+      :class="{'is-active':isActive}"
+      class="hamburger"
+      viewBox="0 0 1024 1024"
+      xmlns="http://www.w3.org/2000/svg"
+      width="64"
+      height="64"
+    >
+      <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
+    }
+  },
+  methods: {
+    toggleClick() {
+      this.$emit('toggleClick')
+    }
+  }
+}
+</script>
+
+<style scoped>
+.hamburger {
+  display: inline-block;
+  vertical-align: middle;
+  width: 20px;
+  height: 20px;
+}
+
+.hamburger.is-active {
+  transform: rotate(180deg);
+}
+</style>

+ 180 - 0
src/components/HeaderSearch/index.vue

@@ -0,0 +1,180 @@
+<template>
+  <div :class="{'show':show}" class="header-search">
+    <svg-icon class-name="search-icon" icon-class="search" @click.stop="click" />
+    <el-select
+      ref="headerSearchSelect"
+      v-model="search"
+      :remote-method="querySearch"
+      filterable
+      default-first-option
+      remote
+      placeholder="Search"
+      class="header-search-select"
+      @change="change"
+    >
+      <el-option v-for="item in options" :key="item.path" :value="item" :label="item.title.join(' > ')" />
+    </el-select>
+  </div>
+</template>
+
+<script>
+// fuse is a lightweight fuzzy-search module
+// make search results more in line with expectations
+import Fuse from 'fuse.js'
+import path from 'path'
+
+export default {
+  name: 'HeaderSearch',
+  data() {
+    return {
+      search: '',
+      options: [],
+      searchPool: [],
+      show: false,
+      fuse: undefined
+    }
+  },
+  computed: {
+    routes() {
+      return this.$store.getters.permission_routes
+    }
+  },
+  watch: {
+    routes() {
+      this.searchPool = this.generateRoutes(this.routes)
+    },
+    searchPool(list) {
+      this.initFuse(list)
+    },
+    show(value) {
+      if (value) {
+        document.body.addEventListener('click', this.close)
+      } else {
+        document.body.removeEventListener('click', this.close)
+      }
+    }
+  },
+  mounted() {
+    this.searchPool = this.generateRoutes(this.routes)
+  },
+  methods: {
+    click() {
+      this.show = !this.show
+      if (this.show) {
+        this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.focus()
+      }
+    },
+    close() {
+      this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.blur()
+      this.options = []
+      this.show = false
+    },
+    change(val) {
+      this.$router.push(val.path)
+      this.search = ''
+      this.options = []
+      this.$nextTick(() => {
+        this.show = false
+      })
+    },
+    initFuse(list) {
+      this.fuse = new Fuse(list, {
+        shouldSort: true,
+        threshold: 0.4,
+        location: 0,
+        distance: 100,
+        maxPatternLength: 32,
+        minMatchCharLength: 1,
+        keys: [{
+          name: 'title',
+          weight: 0.7
+        }, {
+          name: 'path',
+          weight: 0.3
+        }]
+      })
+    },
+    // Filter out the routes that can be displayed in the sidebar
+    // And generate the internationalized title
+    generateRoutes(routes, basePath = '/', prefixTitle = []) {
+      let res = []
+
+      for (const router of routes) {
+        // skip hidden router
+        if (router.hidden) { continue }
+
+        const data = {
+          path: path.resolve(basePath, router.path),
+          title: [...prefixTitle]
+        }
+
+        if (router.meta && router.meta.title) {
+          data.title = [...data.title, router.meta.title]
+
+          if (router.redirect !== 'noRedirect') {
+            // only push the routes with title
+            // special case: need to exclude parent router without redirect
+            res.push(data)
+          }
+        }
+
+        // recursive child routes
+        if (router.children) {
+          const tempRoutes = this.generateRoutes(router.children, data.path, data.title)
+          if (tempRoutes.length >= 1) {
+            res = [...res, ...tempRoutes]
+          }
+        }
+      }
+      return res
+    },
+    querySearch(query) {
+      if (query !== '') {
+        this.options = this.fuse.search(query)
+      } else {
+        this.options = []
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.header-search {
+  font-size: 0 !important;
+
+  .search-icon {
+    cursor: pointer;
+    font-size: 18px;
+    vertical-align: middle;
+  }
+
+  .header-search-select {
+    font-size: 18px;
+    transition: width 0.2s;
+    width: 0;
+    overflow: hidden;
+    background: transparent;
+    border-radius: 0;
+    display: inline-block;
+    vertical-align: middle;
+
+    ::v-deep .el-input__inner {
+      border-radius: 0;
+      border: 0;
+      padding-left: 0;
+      padding-right: 0;
+      box-shadow: none !important;
+      border-bottom: 1px solid #d9d9d9;
+      vertical-align: middle;
+    }
+  }
+
+  &.show {
+    .header-search-select {
+      width: 210px;
+      margin-left: 10px;
+    }
+  }
+}
+</style>

+ 72 - 0
src/components/MeetRole/index.vue

@@ -0,0 +1,72 @@
+<template>
+
+  <el-select
+    v-model="values"
+    :remote-method="fetchList"
+    style="width: 100%"
+    multiple
+    filterable
+    remote
+    reserve-keyword
+    clearable
+    automatic-dropdown
+    placeholder="请选择角色"
+    @change="handlerChange"
+  >
+    <el-option
+      v-for="item in list"
+      :key="item.id"
+      :label="item.title"
+      :value="item.id"
+    />
+  </el-select>
+
+</template>
+
+<script>
+
+import { fetchList } from '@/api/sys/role/role'
+
+export default {
+  name: 'MeetRole',
+  props: {
+    // eslint-disable-next-line vue/require-default-prop
+    value: Array,
+    // eslint-disable-next-line vue/require-default-prop
+    default: Array
+  },
+  data() {
+    return {
+      // 下拉选项值
+      list: [],
+      values: []
+    }
+  },
+
+  watch: {
+    // 检测查询变化
+    value: {
+      handler() {
+        this.values = this.value
+      },
+      deep: true
+    }
+  },
+  created() {
+    this.values = this.value
+    this.fetchList()
+  },
+  methods: {
+
+    fetchList() {
+      fetchList().then(response => {
+        this.list = response.data
+      })
+    },
+    handlerChange(e) {
+      this.$emit('change', e)
+      this.$emit('input', e)
+    }
+  }
+}
+</script>

+ 101 - 0
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/scroll-to'
+
+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>

+ 142 - 0
src/components/PanThumb/index.vue

@@ -0,0 +1,142 @@
+<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>
+    <!-- eslint-disable-next-line -->
+    <div :style="{backgroundImage: `url(${image})`}" class="pan-thumb"></div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'PanThumb',
+  props: {
+    image: {
+      type: String,
+      required: true
+    },
+    zIndex: {
+      type: Number,
+      default: 1
+    },
+    width: {
+      type: String,
+      default: '150px'
+    },
+    height: {
+      type: String,
+      default: '150px'
+    }
+  }
+}
+</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-position: center center;
+  background-size: cover;
+  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>

+ 77 - 0
src/components/RepoSelect/index.vue

@@ -0,0 +1,77 @@
+<template>
+
+  <el-select
+    v-model="currentValue"
+    :multiple="multi"
+    :remote-method="fetchData"
+    filterable
+    remote
+    reserve-keyword
+    clearable
+    automatic-dropdown
+    placeholder="选择或搜索题库"
+    class="filter-item"
+    @change="handlerChange"
+  >
+    <el-option
+      v-for="item in dataList"
+      :key="item.id"
+      :label="item.title"
+      :value="item.id"
+    />
+  </el-select>
+
+</template>
+
+<script>
+
+import { fetchList } from '@/api/qu/repo'
+
+export default {
+  name: 'RepoSelect',
+  props: {
+    multi: {
+      type: Boolean,
+      default: false
+    },
+    // eslint-disable-next-line vue/require-default-prop
+    value: String
+  },
+  data() {
+    return {
+      // 下拉选项值
+      dataList: [],
+      currentValue: []
+    }
+  },
+
+  watch: {
+    // 检测查询变化
+    value: {
+      handler() {
+        this.currentValue = this.value
+      }
+    }
+  },
+  created() {
+    this.currentValue = this.value
+    this.fetchData()
+  },
+  methods: {
+
+    fetchData() {
+      fetchList({}).then(response => {
+        this.dataList = response.data
+      })
+    },
+    handlerChange(e) {
+      const obj = this.dataList.find((item) => {
+        return item.id === e
+      })
+
+      this.$emit('change', obj)
+      this.$emit('input', e)
+    }
+  }
+}
+</script>

+ 145 - 0
src/components/RightPanel/index.vue

@@ -0,0 +1,145 @@
+<template>
+  <div ref="rightPanel" :class="{show:show}" class="rightPanel-container">
+    <div class="rightPanel-background" />
+    <div class="rightPanel">
+      <div :style="{'top':buttonTop+'px','background-color':theme}" class="handle-button" @click="show=!show">
+        <i :class="show?'el-icon-close':'el-icon-setting'" />
+      </div>
+      <div class="rightPanel-items">
+        <slot />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { addClass, removeClass } from '@/utils'
+
+export default {
+  name: 'RightPanel',
+  props: {
+    clickNotClose: {
+      default: false,
+      type: Boolean
+    },
+    buttonTop: {
+      default: 250,
+      type: Number
+    }
+  },
+  data() {
+    return {
+      show: false
+    }
+  },
+  computed: {
+    theme() {
+      return this.$store.state.settings.theme
+    }
+  },
+  watch: {
+    show(value) {
+      if (value && !this.clickNotClose) {
+        this.addEventClick()
+      }
+      if (value) {
+        addClass(document.body, 'showRightPanel')
+      } else {
+        removeClass(document.body, 'showRightPanel')
+      }
+    }
+  },
+  mounted() {
+    this.insertToBody()
+  },
+  beforeDestroy() {
+    const elx = this.$refs.rightPanel
+    elx.remove()
+  },
+  methods: {
+    addEventClick() {
+      window.addEventListener('click', this.closeSidebar)
+    },
+    closeSidebar(evt) {
+      const parent = evt.target.closest('.rightPanel')
+      if (!parent) {
+        this.show = false
+        window.removeEventListener('click', this.closeSidebar)
+      }
+    },
+    insertToBody() {
+      const elx = this.$refs.rightPanel
+      const body = document.querySelector('body')
+      body.insertBefore(elx, body.firstChild)
+    }
+  }
+}
+</script>
+
+<style>
+.showRightPanel {
+  overflow: hidden;
+  position: relative;
+  width: calc(100% - 15px);
+}
+</style>
+
+<style lang="scss" scoped>
+.rightPanel-background {
+  position: fixed;
+  top: 0;
+  left: 0;
+  opacity: 0;
+  transition: opacity .3s cubic-bezier(.7, .3, .1, 1);
+  background: rgba(0, 0, 0, .2);
+  z-index: -1;
+}
+
+.rightPanel {
+  width: 100%;
+  max-width: 260px;
+  height: 100vh;
+  position: fixed;
+  top: 0;
+  right: 0;
+  box-shadow: 0px 0px 15px 0px rgba(0, 0, 0, .05);
+  transition: all .25s cubic-bezier(.7, .3, .1, 1);
+  transform: translate(100%);
+  background: #fff;
+  z-index: 40000;
+}
+
+.show {
+  transition: all .3s cubic-bezier(.7, .3, .1, 1);
+
+  .rightPanel-background {
+    z-index: 20000;
+    opacity: 1;
+    width: 100%;
+    height: 100%;
+  }
+
+  .rightPanel {
+    transform: translate(0);
+  }
+}
+
+.handle-button {
+  width: 48px;
+  height: 48px;
+  position: absolute;
+  left: -48px;
+  text-align: center;
+  font-size: 24px;
+  border-radius: 6px 0 0 6px !important;
+  z-index: 0;
+  pointer-events: auto;
+  cursor: pointer;
+  color: #fff;
+  line-height: 48px;
+  i {
+    font-size: 24px;
+    line-height: 48px;
+  }
+}
+</style>

+ 60 - 0
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: 'you browser can not work',
+          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>

+ 56 - 0
src/components/SizeSelect/index.vue

@@ -0,0 +1,56 @@
+<template>
+  <el-dropdown trigger="click" @command="handleSetSize">
+    <div>
+      <svg-icon class-name="size-icon" icon-class="size" />
+    </div>
+    <el-dropdown-menu slot="dropdown">
+      <el-dropdown-item v-for="item of sizeOptions" :key="item.value" :disabled="size===item.value" :command="item.value">
+        {{ item.label }}
+      </el-dropdown-item>
+    </el-dropdown-menu>
+  </el-dropdown>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      sizeOptions: [
+        { label: '默认', value: 'default' },
+        { label: '中等', value: 'medium' },
+        { label: '小', value: 'small' },
+        { label: '极小', value: 'mini' }
+      ]
+    }
+  },
+  computed: {
+    size() {
+      return this.$store.getters.size
+    }
+  },
+  methods: {
+    handleSetSize(size) {
+      this.$ELEMENT.size = size
+      this.$store.dispatch('app/setSize', size)
+      this.refreshView()
+      this.$message({
+        message: '字体切换成功!',
+        type: 'success'
+      })
+    },
+    refreshView() {
+      // In order to make the cached page re-rendered
+      this.$store.dispatch('tagsView/delAllCachedViews', this.$route)
+
+      const { fullPath } = this.$route
+
+      this.$nextTick(() => {
+        this.$router.replace({
+          path: '/redirect' + fullPath
+        })
+      })
+    }
+  }
+
+}
+</script>

+ 62 - 0
src/components/SvgIcon/index.vue

@@ -0,0 +1,62 @@
+<template>
+  <div v-if="isExternal" :style="styleExternalIcon" class="svg-external-icon svg-icon" v-on="$listeners" />
+  <svg v-else :class="svgClass" aria-hidden="true" v-on="$listeners">
+    <use :href="iconName" />
+  </svg>
+</template>
+
+<script>
+// doc: https://panjiachen.github.io/vue-element-admin-site/feature/component/svg-icon.html#usage
+import { isExternal } from '@/utils/validate'
+
+export default {
+  name: 'SvgIcon',
+  props: {
+    iconClass: {
+      type: String,
+      required: true
+    },
+    className: {
+      type: String,
+      default: ''
+    }
+  },
+  computed: {
+    isExternal() {
+      return isExternal(this.iconClass)
+    },
+    iconName() {
+      return `#icon-${this.iconClass}`
+    },
+    svgClass() {
+      if (this.className) {
+        return 'svg-icon ' + this.className
+      } else {
+        return 'svg-icon'
+      }
+    },
+    styleExternalIcon() {
+      return {
+        mask: `url(${this.iconClass}) no-repeat 50% 50%`,
+        '-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.svg-icon {
+  width: 1em;
+  height: 1em;
+  vertical-align: -0.15em;
+  fill: currentColor;
+  overflow: hidden;
+}
+
+.svg-external-icon {
+  background-color: currentColor;
+  mask-size: cover!important;
+  display: inline-block;
+}
+</style>

+ 175 - 0
src/components/ThemePicker/index.vue

@@ -0,0 +1,175 @@
+<template>
+  <el-color-picker
+    v-model="theme"
+    :predefine="['#409EFF', '#1890ff', '#304156','#212121','#11a983', '#13c2c2', '#6959CD', '#f5222d', ]"
+    class="theme-picker"
+    popper-class="theme-picker-dropdown"
+  />
+</template>
+
+<script>
+const version = require('element-ui/package.json').version // element-ui version from node_modules
+const ORIGINAL_THEME = '#409EFF' // default color
+
+export default {
+  data() {
+    return {
+      chalk: '', // content of theme-chalk css
+      theme: ''
+    }
+  },
+  computed: {
+    defaultTheme() {
+      return this.$store.state.settings.theme
+    }
+  },
+  watch: {
+    defaultTheme: {
+      handler: function(val, oldVal) {
+        this.theme = val
+      },
+      immediate: true
+    },
+    async theme(val) {
+      const oldVal = this.chalk ? this.theme : ORIGINAL_THEME
+      if (typeof val !== 'string') return
+      const themeCluster = this.getThemeCluster(val.replace('#', ''))
+      const originalCluster = this.getThemeCluster(oldVal.replace('#', ''))
+      console.log(themeCluster, originalCluster)
+
+      const $message = this.$message({
+        message: '  Compiling the theme',
+        customClass: 'theme-message',
+        type: 'success',
+        duration: 0,
+        iconClass: 'el-icon-loading'
+      })
+
+      const getHandler = (variable, id) => {
+        return () => {
+          const originalCluster = this.getThemeCluster(ORIGINAL_THEME.replace('#', ''))
+          const newStyle = this.updateStyle(this[variable], originalCluster, themeCluster)
+
+          let styleTag = document.getElementById(id)
+          if (!styleTag) {
+            styleTag = document.createElement('style')
+            styleTag.setAttribute('id', id)
+            document.head.appendChild(styleTag)
+          }
+          styleTag.innerText = newStyle
+        }
+      }
+
+      if (!this.chalk) {
+        const url = `https://unpkg.com/element-ui@${version}/lib/theme-chalk/index.css`
+        await this.getCSSString(url, 'chalk')
+      }
+
+      const chalkHandler = getHandler('chalk', 'chalk-style')
+
+      chalkHandler()
+
+      const styles = [].slice.call(document.querySelectorAll('style'))
+        .filter(style => {
+          const text = style.innerText
+          return new RegExp(oldVal, 'i').test(text) && !/Chalk Variables/.test(text)
+        })
+      styles.forEach(style => {
+        const { innerText } = style
+        if (typeof innerText !== 'string') return
+        style.innerText = this.updateStyle(innerText, originalCluster, themeCluster)
+      })
+
+      this.$emit('change', val)
+
+      $message.close()
+    }
+  },
+
+  methods: {
+    updateStyle(style, oldCluster, newCluster) {
+      let newStyle = style
+      oldCluster.forEach((color, index) => {
+        newStyle = newStyle.replace(new RegExp(color, 'ig'), newCluster[index])
+      })
+      return newStyle
+    },
+
+    getCSSString(url, variable) {
+      return new Promise(resolve => {
+        const xhr = new XMLHttpRequest()
+        xhr.onreadystatechange = () => {
+          if (xhr.readyState === 4 && xhr.status === 200) {
+            this[variable] = xhr.responseText.replace(/@font-face{[^}]+}/, '')
+            resolve()
+          }
+        }
+        xhr.open('GET', url)
+        xhr.send()
+      })
+    },
+
+    getThemeCluster(theme) {
+      const tintColor = (color, tint) => {
+        let red = parseInt(color.slice(0, 2), 16)
+        let green = parseInt(color.slice(2, 4), 16)
+        let blue = parseInt(color.slice(4, 6), 16)
+
+        if (tint === 0) { // when primary color is in its rgb space
+          return [red, green, blue].join(',')
+        } else {
+          red += Math.round(tint * (255 - red))
+          green += Math.round(tint * (255 - green))
+          blue += Math.round(tint * (255 - blue))
+
+          red = red.toString(16)
+          green = green.toString(16)
+          blue = blue.toString(16)
+
+          return `#${red}${green}${blue}`
+        }
+      }
+
+      const shadeColor = (color, shade) => {
+        let red = parseInt(color.slice(0, 2), 16)
+        let green = parseInt(color.slice(2, 4), 16)
+        let blue = parseInt(color.slice(4, 6), 16)
+
+        red = Math.round((1 - shade) * red)
+        green = Math.round((1 - shade) * green)
+        blue = Math.round((1 - shade) * blue)
+
+        red = red.toString(16)
+        green = green.toString(16)
+        blue = blue.toString(16)
+
+        return `#${red}${green}${blue}`
+      }
+
+      const clusters = [theme]
+      for (let i = 0; i <= 9; i++) {
+        clusters.push(tintColor(theme, Number((i / 10).toFixed(2))))
+      }
+      clusters.push(shadeColor(theme, 0.1))
+      return clusters
+    }
+  }
+}
+</script>
+
+<style>
+.theme-message,
+.theme-picker-dropdown {
+  z-index: 99999 !important;
+}
+
+.theme-picker .el-color-picker__trigger {
+  height: 26px !important;
+  width: 26px !important;
+  padding: 2px;
+}
+
+.theme-picker-dropdown .el-color-dropdown__link-btn {
+  display: none;
+}
+</style>

+ 49 - 0
src/directive/clipboard/clipboard.js

@@ -0,0 +1,49 @@
+// Inspired by https://github.com/Inndy/vue-clipboard2
+const Clipboard = require('clipboard')
+if (!Clipboard) {
+  throw new Error('you should npm install `clipboard` --save at first ')
+}
+
+export default {
+  bind(el, binding) {
+    if (binding.arg === 'success') {
+      el._v_clipboard_success = binding.value
+    } else if (binding.arg === 'error') {
+      el._v_clipboard_error = binding.value
+    } else {
+      const clipboard = new Clipboard(el, {
+        text() { return binding.value },
+        action() { return binding.arg === 'cut' ? 'cut' : 'copy' }
+      })
+      clipboard.on('success', e => {
+        const callback = el._v_clipboard_success
+        callback && callback(e) // eslint-disable-line
+      })
+      clipboard.on('error', e => {
+        const callback = el._v_clipboard_error
+        callback && callback(e) // eslint-disable-line
+      })
+      el._v_clipboard = clipboard
+    }
+  },
+  update(el, binding) {
+    if (binding.arg === 'success') {
+      el._v_clipboard_success = binding.value
+    } else if (binding.arg === 'error') {
+      el._v_clipboard_error = binding.value
+    } else {
+      el._v_clipboard.text = function() { return binding.value }
+      el._v_clipboard.action = function() { return binding.arg === 'cut' ? 'cut' : 'copy' }
+    }
+  },
+  unbind(el, binding) {
+    if (binding.arg === 'success') {
+      delete el._v_clipboard_success
+    } else if (binding.arg === 'error') {
+      delete el._v_clipboard_error
+    } else {
+      el._v_clipboard.destroy()
+      delete el._v_clipboard
+    }
+  }
+}

+ 13 - 0
src/directive/clipboard/index.js

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

+ 13 - 0
src/directive/permission/index.js

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

+ 22 - 0
src/directive/permission/permission.js

@@ -0,0 +1,22 @@
+import store from '@/store'
+
+export default {
+  inserted(el, binding, vnode) {
+    const { value } = binding
+    const roles = store.getters && store.getters.roles
+
+    if (value && value instanceof Array && value.length > 0) {
+      const permissionRoles = value
+
+      const hasPermission = roles.some(role => {
+        return permissionRoles.includes(role)
+      })
+
+      if (!hasPermission) {
+        el.parentNode && el.parentNode.removeChild(el)
+      }
+    } else {
+      throw new Error(`need roles! Like v-permission="['admin','editor']"`)
+    }
+  }
+}

+ 91 - 0
src/directive/sticky.js

@@ -0,0 +1,91 @@
+const vueSticky = {}
+let listenAction
+vueSticky.install = Vue => {
+  Vue.directive('sticky', {
+    inserted(el, binding) {
+      const params = binding.value || {}
+      const stickyTop = params.stickyTop || 0
+      const zIndex = params.zIndex || 1000
+      const elStyle = el.style
+
+      elStyle.position = '-webkit-sticky'
+      elStyle.position = 'sticky'
+      // if the browser support css sticky(Currently Safari, Firefox and Chrome Canary)
+      // if (~elStyle.position.indexOf('sticky')) {
+      //     elStyle.top = `${stickyTop}px`;
+      //     elStyle.zIndex = zIndex;
+      //     return
+      // }
+      const elHeight = el.getBoundingClientRect().height
+      const elWidth = el.getBoundingClientRect().width
+      elStyle.cssText = `top: ${stickyTop}px; z-index: ${zIndex}`
+
+      const parentElm = el.parentNode || document.documentElement
+      const placeholder = document.createElement('div')
+      placeholder.style.display = 'none'
+      placeholder.style.width = `${elWidth}px`
+      placeholder.style.height = `${elHeight}px`
+      parentElm.insertBefore(placeholder, el)
+
+      let active = false
+
+      const getScroll = (target, top) => {
+        const prop = top ? 'pageYOffset' : 'pageXOffset'
+        const method = top ? 'scrollTop' : 'scrollLeft'
+        let ret = target[prop]
+        if (typeof ret !== 'number') {
+          ret = window.document.documentElement[method]
+        }
+        return ret
+      }
+
+      const sticky = () => {
+        if (active) {
+          return
+        }
+        if (!elStyle.height) {
+          elStyle.height = `${el.offsetHeight}px`
+        }
+
+        elStyle.position = 'fixed'
+        elStyle.width = `${elWidth}px`
+        placeholder.style.display = 'inline-block'
+        active = true
+      }
+
+      const reset = () => {
+        if (!active) {
+          return
+        }
+
+        elStyle.position = ''
+        placeholder.style.display = 'none'
+        active = false
+      }
+
+      const check = () => {
+        const scrollTop = getScroll(window, true)
+        const offsetTop = el.getBoundingClientRect().top
+        if (offsetTop < stickyTop) {
+          sticky()
+        } else {
+          if (scrollTop < elHeight + stickyTop) {
+            reset()
+          }
+        }
+      }
+      listenAction = () => {
+        check()
+      }
+
+      window.addEventListener('scroll', listenAction)
+    },
+
+    unbind() {
+      window.removeEventListener('scroll', listenAction)
+    }
+  })
+}
+
+export default vueSticky
+

+ 13 - 0
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
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
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]
+  }
+}

+ 60 - 0
src/filters/index.js

@@ -0,0 +1,60 @@
+// import parseTime, formatTime and set to filter
+export { parseTime, formatTime } from '@/utils'
+
+/**
+ * Upper case first char
+ * @param {String} string
+ */
+export function uppercaseFirst(string) {
+  return string.charAt(0).toUpperCase() + string.slice(1)
+}
+
+/**
+ * 通用状态过滤器
+ * @param value
+ * @returns {*}
+ */
+export function stateFilter(value) {
+  const map = {
+    '0': '正常',
+    '1': '禁用'
+  }
+  return map[value]
+}
+
+export function quTypeFilter(value) {
+  const map = {
+    '1': '单选题',
+    '2': '多选题',
+    '3': '判断题'
+  }
+  return map[value]
+}
+
+export function paperStateFilter(value) {
+  const map = {
+    '0': '考试中',
+    '1': '待阅卷',
+    '2': '已考完',
+    '3': '!已弃考'
+  }
+  return map[value]
+}
+
+export function examOpenType(value) {
+  const map = {
+    '1': '完全公开',
+    '2': '指定部门'
+  }
+  return map[value]
+}
+
+export function examStateFilter(value) {
+  const map = {
+    '0': '进行中',
+    '1': '已禁用',
+    '2': '待开始',
+    '3': '已结束'
+  }
+  return map[value]
+}

+ 9 - 0
src/icons/index.js

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

+ 1 - 0
src/icons/svg/404.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M121.718 73.272v9.953c3.957-7.584 6.199-16.05 6.199-24.995C127.917 26.079 99.273 0 63.958 0 28.644 0 0 26.079 0 58.23c0 .403.028.806.028 1.21l22.97-25.953h13.34l-19.76 27.187h6.42V53.77l13.728-19.477v49.361H22.998V73.272H2.158c5.951 20.284 23.608 36.208 45.998 41.399-1.44 3.3-5.618 11.263-12.565 12.674-8.607 1.764 23.358.428 46.163-13.178 17.519-4.611 31.938-15.849 39.77-30.513h-13.506V73.272H85.02V59.464l22.998-25.977h13.008l-19.429 27.187h6.421v-7.433l13.727-19.402v39.433h-.027zm-78.24 2.822a10.516 10.516 0 0 1-.996-4.535V44.548c0-1.613.332-3.124.996-4.535a11.66 11.66 0 0 1 2.713-3.68c1.134-1.032 2.49-1.864 4.04-2.468 1.55-.605 3.21-.908 4.982-.908h11.292c1.77 0 3.431.303 4.981.908 1.522.604 2.85 1.41 3.986 2.418l-12.26 16.303v-2.898a1.96 1.96 0 0 0-.665-1.512c-.443-.403-.996-.604-1.66-.604-.665 0-1.218.201-1.661.604a1.96 1.96 0 0 0-.664 1.512v9.071L44.364 77.606a10.556 10.556 0 0 1-.886-1.512zm35.73-4.535c0 1.613-.332 3.124-.997 4.535a11.66 11.66 0 0 1-2.712 3.68c-1.134 1.032-2.49 1.864-4.04 2.469-1.55.604-3.21.907-4.982.907H55.185c-1.77 0-3.431-.303-4.981-.907-1.55-.605-2.906-1.437-4.041-2.47a12.49 12.49 0 0 1-1.384-1.512l13.727-18.217v6.375c0 .605.222 1.109.665 1.512.442.403.996.604 1.66.604.664 0 1.218-.201 1.66-.604a1.96 1.96 0 0 0 .665-1.512V53.87L75.97 36.838c.913.932 1.66 1.99 2.214 3.175.664 1.41.996 2.922.996 4.535v27.011h.028z"/></svg>

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

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1587360706544" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3488" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><defs><style type="text/css"></style></defs><path d="M204.928 286.464a4.476 4.476 0 1 0 572.928 0 4.476 4.476 0 1 0-572.928 0z" p-id="3489"></path><path d="M869.312 523.264c-35.392-43.2-101.888-51.2-140.16-10.496C669.504 576.128 585.408 616 491.328 616c-83.264 0-158.976-31.04-216.832-82.048-43.84-38.656-112.64-23.36-145.472 25.024-49.28 72.768-78.4 160.064-78.4 254.72 0 19.072 1.28 37.76 3.648 56.064 11.904 90.56 96 154.24 187.264 154.24h539.392c100.544 0 183.744-77.824 191.232-178.048 0.768-10.624 1.216-21.376 1.216-32.192 0-110.464-39.168-211.328-104.064-290.496z" p-id="3490"></path></svg>

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
src/icons/svg/agreement.svg


+ 1 - 0
src/icons/svg/bug.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M127.88 73.143c0 1.412-.506 2.635-1.518 3.669-1.011 1.033-2.209 1.55-3.592 1.55h-17.887c0 9.296-1.783 17.178-5.35 23.645l16.609 17.044c1.011 1.034 1.517 2.257 1.517 3.67 0 1.412-.506 2.635-1.517 3.668-.958 1.033-2.155 1.55-3.593 1.55-1.438 0-2.635-.517-3.593-1.55l-15.811-16.063a15.49 15.49 0 0 1-1.196 1.06c-.532.434-1.65 1.208-3.353 2.322a50.104 50.104 0 0 1-5.192 2.974c-1.758.87-3.94 1.658-6.546 2.364-2.607.706-5.189 1.06-7.748 1.06V47.044H58.89v73.062c-2.716 0-5.417-.367-8.106-1.102-2.688-.734-5.003-1.631-6.945-2.692a66.769 66.769 0 0 1-5.268-3.179c-1.571-1.057-2.73-1.94-3.476-2.65L33.9 109.34l-14.611 16.877c-1.066 1.14-2.344 1.711-3.833 1.711-1.277 0-2.422-.434-3.434-1.304-1.012-.978-1.557-2.187-1.635-3.627-.079-1.44.333-2.705 1.236-3.794l16.129-18.51c-3.087-6.197-4.63-13.644-4.63-22.342H5.235c-1.383 0-2.58-.517-3.592-1.55S.125 74.545.125 73.132c0-1.412.506-2.635 1.518-3.668 1.012-1.034 2.21-1.55 3.592-1.55h17.887V43.939L9.308 29.833c-1.012-1.033-1.517-2.256-1.517-3.669 0-1.412.505-2.635 1.517-3.668 1.012-1.034 2.21-1.55 3.593-1.55s2.58.516 3.593 1.55l13.813 14.106h67.396l13.814-14.106c1.012-1.034 2.21-1.55 3.592-1.55 1.384 0 2.581.516 3.593 1.55 1.012 1.033 1.518 2.256 1.518 3.668 0 1.413-.506 2.636-1.518 3.67l-13.814 14.105v23.975h17.887c1.383 0 2.58.516 3.593 1.55 1.011 1.033 1.517 2.256 1.517 3.668l-.005.01zM89.552 26.175H38.448c0-7.23 2.489-13.386 7.466-18.469C50.892 2.623 56.92.082 64 .082c7.08 0 13.108 2.541 18.086 7.624 4.977 5.083 7.466 11.24 7.466 18.469z"/></svg>

+ 1 - 0
src/icons/svg/chart.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M0 54.857h36.571V128H0V54.857zM91.429 27.43H128V128H91.429V27.429zM45.714 0h36.572v128H45.714V0z"/></svg>

+ 1 - 0
src/icons/svg/clipboard.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M54.857 118.857h64V73.143H89.143c-1.902 0-3.52-.668-4.855-2.002-1.335-1.335-2.002-2.954-2.002-4.855V36.57H54.857v82.286zM73.143 16v-4.571a2.2 2.2 0 0 0-.677-1.61 2.198 2.198 0 0 0-1.609-.676H20.571c-.621 0-1.158.225-1.609.676a2.198 2.198 0 0 0-.676 1.61V16a2.2 2.2 0 0 0 .676 1.61c.451.45.988.676 1.61.676h50.285c.622 0 1.158-.226 1.61-.677.45-.45.676-.987.676-1.609zm18.286 48h21.357L91.43 42.642V64zM128 73.143v48c0 1.902-.667 3.52-2.002 4.855-1.335 1.335-2.953 2.002-4.855 2.002H52.57c-1.901 0-3.52-.667-4.854-2.002-1.335-1.335-2.003-2.953-2.003-4.855v-11.429H6.857c-1.902 0-3.52-.667-4.855-2.002C.667 106.377 0 104.759 0 102.857v-96c0-1.902.667-3.52 2.002-4.855C3.337.667 4.955 0 6.857 0h77.714c1.902 0 3.52.667 4.855 2.002 1.335 1.335 2.003 2.953 2.003 4.855V30.29c1 .622 1.856 1.29 2.569 2.003l29.147 29.147c1.335 1.335 2.478 3.145 3.429 5.43.95 2.287 1.426 4.383 1.426 6.291v-.018z"/></svg>

+ 1 - 0
src/icons/svg/component.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M0 0h54.857v54.857H0V0zm0 73.143h54.857V128H0V73.143zm73.143 0H128V128H73.143V73.143zm27.428-18.286C115.72 54.857 128 42.577 128 27.43 128 12.28 115.72 0 100.571 0 85.423 0 73.143 12.28 73.143 27.429c0 15.148 12.28 27.428 27.428 27.428z"/></svg>

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
src/icons/svg/configure.svg


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
src/icons/svg/dashboard.svg


+ 1 - 0
src/icons/svg/documentation.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M71.984 44.815H115.9L71.984 9.642v35.173zM16.094.05h63.875l47.906 38.37v76.74c0 3.392-1.682 6.645-4.677 9.044-2.995 2.399-7.056 3.746-11.292 3.746H16.094c-4.236 0-8.297-1.347-11.292-3.746-2.995-2.399-4.677-5.652-4.677-9.044V12.84C.125 5.742 7.23.05 16.094.05zm71.86 102.32V89.58h-71.86v12.79h71.86zm23.952-25.58V64H16.094v12.79h95.812z"/></svg>

+ 1 - 0
src/icons/svg/drag.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M73.137 29.08h-9.209 29.7L63.886.093 34.373 29.08h20.49v27.035H27.238v17.948h27.625v27.133h18.274V74.063h27.41V56.115h-27.41V29.08zm-9.245 98.827l27.518-26.711H36.59l27.302 26.71zM.042 64.982l27.196 27.029V38.167L.042 64.982zm100.505-26.815V92.01l27.41-27.029-27.41-26.815z"/></svg>

+ 1 - 0
src/icons/svg/edit.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M106.133 67.2a4.797 4.797 0 0 0-4.8 4.8c0 .187.014.36.027.533h-.027V118.4H9.6V26.667h50.133c2.654 0 4.8-2.147 4.8-4.8 0-2.654-2.146-4.8-4.8-4.8H9.6a9.594 9.594 0 0 0-9.6 9.6V118.4c0 5.307 4.293 9.6 9.6 9.6h91.733c5.307 0 9.6-4.293 9.6-9.6V72.533h-.026c.013-.173.026-.346.026-.533 0-2.653-2.146-4.8-4.8-4.8z"/><path d="M125.16 13.373L114.587 2.8c-3.747-3.747-9.854-3.72-13.6.027l-52.96 52.96a4.264 4.264 0 0 0-.907 1.36L33.813 88.533c-.746 1.76-.226 3.534.907 4.68 1.133 1.147 2.92 1.667 4.693.92l31.4-13.293c.507-.213.96-.52 1.36-.907l52.96-52.96c3.747-3.746 3.774-9.853.027-13.6zM66.107 72.4l-18.32 7.76 7.76-18.32L92.72 24.667l10.56 10.56L66.107 72.4zm52.226-52.227l-8.266 8.267-10.56-10.56 8.266-8.267.027-.026 10.56 10.56-.027.026z"/></svg>

+ 1 - 0
src/icons/svg/education.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M88.883 119.565c-7.284 0-19.434 2.495-21.333 8.25v.127c-4.232.13-5.222 0-7.108 0-1.895-5.76-14.045-8.256-21.333-8.256H0V0h42.523c9.179 0 17.109 5.47 21.47 13.551C68.352 5.475 76.295 0 85.478 0H128v119.57l-39.113-.005h-.004zM60.442 24.763c0-9.651-8.978-16.507-17.777-16.507H7.108V111.43H39.11c7.054-.14 18.177.082 21.333 6.12v-4.628c-.134-5.722-.004-13.522 0-13.832V27.413l.004-2.655-.004.005zm60.442-16.517h-35.55c-8.802 0-17.78 6.856-17.78 16.493v74.259c.004.32.138 8.115 0 13.813v4.627c3.155-6.022 14.279-6.26 21.333-6.114h32V8.25l-.003-.005z"/></svg>

+ 1 - 0
src/icons/svg/email.svg

@@ -0,0 +1 @@
+<svg width="128" height="96" xmlns="http://www.w3.org/2000/svg"><path d="M64.125 56.975L120.188.912A12.476 12.476 0 0 0 115.5 0h-103c-1.588 0-3.113.3-4.513.838l56.138 56.137z"/><path d="M64.125 68.287l-62.3-62.3A12.42 12.42 0 0 0 0 12.5v71C0 90.4 5.6 96 12.5 96h103c6.9 0 12.5-5.6 12.5-12.5v-71a12.47 12.47 0 0 0-1.737-6.35L64.125 68.287z"/></svg>

+ 1 - 0
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>

+ 1 - 0
src/icons/svg/excel.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M78.208 16.576v8.384h38.72v5.376h-38.72v8.704h38.72v5.376h-38.72v8.576h38.72v5.376h-38.72v8.576h38.72v5.376h-38.72v8.576h38.72v5.376h-38.72v8.512h38.72v5.376h-38.72v11.136H128v-94.72H78.208zM0 114.368L72.128 128V0L0 13.632v100.736z"/><path d="M28.672 82.56h-11.2l14.784-23.488-14.08-22.592h11.52l8.192 14.976 8.448-14.976h11.136l-14.08 22.208L58.368 82.56H46.656l-8.768-15.68z"/></svg>

+ 1 - 0
src/icons/svg/exit-fullscreen.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M49.217 41.329l-.136-35.24c-.06-2.715-2.302-4.345-5.022-4.405h-3.65c-2.712-.06-4.866 2.303-4.806 5.016l.152 19.164-24.151-23.79a6.698 6.698 0 0 0-9.499 0 6.76 6.76 0 0 0 0 9.526l23.93 23.713-18.345.074c-2.712-.069-5.228 1.813-5.64 5.02v3.462c.069 2.721 2.31 4.97 5.022 5.03l35.028-.207c.052.005.087.025.133.025l2.457.054a4.626 4.626 0 0 0 3.436-1.38c.88-.874 1.205-2.096 1.169-3.462l-.262-2.465c0-.048.182-.081.182-.136h.002zm52.523 51.212l18.32-.073c2.713.06 5.224-1.609 5.64-4.815v-3.462c-.068-2.722-2.317-4.97-5.021-5.04l-34.58.21c-.053 0-.086-.021-.138-.021l-2.451-.06a4.64 4.64 0 0 0-3.445 1.381c-.885.868-1.201 2.094-1.174 3.46l.27 2.46c.005.06-.177.095-.177.141l.141 34.697c.069 2.713 2.31 4.338 5.022 4.397l3.45.006c2.705.062 4.867-2.31 4.8-5.026l-.153-18.752 24.151 23.946a6.69 6.69 0 0 0 9.494 0 6.747 6.747 0 0 0 0-9.523L101.74 92.54v.001zM48.125 80.662a4.636 4.636 0 0 0-3.437-1.382l-2.457.06c-.05 0-.082.022-.137.022l-35.025-.21c-2.712.07-4.957 2.318-5.022 5.04v3.462c.409 3.206 2.925 4.874 5.633 4.814l18.554.06-24.132 23.928c-2.62 2.626-2.62 6.89 0 9.524a6.694 6.694 0 0 0 9.496 0l24.155-23.79-.155 18.866c-.06 2.722 2.094 5.093 4.801 5.025h3.65c2.72-.069 4.962-1.685 5.022-4.406l.141-34.956c0-.05-.182-.082-.182-.136l.262-2.46c.03-1.366-.286-2.592-1.166-3.46h-.001zM80.08 47.397a4.62 4.62 0 0 0 3.443 1.374l2.45-.054c.055 0 .088-.02.143-.028l35.08.21c2.712-.062 4.953-2.312 5.021-5.033l.009-3.463c-.417-3.211-2.937-5.084-5.64-5.025l-18.615-.073 23.917-23.715c2.63-2.623 2.63-6.879.008-9.513a6.691 6.691 0 0 0-9.494 0L92.251 26.016l.155-19.312c.065-2.713-2.097-5.085-4.802-5.025h-3.45c-2.713.069-4.954 1.693-5.022 4.406l-.139 35.247c0 .054.18.088.18.136l-.267 2.465c-.028 1.366.288 2.588 1.174 3.463v.001z"/></svg>

+ 1 - 0
src/icons/svg/eye-open.svg

@@ -0,0 +1 @@
+<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="128" height="128"><defs><style/></defs><path d="M512 128q69.675 0 135.51 21.163t115.498 54.997 93.483 74.837 73.685 82.006 51.67 74.837 32.17 54.827L1024 512q-2.347 4.992-6.315 13.483T998.87 560.17t-31.658 51.669-44.331 59.99-56.832 64.34-69.504 60.16-82.347 51.5-94.848 34.687T512 896q-69.675 0-135.51-21.163t-115.498-54.826-93.483-74.326-73.685-81.493-51.67-74.496-32.17-54.997L0 513.707q2.347-4.992 6.315-13.483t18.816-34.816 31.658-51.84 44.331-60.33 56.832-64.683 69.504-60.331 82.347-51.84 94.848-34.816T512 128.085zm0 85.333q-46.677 0-91.648 12.331t-81.152 31.83-70.656 47.146-59.648 54.485-48.853 57.686-37.675 52.821-26.325 43.99q12.33 21.674 26.325 43.52t37.675 52.351 48.853 57.003 59.648 53.845T339.2 767.02t81.152 31.488T512 810.667t91.648-12.331 81.152-31.659 70.656-46.848 59.648-54.186 48.853-57.344 37.675-52.651T927.957 512q-12.33-21.675-26.325-43.648t-37.675-52.65-48.853-57.345-59.648-54.186-70.656-46.848-81.152-31.659T512 213.334zm0 128q70.656 0 120.661 50.006T682.667 512 632.66 632.661 512 682.667 391.339 632.66 341.333 512t50.006-120.661T512 341.333zm0 85.334q-35.328 0-60.33 25.002T426.666 512t25.002 60.33T512 597.334t60.33-25.002T597.334 512t-25.002-60.33T512 426.666z"/></svg>

+ 1 - 0
src/icons/svg/eye.svg

@@ -0,0 +1 @@
+<svg width="128" height="64" xmlns="http://www.w3.org/2000/svg"><path d="M127.072 7.994c1.37-2.208.914-5.152-.914-6.87-2.056-1.717-4.797-1.226-6.396.982-.229.245-25.586 32.382-55.74 32.382-29.24 0-55.74-32.382-55.968-32.627-1.6-1.963-4.57-2.208-6.397-.49C-.17 3.086-.399 6.275 1.2 8.238c.457.736 5.94 7.36 14.62 14.72L4.17 35.96c-1.828 1.963-1.6 5.152.228 6.87.457.98 1.6 1.471 2.742 1.471s2.284-.49 3.198-1.472l12.564-13.983c5.94 4.416 13.021 8.587 20.788 11.53l-4.797 17.418c-.685 2.699.686 5.397 3.198 6.133h1.37c2.057 0 3.884-1.472 4.341-3.68L52.6 42.83c3.655.736 7.538 1.227 11.422 1.227 3.883 0 7.767-.49 11.422-1.227l4.797 17.173c.457 2.208 2.513 3.68 4.34 3.68.457 0 .914 0 1.143-.246 2.513-.736 3.883-3.434 3.198-6.133l-4.797-17.172c7.767-2.944 14.848-7.114 20.788-11.53l12.336 13.738c.913.981 2.056 1.472 3.198 1.472s2.284-.49 3.198-1.472c1.828-1.963 1.828-4.906.228-6.87l-11.65-13.001c9.366-7.36 14.849-14.474 14.849-14.474z"/></svg>

+ 5 - 0
src/icons/svg/fire.svg

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg t="1587202500000" class="icon" viewBox="0 0 1024 1024" version="1.1" p-id="1320" width="128" height="128" xmlns="http://www.w3.org/2000/svg">
+  <defs/>
+  <path d="M 289.01 1024 C 211.008 881.997 252.538 800.634 312.492 723.96 C 378.158 640.015 395.085 556.89 395.085 556.89 C 395.085 556.89 446.704 615.584 426.057 707.392 C 517.24 618.594 534.446 477.124 520.681 422.936 C 726.787 548.944 814.881 821.768 696.166 1023.98 C 1327.567 711.467 853.225 243.846 770.635 191.153 C 798.165 243.825 803.385 332.991 747.763 376.264 C 653.627 63.997 420.882 0 420.882 0 C 448.414 161.028 321.084 337.128 198.321 468.707 C 194.014 404.484 189.424 360.188 150.82 298.73 C 142.159 415.379 40.254 510.464 12.654 627.339 C -24.734 785.622 40.651 901.514 288.989 1023.939 Z" p-id="1321" style=""/>
+</svg>

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
src/icons/svg/form.svg


+ 1 - 0
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>

+ 1 - 0
src/icons/svg/guide.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M1.482 70.131l36.204 16.18 69.932-65.485-61.38 70.594 46.435 18.735c1.119.425 2.397-.17 2.797-1.363v-.085L127.998.047 1.322 65.874c-1.12.597-1.519 1.959-1.04 3.151.32.511.72.937 1.2 1.107zm44.676 57.821L64.22 107.26l-18.062-7.834v28.527z"/></svg>

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
src/icons/svg/hot.svg


+ 1 - 0
src/icons/svg/icon.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M115.147.062a13 13 0 0 1 4.94.945c1.55.63 2.907 1.526 4.069 2.688a13.148 13.148 0 0 1 2.761 4.069c.678 1.55 1.017 3.245 1.017 5.086v102.3c0 3.681-1.187 6.733-3.56 9.155-2.373 2.422-5.352 3.633-8.937 3.633H12.992c-3.875 0-7-1.26-9.373-3.779-2.373-2.518-3.56-5.667-3.56-9.445V12.704c0-3.39 1.163-6.345 3.488-8.863C5.872 1.32 8.972.062 12.847.062h102.3zM81.434 109.047c1.744 0 3.003-.412 3.778-1.235.775-.824 1.163-1.914 1.163-3.27 0-1.26-.388-2.325-1.163-3.197-.775-.872-2.034-1.307-3.778-1.307H72.57c.097-.194.145-.485.145-.872V27.09h9.01c1.743 0 2.954-.436 3.633-1.308.678-.872 1.017-1.938 1.017-3.197 0-1.26-.34-2.325-1.017-3.197-.679-.872-1.89-1.308-3.633-1.308H46.268c-1.743 0-2.954.436-3.632 1.308-.678.872-1.018 1.938-1.018 3.197 0 1.26.34 2.325 1.018 3.197.678.872 1.889 1.308 3.632 1.308h8.138v72.075c0 .193.024.339.073.436.048.096.072.242.072.436H46.56c-1.744 0-3.003.435-3.778 1.307-.775.872-1.163 1.938-1.163 3.197 0 1.356.388 2.446 1.163 3.27.775.823 2.034 1.235 3.778 1.235h34.875z"/></svg>

+ 1 - 0
src/icons/svg/international.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M83.287 103.01c-1.57-3.84-6.778-10.414-15.447-19.548-2.327-2.444-2.182-4.306-1.338-9.862v-.64c.553-3.81 1.513-6.05 14.313-8.087 6.516-1.018 8.203 1.57 10.589 5.178l.785 1.193a12.625 12.625 0 0 0 6.43 5.207c1.134.524 2.53 1.164 4.421 2.24 4.596 2.53 4.596 5.41 4.596 11.753v.727a26.91 26.91 0 0 1-5.178 17.454 59.055 59.055 0 0 1-19.025 11.026c3.49-6.546.814-14.313 0-16.553l-.146-.087zM64 5.12a58.502 58.502 0 0 1 25.484 5.818 54.313 54.313 0 0 0-12.859 10.327c-.93 1.28-1.716 2.473-2.472 3.579-2.444 3.694-3.637 5.352-5.818 5.614a25.105 25.105 0 0 1-4.219 0c-4.276-.29-10.094-.64-11.956 4.422-1.193 3.23-1.396 11.956 2.444 16.495.66 1.077.778 2.4.32 3.578a7.01 7.01 0 0 1-2.066 3.229 18.938 18.938 0 0 1-2.909-2.91 18.91 18.91 0 0 0-8.32-6.603c-1.25-.349-2.647-.64-3.985-.93-3.782-.786-8.03-1.688-9.019-3.812a14.895 14.895 0 0 1-.727-5.818 21.935 21.935 0 0 0-1.396-9.25 8.873 8.873 0 0 0-5.557-4.946A58.705 58.705 0 0 1 64 5.12zM0 64c0 35.346 28.654 64 64 64 35.346 0 64-28.654 64-64 0-35.346-28.654-64-64-64C28.654 0 0 28.654 0 64z"/></svg>

+ 1 - 0
src/icons/svg/language.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M84.742 36.8c2.398 7.2 5.595 12.8 11.19 18.4 4.795-4.8 7.992-11.2 10.39-18.4h-21.58zm-52.748 40h20.78l-10.39-28-10.39 28z"/><path d="M111.916 0H16.009C7.218 0 .025 7.2.025 16v96c0 8.8 7.193 16 15.984 16h95.907c8.791 0 15.984-7.2 15.984-16V16c0-8.8-6.394-16-15.984-16zM72.754 103.2c-1.598 1.6-3.197 1.6-4.795 1.6-.8 0-2.398 0-3.197-.8-.8-.8-1.599 0-1.599-.8s-.799-1.6-1.598-3.2c-.8-1.6-.8-2.4-1.599-4l-3.196-8.8H28.797L25.6 96c-1.598 3.2-2.398 5.6-3.197 7.2-.8 1.6-2.398 1.6-4.795 1.6-1.599 0-3.197-.8-4.796-1.6-1.598-1.6-2.397-2.4-2.397-4 0-.8 0-1.6.799-3.2.8-1.6.8-2.4 1.598-4l17.583-44.8c.8-1.6.8-3.2 1.599-4.8.799-1.6 1.598-3.2 2.397-4 .8-.8 1.599-2.4 3.197-3.2 1.599-.8 3.197-.8 4.796-.8 1.598 0 3.196 0 4.795.8 1.598.8 2.398 1.6 3.197 3.2.799.8 1.598 2.4 2.397 4 .8 1.6 1.599 3.2 2.398 5.6l17.583 44c1.598 3.2 2.398 5.6 2.398 7.2-.8.8-1.599 2.4-2.398 4zM116.711 72c-8.791-3.2-15.185-7.2-20.78-12-5.594 5.6-12.787 9.6-21.579 12l-2.397-4c8.791-2.4 15.984-5.6 21.579-11.2C87.939 51.2 83.144 44 81.545 36h-7.992v-3.2h21.58c-1.6-2.4-3.198-5.6-4.796-8l2.397-.8c1.599 2.4 3.997 5.6 5.595 8.8h19.98v4h-7.992c-2.397 8-6.393 15.2-11.189 20 5.595 4.8 11.988 8.8 20.78 11.2l-3.197 4z"/></svg>

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

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><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"/></svg>

+ 1 - 0
src/icons/svg/list.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M1.585 12.087c0 6.616 3.974 11.98 8.877 11.98 4.902 0 8.877-5.364 8.877-11.98 0-6.616-3.975-11.98-8.877-11.98-4.903 0-8.877 5.364-8.877 11.98zM125.86.107H35.613c-1.268 0-2.114 1.426-2.114 2.852v18.255c0 1.712 1.057 2.853 2.114 2.853h90.247c1.268 0 2.114-1.426 2.114-2.853V2.96c0-1.711-1.057-2.852-2.114-2.852zM.106 62.86c0 6.615 3.974 11.979 8.876 11.979 4.903 0 8.877-5.364 8.877-11.98 0-6.616-3.974-11.98-8.877-11.98-4.902 0-8.876 5.364-8.876 11.98zM124.17 50.88H33.921c-1.268 0-2.114 1.425-2.114 2.851v18.256c0 1.711 1.057 2.852 2.114 2.852h90.247c1.268 0 2.114-1.426 2.114-2.852V53.73c0-1.426-.846-2.852-2.114-2.852zM.106 115.913c0 6.616 3.974 11.98 8.876 11.98 4.903 0 8.877-5.364 8.877-11.98 0-6.616-3.974-11.98-8.877-11.98-4.902 0-8.876 5.364-8.876 11.98zm124.064-11.98H33.921c-1.268 0-2.114 1.426-2.114 2.853v18.255c0 1.711 1.057 2.852 2.114 2.852h90.247c1.268 0 2.114-1.426 2.114-2.852v-18.255c0-1.427-.846-2.853-2.114-2.853z"/></svg>

+ 1 - 0
src/icons/svg/lock.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M119.88 49.674h-7.987V39.52C111.893 17.738 90.45.08 63.996.08 37.543.08 16.1 17.738 16.1 39.52v10.154H8.113c-4.408 0-7.987 2.94-7.987 6.577v65.13c0 3.637 3.57 6.577 7.987 6.577H119.88c4.407 0 7.987-2.94 7.987-6.577v-65.13c-.008-3.636-3.58-6.577-7.987-6.577zm-23.953 0H32.065V39.52c0-14.524 14.301-26.295 31.931-26.295 17.63 0 31.932 11.777 31.932 26.295v10.153z"/></svg>

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
src/icons/svg/log.svg


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
src/icons/svg/map.svg


+ 1 - 0
src/icons/svg/message.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M0 20.967v59.59c0 11.59 8.537 20.966 19.075 20.966h28.613l1 26.477L76.8 101.523h32.125c10.538 0 19.075-9.377 19.075-20.966v-59.59C128 9.377 119.463 0 108.925 0h-89.85C8.538 0 0 9.377 0 20.967zm82.325 33.1c0-5.524 4.013-9.935 9.037-9.935 5.026 0 9.038 4.41 9.038 9.934 0 5.524-4.025 9.934-9.038 9.934-5.024 0-9.037-4.41-9.037-9.934zm-27.613 0c0-5.524 4.013-9.935 9.038-9.935s9.037 4.41 9.037 9.934c0 5.524-4.025 9.934-9.037 9.934-5.025 0-9.038-4.41-9.038-9.934zm-27.1 0c0-5.524 4.013-9.935 9.038-9.935s9.038 4.41 9.038 9.934c0 5.524-4.026 9.934-9.05 9.934-5.013 0-9.025-4.41-9.025-9.934z"/></svg>

+ 1 - 0
src/icons/svg/money.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M54.122 127.892v-28.68H7.513V87.274h46.609v-12.4H7.513v-12.86h38.003L.099 0h22.6l32.556 45.07c3.617 5.144 6.44 9.611 8.487 13.385 1.788-3.05 4.89-7.779 9.301-14.186L103.93 0h24.01L82.385 62.013h38.34v12.862h-46.41v12.4h46.41v11.937h-46.41v28.68H54.123z"/></svg>

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä