Browse Source

Signed-off-by: ljx <809268652@qq.com>

ljx 1 year ago
commit
ef1e8059e6
100 changed files with 8042 additions and 0 deletions
  1. 3 0
      .gitignore
  2. 136 0
      Jenkinsfile
  3. 3 0
      README.md
  4. 9 0
      csair-vr-portal-ui/.editorconfig
  5. 4 0
      csair-vr-portal-ui/.eslintignore
  6. 30 0
      csair-vr-portal-ui/.eslintrc.js
  7. 3 0
      csair-vr-portal-ui/.gitignore
  8. 10 0
      csair-vr-portal-ui/.postcssrc.js
  9. 123 0
      csair-vr-portal-ui/Jenkinsfile
  10. 17 0
      csair-vr-portal-ui/README.md
  11. 41 0
      csair-vr-portal-ui/build/build.js
  12. 54 0
      csair-vr-portal-ui/build/check-versions.js
  13. BIN
      csair-vr-portal-ui/build/logo.png
  14. 102 0
      csair-vr-portal-ui/build/utils.js
  15. 22 0
      csair-vr-portal-ui/build/vue-loader.conf.js
  16. 94 0
      csair-vr-portal-ui/build/webpack.base.conf.js
  17. 95 0
      csair-vr-portal-ui/build/webpack.dev.conf.js
  18. 145 0
      csair-vr-portal-ui/build/webpack.prod.conf.js
  19. 12 0
      csair-vr-portal-ui/config/dev.env.js
  20. 76 0
      csair-vr-portal-ui/config/index.js
  21. 13 0
      csair-vr-portal-ui/config/prod.env.js
  22. 111 0
      csair-vr-portal-ui/index.html
  23. 77 0
      csair-vr-portal-ui/package.json
  24. 26 0
      csair-vr-portal-ui/src/App.vue
  25. 17 0
      csair-vr-portal-ui/src/api/couresApi.js
  26. 33 0
      csair-vr-portal-ui/src/api/homeApi.js
  27. 12 0
      csair-vr-portal-ui/src/api/inquireApi.js
  28. 18 0
      csair-vr-portal-ui/src/api/newsApi.js
  29. 13 0
      csair-vr-portal-ui/src/api/searchApi.js
  30. 18 0
      csair-vr-portal-ui/src/api/vrpanoramicApi.js
  31. BIN
      csair-vr-portal-ui/src/assets/403bg.png
  32. 18 0
      csair-vr-portal-ui/src/assets/css/app.css
  33. BIN
      csair-vr-portal-ui/src/assets/images/3d-model.png
  34. BIN
      csair-vr-portal-ui/src/assets/images/backTop.png
  35. BIN
      csair-vr-portal-ui/src/assets/images/backTopBack.png
  36. BIN
      csair-vr-portal-ui/src/assets/images/course-text.png
  37. BIN
      csair-vr-portal-ui/src/assets/images/head-logo.png
  38. BIN
      csair-vr-portal-ui/src/assets/images/logo.png
  39. BIN
      csair-vr-portal-ui/src/assets/images/new-info-1.png
  40. BIN
      csair-vr-portal-ui/src/assets/images/new-info-2.png
  41. BIN
      csair-vr-portal-ui/src/assets/images/new-text.png
  42. BIN
      csair-vr-portal-ui/src/assets/images/vr-panorama.png
  43. 8 0
      csair-vr-portal-ui/src/components/layout/index.js
  44. 15 0
      csair-vr-portal-ui/src/components/layout/white-board.vue
  45. 2 0
      csair-vr-portal-ui/src/constants/constants.js
  46. 27 0
      csair-vr-portal-ui/src/filters/date-format.js
  47. 7 0
      csair-vr-portal-ui/src/filters/index.js
  48. 67 0
      csair-vr-portal-ui/src/filters/str-format.js
  49. 49 0
      csair-vr-portal-ui/src/main.js
  50. 5 0
      csair-vr-portal-ui/src/plugins/element-ui.js
  51. 1 0
      csair-vr-portal-ui/src/plugins/index.js
  52. 82 0
      csair-vr-portal-ui/src/router/index.js
  53. 319 0
      csair-vr-portal-ui/src/utils/date-kit.js
  54. 121 0
      csair-vr-portal-ui/src/utils/http-kit.js
  55. 20 0
      csair-vr-portal-ui/src/utils/str-kit.js
  56. 74 0
      csair-vr-portal-ui/src/views/course/detail.vue
  57. 104 0
      csair-vr-portal-ui/src/views/course/index.vue
  58. 80 0
      csair-vr-portal-ui/src/views/exception/403.vue
  59. 69 0
      csair-vr-portal-ui/src/views/index.vue
  60. 20 0
      csair-vr-portal-ui/src/views/login.vue
  61. 20 0
      csair-vr-portal-ui/src/views/logout.vue
  62. 227 0
      csair-vr-portal-ui/src/views/main.vue
  63. 65 0
      csair-vr-portal-ui/src/views/new/detail.vue
  64. 96 0
      csair-vr-portal-ui/src/views/new/index.vue
  65. 15 0
      csair-vr-portal-ui/src/views/redit.vue
  66. 198 0
      csair-vr-portal-ui/src/views/search.vue
  67. 146 0
      csair-vr-portal-ui/src/views/vr/index.vue
  68. 0 0
      csair-vr-portal-ui/static/.gitkeep
  69. 952 0
      csair-vr-portal-ui/static/css/quill/quill.bubble.css
  70. 397 0
      csair-vr-portal-ui/static/css/quill/quill.core.css
  71. 945 0
      csair-vr-portal-ui/static/css/quill/quill.snow.css
  72. BIN
      csair-vr-portal-ui/static/img/vr-panorama.png
  73. BIN
      lib/dom4j-2.1.1.jar
  74. BIN
      lib/tomcat-embed-websocket-9.0.22.jar
  75. 222 0
      linghang.iml
  76. 348 0
      pom.xml
  77. 6 0
      sql/20200409-create_view.sql
  78. 198 0
      src/main/java/cn/com/sailfish/linghang/GenTable.java
  79. 190 0
      src/main/java/cn/com/sailfish/linghang/GetGen.java
  80. 67 0
      src/main/java/cn/com/sailfish/linghang/LinghangApplication.java
  81. 13 0
      src/main/java/cn/com/sailfish/linghang/common/Constants.java
  82. 554 0
      src/main/java/cn/com/sailfish/linghang/common/ErrorConstants.java
  83. 165 0
      src/main/java/cn/com/sailfish/linghang/common/ErrorMessages.java
  84. 35 0
      src/main/java/cn/com/sailfish/linghang/common/Http401UnauthorizedEntryPoint.java
  85. 43 0
      src/main/java/cn/com/sailfish/linghang/common/RestRespDTO.java
  86. 45 0
      src/main/java/cn/com/sailfish/linghang/common/auditor/Auditable.java
  87. 20 0
      src/main/java/cn/com/sailfish/linghang/common/auditor/AuditorAwareImpl.java
  88. 40 0
      src/main/java/cn/com/sailfish/linghang/config/CrossConfig.java
  89. 46 0
      src/main/java/cn/com/sailfish/linghang/config/FileUploadConfig.java
  90. 51 0
      src/main/java/cn/com/sailfish/linghang/config/LiquibaseConfig.java
  91. 35 0
      src/main/java/cn/com/sailfish/linghang/config/RequestLoggingFilterConfig.java
  92. 39 0
      src/main/java/cn/com/sailfish/linghang/config/SailfishPropertiesConfig.java
  93. 94 0
      src/main/java/cn/com/sailfish/linghang/config/SwaggerConfig.java
  94. 147 0
      src/main/java/cn/com/sailfish/linghang/config/WebSecurityConfig.java
  95. 20 0
      src/main/java/cn/com/sailfish/linghang/config/WebSocketConfig.java
  96. 23 0
      src/main/java/cn/com/sailfish/linghang/config/property/CaptchaProperty.java
  97. 45 0
      src/main/java/cn/com/sailfish/linghang/config/property/ExportTemplateProperty.java
  98. 28 0
      src/main/java/cn/com/sailfish/linghang/config/property/FileUploadProperty.java
  99. 72 0
      src/main/java/cn/com/sailfish/linghang/config/property/SecurityProperty.java
  100. 30 0
      src/main/java/cn/com/sailfish/linghang/config/property/SsoProperty.java

+ 3 - 0
.gitignore

@@ -0,0 +1,3 @@
+/.idea/
+/target/
+/logs/

+ 136 - 0
Jenkinsfile

@@ -0,0 +1,136 @@
+pipeline {
+  agent any
+//   tools { maven '' }
+//   parameters {}
+  environment {
+    _productFileName = buildProductFileName() // 产物文件名
+    _productBackupPath = '/app/csair/backup' // 产物备份目录
+  }
+  triggers {
+//     cron('0 0 * * *') // 周期任务
+//     pollSCM('H/1 * * * *') // 轮询代码仓库(每分钟判断一次代码是否有变化)
+    gitlab(
+      triggerOnPush: true,
+      triggerOnMergeRequest: true,
+      triggerOnNoteRequest: true,
+      branchFilterType: 'All',
+      secretToken: 'asdfghjkl'
+    )
+  }
+  options {
+    buildDiscarder(logRotator(numToKeepStr: '10')) // 保存最近历史构建记录的数量
+    disableConcurrentBuilds() // 同一个pipeline,Jenkins默认是可以同时执行多次的,此选项为了禁止pipeline同时执行
+    // checkoutToSubdirectory('sub') // Jenkins默认拉取源码至工作空间的根目录中,此选项可以指定检出到工作空间的子目录中
+    retry(2) // 当发生失败时进行重试(包括第1次失败)
+    timestamps() // 添加日志打印时间
+    timeout(time: 15, unit: 'MINUTES') // 如果pipeline执行时间过长,超出了设置的timeout时间,Jenkins将中止pipeline(SECONDS、MINUTES、HOURS)
+    gitLabConnection('gitlab') // 连接gitlab服务(需要在Jenkins中设置Jenkins -> Configure System)
+  }
+  post {
+    always { // 不论当前完成状态是什么,都执行
+      // pom.xml中增加plugin -> maven-pmd-plugin
+      // Jenkins PMD插件[PMD](https://plugins.jenkins.io/pmd)
+      pmd(canRunOnFailed: true, pattern: '**/target/pmd.xml')
+      cleanWs() // 清理工作空间插件[Workspace Cleanup Plugin](https://plugins.jenkins.io/ws-cleanup)
+    }
+    failure {
+      updateGitlabCommitStatus name: 'build', state: 'failed'
+    }
+    success {
+      updateGitlabCommitStatus name: 'build', state: 'success'
+    }
+  }
+  stages {
+    stage('Env & Param') {
+      parallel {
+        stage('Env') {
+          steps {
+            sh 'printenv'
+            echo "系统当前用户    [${env.USER}]"
+            echo "WORKSPACE     [${env.WORKSPACE}]"
+            echo "JENKINS_URL   [${env.JENKINS_URL}]"
+            echo "${_productFileName}"
+          }
+        }
+        stage('Job') {
+          steps {
+            echo "Running [${env.BUILD_NUMBER}] on [${env.BUILD_URL}]"
+            echo "BRANCH_NAME [${env.BRANCH_NAME}] GIT_BRANCH [${env.GIT_BRANCH}]"
+          }
+        }
+      }
+    }
+    stage('Build') {
+      steps {
+        sh 'mvn clean package -Dautoconfig.skip=true -Dmaven.test.skip=true'
+      }
+    }
+    stage('Static check') {
+      parallel {
+        stage('pmd') {
+          steps {
+            sh 'mvn pmd:pmd'
+          }
+        }
+      }
+    }
+    stage('Product') {
+      steps {
+        dir("${env.WORKSPACE}/target") {
+          sh "mv ${env.WORKSPACE}/target/*.jar ${env.WORKSPACE}/target/${_productFileName}"
+        }
+        archiveArtifacts(artifacts: '**/target/*.jar', caseSensitive: true, fingerprint: true)
+      }
+    }
+    stage('Release') {
+      parallel {
+        stage('Test') {
+          when {
+            branch 'test'
+          }
+          steps {
+            echo 'Test环境'
+          }
+        }
+        stage('Master') {
+          when {
+            branch 'master'
+          }
+          steps {
+            script {
+              // 判断产物备份目录是否存在,否则创建文件夹
+              if(!fileExists("${_productBackupPath}")) {
+                sh "mkdir ${_productBackupPath}"
+              }
+            }
+
+            sh "scp -r ${WORKSPACE}/target/${_productFileName} ${_productBackupPath}"
+          }
+        }
+        stage('Tag') {
+          when {
+            tag "v*.*.*"
+          }
+          steps {
+            script {
+              // 判断产物备份目录是否存在,否则创建文件夹
+              if(!fileExists("${_productBackupPath}")) {
+                sh "mkdir ${_productBackupPath}"
+              }
+            }
+
+            sh "scp -r ${WORKSPACE}/target/${_productFileName} ${_productBackupPath}"
+          }
+        }
+      }
+    }
+  }
+}
+
+// 生成产物的文件名
+def buildProductFileName() {
+  String projectName = "${env.JOB_NAME}".tokenize('//')[0]
+  String branchName = "${env.JOB_NAME}".tokenize('//')[1]
+  String date = new Date().format('yyyyMMddHHmmss')
+  return "${projectName}-${branchName}-${date}.jar"
+}

+ 3 - 0
README.md

@@ -0,0 +1,3 @@
+# csair-vr-portal-back-end
+
+南方航空虚拟现实航空实训平台-服务端

+ 9 - 0
csair-vr-portal-ui/.editorconfig

@@ -0,0 +1,9 @@
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true

+ 4 - 0
csair-vr-portal-ui/.eslintignore

@@ -0,0 +1,4 @@
+/build/
+/config/
+/dist/
+/*.js

+ 30 - 0
csair-vr-portal-ui/.eslintrc.js

@@ -0,0 +1,30 @@
+// https://eslint.org/docs/user-guide/configuring
+
+module.exports = {
+    root: true,
+    parserOptions: {
+        parser: 'babel-eslint'
+    },
+    env: {
+        browser: true,
+    },
+    extends: [
+        // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention
+        // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules.
+        'plugin:vue/essential',
+        // https://github.com/standard/standard/blob/master/docs/RULES-en.md
+        'standard'
+    ],
+    // required to lint *.vue files
+    plugins: [
+        'vue'
+    ],
+    // add your custom rules here
+    rules: {
+        'indent': ['off', 2],
+        // allow async-await
+        'generator-star-spacing': 'off',
+        // allow debugger during development
+        'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
+    }
+}

+ 3 - 0
csair-vr-portal-ui/.gitignore

@@ -0,0 +1,3 @@
+/.idea/
+/node_modules/
+/package-lock.json

+ 10 - 0
csair-vr-portal-ui/.postcssrc.js

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

+ 123 - 0
csair-vr-portal-ui/Jenkinsfile

@@ -0,0 +1,123 @@
+pipeline {
+  agent {
+    node {
+      label '112.74.105.17'
+    }
+  }
+//   tools { node '' }
+//   parameters {}
+  environment {
+    _productFileName = buildProductFileName() // 产物文件名
+    _remote = "root@47.97.230.53"
+    _productBackupPath = '/app/csair/backup' // 产物备份目录
+  }
+  triggers {
+//     cron('0 0 * * *') // 周期任务
+//     pollSCM('H/1 * * * *') // 轮询代码仓库(每分钟判断一次代码是否有变化)
+    gitlab(
+      triggerOnPush: true,
+      triggerOnMergeRequest: true,
+      triggerOnNoteRequest: true,
+      branchFilterType: 'All',
+      secretToken: 'asdfghjkl'
+    )
+  }
+  options {
+    buildDiscarder(logRotator(numToKeepStr: '10')) // 保存最近历史构建记录的数量
+    disableConcurrentBuilds() // 同一个pipeline,Jenkins默认是可以同时执行多次的,此选项为了禁止pipeline同时执行
+    // checkoutToSubdirectory('sub') // Jenkins默认拉取源码至工作空间的根目录中,此选项可以指定检出到工作空间的子目录中
+    retry(1) // 当发生失败时进行重试(包括第1次失败)
+    timestamps() // 添加日志打印时间
+    timeout(time: 15, unit: 'MINUTES') // 如果pipeline执行时间过长,超出了设置的timeout时间,Jenkins将中止pipeline(SECONDS、MINUTES、HOURS)
+    gitLabConnection('gitlab') // 连接gitlab服务(需要在Jenkins中设置Jenkins -> Configure System)
+  }
+  post {
+    always { // 不论当前完成状态是什么,都执行
+      cleanWs() // 清理工作空间插件[Workspace Cleanup Plugin](https://plugins.jenkins.io/ws-cleanup)
+    }
+    failure {
+      updateGitlabCommitStatus name: 'build', state: 'failed'
+    }
+    success {
+      updateGitlabCommitStatus name: 'build', state: 'success'
+    }
+  }
+  stages {
+    stage('Env & Param') {
+      parallel {
+        stage('Env') {
+          steps {
+            sh 'printenv'
+            echo "系统当前用户    [${env.USER}]"
+            echo "WORKSPACE     [${env.WORKSPACE}]"
+            echo "JENKINS_URL   [${env.JENKINS_URL}]"
+            echo "${_productFileName}"
+          }
+        }
+        stage('Job') {
+          steps {
+            echo "Running [${env.BUILD_NUMBER}] on [${env.BUILD_URL}]"
+            echo "BRANCH_NAME [${env.BRANCH_NAME}] GIT_BRANCH [${env.GIT_BRANCH}]"
+          }
+        }
+      }
+    }
+    stage('NPM Install') {
+      steps {
+        sh 'npm install'
+      }
+    }
+    stage('Static check') {
+      parallel {
+        stage('eslint') {
+          steps {
+            echo 'eslint'
+          }
+        }
+      }
+    }
+    stage('NPM build') {
+      steps {
+        sh 'npm run build'
+      }
+    }
+    stage('Product') {
+      steps {
+        dir("${env.WORKSPACE}/dist") {
+          sh "tar -zcvf ${env.WORKSPACE}/${_productFileName} ./"
+        }
+
+        archiveArtifacts(artifacts: '*.tar.gz', caseSensitive: true, fingerprint: true)
+      }
+    }
+    stage('Release') {
+      parallel {
+        stage('Master') {
+          when {
+            branch 'master'
+          }
+          steps {
+            script {
+              sh "scp -r ${WORKSPACE}/${_productFileName} ${_remote}:${_productBackupPath}"
+              sh """
+                ssh ${_remote} "
+                  source /etc/profile
+                  rm -rf /app/csair/portal/*
+                  tar -zxvf ${_productBackupPath}/${_productFileName} -C /app/csair/portal
+                "
+              """
+            }
+          }
+        }
+      }
+    }
+  }
+}
+
+// 生成产物的文件名
+def buildProductFileName() {
+  String projectName = "${env.JOB_NAME}".tokenize('//')[0]
+  String branchName = "${env.JOB_NAME}".tokenize('//')[1]
+  String date = new Date().format('yyyyMMddHHmmss')
+  return "${projectName}-${branchName}-${date}.tar.gz"
+}

+ 17 - 0
csair-vr-portal-ui/README.md

@@ -0,0 +1,17 @@
+# csair-vr-portal-ui
+南方航空虚拟现实航空实训平台主页
+
+## Build Setup
+```bash
+# Install dependencies
+npm install
+
+# Serve with hot reload at localhost:9528
+npm run dev
+
+# Build for production with minification
+npm run build
+```
+
+# MockPlus
+https://app.mockplus.cn/app/re3yo70tE/specs/1vnJo75kT

+ 41 - 0
csair-vr-portal-ui/build/build.js

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

+ 54 - 0
csair-vr-portal-ui/build/check-versions.js

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

BIN
csair-vr-portal-ui/build/logo.png


+ 102 - 0
csair-vr-portal-ui/build/utils.js

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

+ 22 - 0
csair-vr-portal-ui/build/vue-loader.conf.js

@@ -0,0 +1,22 @@
+'use strict'
+const utils = require('./utils')
+const config = require('../config')
+const isProduction = process.env.NODE_ENV === 'production'
+const sourceMapEnabled = isProduction
+  ? config.build.productionSourceMap
+  : config.dev.cssSourceMap
+
+module.exports = {
+  loaders: utils.cssLoaders({
+    sourceMap: sourceMapEnabled,
+    extract: isProduction
+  }),
+  cssSourceMap: sourceMapEnabled,
+  cacheBusting: config.dev.cacheBusting,
+  transformToRequire: {
+    video: ['src', 'poster'],
+    source: 'src',
+    img: 'src',
+    image: 'xlink:href'
+  }
+}

+ 94 - 0
csair-vr-portal-ui/build/webpack.base.conf.js

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

+ 95 - 0
csair-vr-portal-ui/build/webpack.dev.conf.js

@@ -0,0 +1,95 @@
+'use strict'
+const utils = require('./utils')
+const webpack = require('webpack')
+const config = require('../config')
+const merge = require('webpack-merge')
+const path = require('path')
+const baseWebpackConfig = require('./webpack.base.conf')
+const CopyWebpackPlugin = require('copy-webpack-plugin')
+const HtmlWebpackPlugin = require('html-webpack-plugin')
+const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
+const portfinder = require('portfinder')
+
+const HOST = process.env.HOST
+const PORT = process.env.PORT && Number(process.env.PORT)
+
+const devWebpackConfig = merge(baseWebpackConfig, {
+  module: {
+    rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
+  },
+  // cheap-module-eval-source-map is faster for development
+  devtool: config.dev.devtool,
+
+  // these devServer options should be customized in /config/index.js
+  devServer: {
+    clientLogLevel: 'warning',
+    historyApiFallback: {
+      rewrites: [
+        { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
+      ],
+    },
+    hot: true,
+    contentBase: false, // since we use CopyWebpackPlugin.
+    compress: true,
+    host: HOST || config.dev.host,
+    port: PORT || config.dev.port,
+    open: config.dev.autoOpenBrowser,
+    overlay: config.dev.errorOverlay
+      ? { warnings: false, errors: true }
+      : false,
+    publicPath: config.dev.assetsPublicPath,
+    proxy: config.dev.proxyTable,
+    quiet: true, // necessary for FriendlyErrorsPlugin
+    watchOptions: {
+      poll: config.dev.poll,
+    }
+  },
+  plugins: [
+    new webpack.DefinePlugin({
+      'process.env': require('../config/dev.env')
+    }),
+    new webpack.HotModuleReplacementPlugin(),
+    new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
+    new webpack.NoEmitOnErrorsPlugin(),
+    // https://github.com/ampedandwired/html-webpack-plugin
+    new HtmlWebpackPlugin({
+      filename: 'index.html',
+      template: 'index.html',
+      inject: true
+    }),
+    // copy custom static assets
+    new CopyWebpackPlugin([
+      {
+        from: path.resolve(__dirname, '../static'),
+        to: config.dev.assetsSubDirectory,
+        ignore: ['.*']
+      }
+    ])
+  ]
+})
+
+module.exports = new Promise((resolve, reject) => {
+  portfinder.basePort = process.env.PORT || config.dev.port
+  portfinder.getPort((err, port) => {
+    if (err) {
+      reject(err)
+    } else {
+      // publish the new Port, necessary for e2e tests
+      process.env.PORT = port
+      // add port to devServer config
+      devWebpackConfig.devServer.port = port
+
+      // Add FriendlyErrorsPlugin
+      devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
+        compilationSuccessInfo: {
+          messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],
+        },
+        onErrors: config.dev.notifyOnErrors
+        ? utils.createNotifierCallback()
+        : undefined
+      }))
+
+      resolve(devWebpackConfig)
+    }
+  })
+})

+ 145 - 0
csair-vr-portal-ui/build/webpack.prod.conf.js

@@ -0,0 +1,145 @@
+'use strict'
+const path = require('path')
+const utils = require('./utils')
+const webpack = require('webpack')
+const config = require('../config')
+const merge = require('webpack-merge')
+const baseWebpackConfig = require('./webpack.base.conf')
+const CopyWebpackPlugin = require('copy-webpack-plugin')
+const HtmlWebpackPlugin = require('html-webpack-plugin')
+const ExtractTextPlugin = require('extract-text-webpack-plugin')
+const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
+const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
+
+const env = require('../config/prod.env')
+
+const webpackConfig = merge(baseWebpackConfig, {
+  module: {
+    rules: utils.styleLoaders({
+      sourceMap: config.build.productionSourceMap,
+      extract: true,
+      usePostCSS: true
+    })
+  },
+  devtool: config.build.productionSourceMap ? config.build.devtool : false,
+  output: {
+    path: config.build.assetsRoot,
+    filename: utils.assetsPath('js/[name].[chunkhash].js'),
+    chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
+  },
+  plugins: [
+    // http://vuejs.github.io/vue-loader/en/workflow/production.html
+    new webpack.DefinePlugin({
+      'process.env': env
+    }),
+    new UglifyJsPlugin({
+      uglifyOptions: {
+        compress: {
+          warnings: false
+        }
+      },
+      sourceMap: config.build.productionSourceMap,
+      parallel: true
+    }),
+    // extract css into its own file
+    new ExtractTextPlugin({
+      filename: utils.assetsPath('css/[name].[contenthash].css'),
+      // Setting the following option to `false` will not extract CSS from codesplit chunks.
+      // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack.
+      // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`, 
+      // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110
+      allChunks: true,
+    }),
+    // Compress extracted CSS. We are using this plugin so that possible
+    // duplicated CSS from different components can be deduped.
+    new OptimizeCSSPlugin({
+      cssProcessorOptions: config.build.productionSourceMap
+        ? { safe: true, map: { inline: false } }
+        : { safe: true }
+    }),
+    // generate dist index.html with correct asset hash for caching.
+    // you can customize output by editing /index.html
+    // see https://github.com/ampedandwired/html-webpack-plugin
+    new HtmlWebpackPlugin({
+      filename: config.build.index,
+      template: 'index.html',
+      inject: true,
+      minify: {
+        removeComments: true,
+        collapseWhitespace: true,
+        removeAttributeQuotes: true
+        // more options:
+        // https://github.com/kangax/html-minifier#options-quick-reference
+      },
+      // necessary to consistently work with multiple chunks via CommonsChunkPlugin
+      chunksSortMode: 'dependency'
+    }),
+    // keep module.id stable when vendor modules does not change
+    new webpack.HashedModuleIdsPlugin(),
+    // enable scope hoisting
+    new webpack.optimize.ModuleConcatenationPlugin(),
+    // split vendor js into its own file
+    new webpack.optimize.CommonsChunkPlugin({
+      name: 'vendor',
+      minChunks (module) {
+        // any required modules inside node_modules are extracted to vendor
+        return (
+          module.resource &&
+          /\.js$/.test(module.resource) &&
+          module.resource.indexOf(
+            path.join(__dirname, '../node_modules')
+          ) === 0
+        )
+      }
+    }),
+    // extract webpack runtime and module manifest to its own file in order to
+    // prevent vendor hash from being updated whenever app bundle is updated
+    new webpack.optimize.CommonsChunkPlugin({
+      name: 'manifest',
+      minChunks: Infinity
+    }),
+    // This instance extracts shared chunks from code splitted chunks and bundles them
+    // in a separate chunk, similar to the vendor chunk
+    // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk
+    new webpack.optimize.CommonsChunkPlugin({
+      name: 'app',
+      async: 'vendor-async',
+      children: true,
+      minChunks: 3
+    }),
+
+    // copy custom static assets
+    new CopyWebpackPlugin([
+      {
+        from: path.resolve(__dirname, '../static'),
+        to: config.build.assetsSubDirectory,
+        ignore: ['.*']
+      }
+    ])
+  ]
+})
+
+if (config.build.productionGzip) {
+  const CompressionWebpackPlugin = require('compression-webpack-plugin')
+
+  webpackConfig.plugins.push(
+    new CompressionWebpackPlugin({
+      asset: '[path].gz[query]',
+      algorithm: 'gzip',
+      test: new RegExp(
+        '\\.(' +
+        config.build.productionGzipExtensions.join('|') +
+        ')$'
+      ),
+      threshold: 10240,
+      minRatio: 0.8
+    })
+  )
+}
+
+if (config.build.bundleAnalyzerReport) {
+  const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
+  webpackConfig.plugins.push(new BundleAnalyzerPlugin())
+}
+
+module.exports = webpackConfig

+ 12 - 0
csair-vr-portal-ui/config/dev.env.js

@@ -0,0 +1,12 @@
+'use strict'
+const merge = require('webpack-merge')
+const prodEnv = require('./prod.env')
+
+module.exports = merge(prodEnv, {
+  NODE_ENV: '"development"',
+  PORTAL_URL: '"http://localhost:9191/linghang/portal"',
+  ADMIN_URL: '"http://localhost:9191/linghang/admin"',
+  HOME_URL: '"http://localhost:9191/linghang"',
+  OSS_URL: '"http://localhost:9193/oss"',
+  YUN_URL: '"https://xr.csair.com/720yun"'
+})

+ 76 - 0
csair-vr-portal-ui/config/index.js

@@ -0,0 +1,76 @@
+'use strict'
+// Template version: 1.3.1
+// see http://vuejs-templates.github.io/webpack for documentation.
+
+const path = require('path')
+
+module.exports = {
+  dev: {
+
+    // Paths
+    assetsSubDirectory: 'static',
+    assetsPublicPath: '/',
+    proxyTable: {},
+
+    // Various Dev Server settings
+    host: 'localhost', // can be overwritten by process.env.HOST
+    port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
+    autoOpenBrowser: false,
+    errorOverlay: true,
+    notifyOnErrors: true,
+    poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-
+
+    // Use Eslint Loader?
+    // If true, your code will be linted during bundling and
+    // linting errors and warnings will be shown in the console.
+    useEslint: true,
+    // If true, eslint errors and warnings will also be shown in the error overlay
+    // in the browser.
+    showEslintErrorsInOverlay: false,
+
+    /**
+     * Source Maps
+     */
+
+    // https://webpack.js.org/configuration/devtool/#development
+    devtool: 'cheap-module-eval-source-map',
+
+    // If you have problems debugging vue-files in devtools,
+    // set this to false - it *may* help
+    // https://vue-loader.vuejs.org/en/options.html#cachebusting
+    cacheBusting: true,
+
+    cssSourceMap: true
+  },
+
+  build: {
+    // Template for index.html
+    index: path.resolve(__dirname, '../dist/index.html'),
+
+    // Paths
+    assetsRoot: path.resolve(__dirname, '../dist'),
+    assetsSubDirectory: 'static',
+    assetsPublicPath: './',
+
+    /**
+     * Source Maps
+     */
+
+    productionSourceMap: true,
+    // https://webpack.js.org/configuration/devtool/#production
+    devtool: '#source-map',
+
+    // Gzip off by default as many popular static hosts such as
+    // Surge or Netlify already gzip all static assets for you.
+    // Before setting to `true`, make sure to:
+    // npm install --save-dev compression-webpack-plugin
+    productionGzip: false,
+    productionGzipExtensions: ['js', 'css'],
+
+    // Run the build command with an extra argument to
+    // View the bundle analyzer report after build finishes:
+    // `npm run build --report`
+    // Set to `true` or `false` to always turn it on or off
+    bundleAnalyzerReport: process.env.npm_config_report
+  }
+}

+ 13 - 0
csair-vr-portal-ui/config/prod.env.js

@@ -0,0 +1,13 @@
+'use strict'
+module.exports = {
+  NODE_ENV: '"production"',
+  PORTAL_URL: '"https://430jy.uutime.cn/csair/portal"',
+  ADMIN_URL: '"https://430jy.uutime.cn/csair/admin"',
+  HOME_URL: '"https://430jy.uutime.cn/linghang"',
+  OSS_URL: '"https://430jy.uutime.cn/oss"',
+//  PORTAL_URL: '"/portal"',
+//  ADMIN_URL: '"/back-end-ui"',
+//  HOME_URL: '"https://10.79.11.52"',
+//  OSS_URL: '"http://localhost:9193/oss"',
+  YUN_URL: '"https://xr.csair.com/720yun"'
+}

+ 111 - 0
csair-vr-portal-ui/index.html

@@ -0,0 +1,111 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width,initial-scale=1.0">
+  <title>南航虚拟现实航空实训平台</title>
+  <link href="static/css/quill/quill.snow.css" rel="stylesheet">
+  <link href="static/css/quill/quill.bubble.css" rel="stylesheet">
+  <link href="static/css/quill/quill.core.css" rel="stylesheet">
+
+  <style>
+    html, body, h1, h2, h3, h4, h5, h6, div, dl, dt, dd, ul, ol, li, p, blockquote, pre, hr, figure, table, caption, th, td, form, fieldset, legend, input, button, textarea, menu {
+      margin: 0;
+      padding: 0;
+    }
+
+    header, footer, section, article, aside, nav, hgroup, address, figure, figcaption, menu, details {
+      display: block;
+    }
+
+    table {
+      border-collapse: collapse;
+      border-spacing: 0;
+    }
+
+    caption, th {
+      text-align: left;
+      font-weight: normal;
+    }
+
+    html, body, fieldset, img, iframe, abbr {
+      border: 0;
+    }
+
+    i, cite, em, var, address, dfn {
+      font-style: normal;
+    }
+
+    [hidefocus], summary {
+      outline: 0;
+    }
+
+    li {
+      list-style: none;
+    }
+
+    h1, h2, h3, h4, h5, h6, small {
+      font-size: 100%;
+    }
+
+    sup, sub {
+      font-size: 83%;
+    }
+
+    pre, code, kbd, samp {
+      font-family: inherit;
+    }
+
+    q:before, q:after {
+      content: none;
+    }
+
+    textarea {
+      overflow: auto;
+      resize: none;
+    }
+
+    label, summary {
+      cursor: default;
+    }
+
+    a, button {
+      cursor: pointer;
+    }
+
+    h1, h2, h3, h4, h5, h6, em, strong, b {
+      font-weight: normal;
+    }
+
+    del, ins, u, s, a, a:hover {
+      text-decoration: none;
+    }
+
+    body, textarea, input, button, select, keygen, legend {
+      font: 14px/1em 'microsoft yahei', arial;
+      color: #333;
+      outline: 0;
+    }
+
+    body {
+      background: #fff;
+    }
+
+    a {
+      line-height: 24px;
+    }
+
+    a {
+      color: #333;
+    }
+
+    a:hover {
+    }
+  </style>
+</head>
+
+<body>
+<div id="app"></div>
+</body>
+
+</html>

+ 77 - 0
csair-vr-portal-ui/package.json

@@ -0,0 +1,77 @@
+{
+  "name": "vue",
+  "version": "1.0.0",
+  "description": "A Vue.js project",
+  "author": "",
+  "private": true,
+  "scripts": {
+    "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
+    "start": "npm run dev",
+    "lint": "eslint --ext .js,.vue src",
+    "build": "node build/build.js"
+  },
+  "dependencies": {
+    "axios": "^0.19.0",
+    "element-ui": "^2.10.1",
+    "nprogress": "^0.2.0",
+    "vue": "^2.5.2",
+    "vue-router": "^3.0.1"
+  },
+  "devDependencies": {
+    "autoprefixer": "^7.1.2",
+    "babel-core": "^6.22.1",
+    "babel-eslint": "^8.2.1",
+    "babel-helper-vue-jsx-merge-props": "^2.0.3",
+    "babel-loader": "^7.1.1",
+    "babel-plugin-syntax-dynamic-import": "^6.18.0",
+    "babel-plugin-syntax-jsx": "^6.18.0",
+    "babel-plugin-transform-runtime": "^6.22.0",
+    "babel-plugin-transform-vue-jsx": "^3.5.0",
+    "babel-preset-env": "^1.3.2",
+    "babel-preset-stage-2": "^6.22.0",
+    "chalk": "^2.0.1",
+    "copy-webpack-plugin": "^4.0.1",
+    "css-loader": "^0.28.0",
+    "eslint": "^4.15.0",
+    "eslint-config-standard": "^10.2.1",
+    "eslint-friendly-formatter": "^3.0.0",
+    "eslint-loader": "^1.7.1",
+    "eslint-plugin-import": "^2.7.0",
+    "eslint-plugin-node": "^5.2.0",
+    "eslint-plugin-promise": "^3.4.0",
+    "eslint-plugin-standard": "^3.0.1",
+    "eslint-plugin-vue": "^4.0.0",
+    "extract-text-webpack-plugin": "^3.0.0",
+    "file-loader": "^1.1.4",
+    "friendly-errors-webpack-plugin": "^1.6.1",
+    "html-webpack-plugin": "^2.30.1",
+    "node-notifier": "^5.1.2",
+    "optimize-css-assets-webpack-plugin": "^3.2.0",
+    "ora": "^1.2.0",
+    "portfinder": "^1.0.13",
+    "postcss-import": "^11.0.0",
+    "postcss-loader": "^2.0.8",
+    "postcss-url": "^7.2.1",
+    "rimraf": "^2.6.0",
+    "semver": "^5.3.0",
+    "shelljs": "^0.7.6",
+    "uglifyjs-webpack-plugin": "^1.1.1",
+    "url-loader": "^0.5.8",
+    "vue-loader": "^13.3.0",
+    "vue-style-loader": "^3.0.1",
+    "vue-template-compiler": "^2.5.2",
+    "webpack": "^3.6.0",
+    "webpack-bundle-analyzer": "^2.9.0",
+    "webpack-dev-server": "^2.9.1",
+    "webpack-merge": "^4.1.0"
+  },
+  "engines": {
+    "node": ">= 6.0.0",
+    "npm": ">= 3.0.0"
+  },
+  "browserslist": [
+    "> 1%",
+    "last 2 versions",
+    "not ie <= 8"
+  ]
+}

+ 26 - 0
csair-vr-portal-ui/src/App.vue

@@ -0,0 +1,26 @@
+<template>
+  <div id="app">
+    <router-view/>
+  </div>
+</template>
+
+<script>
+  export default {
+    name: 'App',
+    components: {},
+    data () {
+      return {}
+    },
+    mounted () {},
+    methods: {}
+  }
+</script>
+
+<style>
+  #app {
+    font-family: 'Avenir', Helvetica, Arial, sans-serif;
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale;
+    color: #2c3e50;
+  }
+</style>

+ 17 - 0
csair-vr-portal-ui/src/api/couresApi.js

@@ -0,0 +1,17 @@
+import HttpKit from '@/utils/http-kit'
+
+export default {
+  /**
+   * 功能描述:查询图片
+   */
+  page (form) {
+    return HttpKit.post(`/web/course?page=${form.page}&pageSize=${form.pageSize}`).then(
+      res => res.data
+    )
+  },
+  findById (courseId) {
+    return HttpKit.get(`/web/course/${courseId}`).then(
+      res => res.data
+    )
+  }
+}

+ 33 - 0
csair-vr-portal-ui/src/api/homeApi.js

@@ -0,0 +1,33 @@
+import HttpKit from '@/utils/http-kit'
+
+export default {
+  /**
+   * 功能描述:查询所有[banner]
+   * @param name
+   */
+  findAll () {
+    return HttpKit.post(`/admin/bannerControl/searchBannerControl`).then(
+      res => res.data
+    )
+  },
+  expo () {
+    return HttpKit.post(`/web/home`).then(
+      res => res.data
+    )
+  },
+  findRabcByid (value) {
+    return HttpKit.get(`/admin/RBAC/${value}/RABCfindbyid`).then(
+      res => res.data
+    )
+  },
+  getCurrentInfo () {
+    return HttpKit.get(`/apiv1/web/user/getCurrentInfo`).then(
+      res => res.data
+    )
+  },
+  logout () {
+  return HttpKit.get(`/apiv1/web/user/logout`).then(
+    res => res.data
+  )
+}
+}

+ 12 - 0
csair-vr-portal-ui/src/api/inquireApi.js

@@ -0,0 +1,12 @@
+import HttpKit from '@/utils/http-kit'
+
+export default {
+  /**
+   * 功能描述:查询图片
+   */
+  inquire (form) {
+    return HttpKit.post(`/web/inquire/all?crux=${form.searchValue}&page=${form.page}&pageSize=${form.pageSize}&type=${form.type}&newstype=${form.newstype}`).then(
+      res => res.data
+    )
+  }
+}

+ 18 - 0
csair-vr-portal-ui/src/api/newsApi.js

@@ -0,0 +1,18 @@
+import HttpKit from '@/utils/http-kit'
+
+export default {
+  /**
+   * 功能描述:
+   * @param newId
+   */
+  findById (newId) {
+    return HttpKit.get(`/web/news/${newId}`).then(
+      res => res.data
+    )
+  },
+  page (form) {
+    return HttpKit.post(`/web/news?page=${form.page}&pageSize=${form.pageSize}`).then(
+      res => res.data
+    )
+  }
+}

+ 13 - 0
csair-vr-portal-ui/src/api/searchApi.js

@@ -0,0 +1,13 @@
+import HttpKit from '@/utils/http-kit'
+
+export default {
+  /**
+   * 功能描述:搜索列表
+   */
+  page (searchForm) {
+    return HttpKit.post(`/web/view/all?crux=${searchForm.crux}&page=${searchForm.page}&pageSize=${searchForm.pageSize}&type=${searchForm.type}`).then(
+      res => res.data
+    )
+  }
+
+}

+ 18 - 0
csair-vr-portal-ui/src/api/vrpanoramicApi.js

@@ -0,0 +1,18 @@
+import HttpKit from '@/utils/http-kit'
+
+export default {
+  /**
+   * 功能描述:查询所有[banner]
+   * @param name
+   */
+  findById (vrPanoramicId) {
+    return HttpKit.get(`/web/vrPanoram/${vrPanoramicId}`).then(
+      res => res.data
+    )
+  },
+  page (form) {
+    return HttpKit.post(`/web/vrPanoram?page=${form.page}&pageSize=${form.pageSize}`).then(
+      res => res.data
+    )
+  }
+}

BIN
csair-vr-portal-ui/src/assets/403bg.png


+ 18 - 0
csair-vr-portal-ui/src/assets/css/app.css

@@ -0,0 +1,18 @@
+.el-carousel__button {
+  width: 65px;
+  height: 8px;
+  border-radius: 4px;
+}
+.el-input--mini .el-input__inner {
+  height: 45px;
+  line-height: 45px;
+}
+.searchClass .el-input__inner {
+  height: 34px;
+  line-height: 34px;
+  border: none;
+  background-color: transparent;
+}
+.el-carousel__indicator--horizontal {
+  padding: 12px 24px;
+}

BIN
csair-vr-portal-ui/src/assets/images/3d-model.png


BIN
csair-vr-portal-ui/src/assets/images/backTop.png


BIN
csair-vr-portal-ui/src/assets/images/backTopBack.png


BIN
csair-vr-portal-ui/src/assets/images/course-text.png


BIN
csair-vr-portal-ui/src/assets/images/head-logo.png


BIN
csair-vr-portal-ui/src/assets/images/logo.png


BIN
csair-vr-portal-ui/src/assets/images/new-info-1.png


BIN
csair-vr-portal-ui/src/assets/images/new-info-2.png


BIN
csair-vr-portal-ui/src/assets/images/new-text.png


BIN
csair-vr-portal-ui/src/assets/images/vr-panorama.png


+ 8 - 0
csair-vr-portal-ui/src/components/layout/index.js

@@ -0,0 +1,8 @@
+import WhiteBoard from '@/components/layout/white-board'
+
+/* 全局安装通用组件 */
+export default {
+  install (Vue) {
+    Vue.component('white-board', WhiteBoard)
+  }
+}

+ 15 - 0
csair-vr-portal-ui/src/components/layout/white-board.vue

@@ -0,0 +1,15 @@
+<template>
+  <el-row style="background-color: #fff; padding: 10px 25px; margin-bottom: 10px;">
+    <slot></slot>
+  </el-row>
+</template>
+
+<script>
+  export default {
+    name: 'LayoutWhiteBoard'
+  }
+</script>
+
+<style scoped>
+
+</style>

+ 2 - 0
csair-vr-portal-ui/src/constants/constants.js

@@ -0,0 +1,2 @@
+export default {
+}

+ 27 - 0
csair-vr-portal-ui/src/filters/date-format.js

@@ -0,0 +1,27 @@
+import DateKit from '@/utils/date-kit'
+
+const dateFilter = {
+  /**
+   * 功能描述:格式化时间字段
+   * 使用方式:
+   * {{item.date | fmtDate}}
+   * {{row.createAt | fmtDate('yyyy-MM-dd')}}
+   * @param value
+   * @param format
+   * @returns {*}
+   */
+  formatDate (value, format = 'yyyy-MM-dd HH:mm:ss') {
+    if (!value) {
+      return ''
+    }
+
+    return DateKit.format(new Date(Date.parse(value.replace(/-/g, '/'))), format)
+  },
+
+  // 全局安装器
+  install (Vue) {
+    Vue.filter('fmtDate', this.formatDate)
+  }
+}
+
+export default dateFilter

+ 7 - 0
csair-vr-portal-ui/src/filters/index.js

@@ -0,0 +1,7 @@
+import Vue from 'vue'
+import DateFormat from './date-format'
+import StrFormat from './str-format'
+
+// 全局注册过滤器
+Vue.use(DateFormat)
+Vue.use(StrFormat)

+ 67 - 0
csair-vr-portal-ui/src/filters/str-format.js

@@ -0,0 +1,67 @@
+import StrKit from '@/utils/str-kit'
+
+const strFilter = {
+  /**
+   * 功能描述:剪切字符串过滤器
+   * 使用方式:
+   * {{item.deviceName | cutStr(14)}}
+   * @param value
+   * @param len
+   * @param placeholder
+   * @returns {*}
+   */
+  cutStr (value, len, placeholder) {
+    if (!value) {
+      return ''
+    }
+    len = len || 18
+    placeholder = placeholder || '...'
+
+    let count = 0
+    let titleLenOld = StrKit.getByteLen(value)
+    let titleLenNew = 0 // 初始化重新构造的title的长度
+
+    for (var i = 0; i < value.length; i++) {
+      if (titleLenNew < len) {
+        ++count
+        if (value[i].match(/[^x00-xff]/ig) !== null) { // 全角
+          titleLenNew += 2
+        } else {
+          titleLenNew += 1
+        }
+      } else {
+        break
+      }
+    }
+    value = value.substring(0, count) + (titleLenOld <= len ? '' : placeholder)
+    return value
+  },
+
+  /**
+   * 功能描述:格式化手机号码
+   * 使用方式:
+   * {{item.deviceName | fmtPhoneNumber}}
+   * @param val
+   * @returns {string|string | *}
+   */
+  fmtPhoneNumber (val) {
+    val = val.replace(/[^\d]/g, '').substr(0, 11)
+    if (val.length <= 3) {
+      return val
+    } else if (val.length <= 7) {
+      val = val.replace(/(\d{3})(\d{0,4})/, '$1-$2')
+    } else {
+      val = val.replace(/(\d{3})(\d{0,4})(\d{0,4})/, '$1-$2-$3')
+    }
+
+    return val
+  },
+
+  // 全局安装器
+  install (Vue) {
+    Vue.filter('cutStr', this.cutStr)
+    Vue.filter('fmtPhoneNumber', this.fmtPhoneNumber)
+  }
+}
+
+export default strFilter

+ 49 - 0
csair-vr-portal-ui/src/main.js

@@ -0,0 +1,49 @@
+// The Vue build version to load with the `import` command
+// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
+import Vue from 'vue'
+import App from './App'
+import router from './router'
+
+/* 全局安装[通用组件] */
+import Layout from '@/components/layout'
+
+import Constants from '@/constants/constants'
+/* 全局安装[过滤器] */
+import './filters'
+
+import './plugins'
+
+import '@/assets/css/app.css'
+
+/* 进度条(网络请求、页面路由切换) */
+import NProgress from 'nprogress'
+import 'nprogress/nprogress.css'
+NProgress.configure({
+  easing: 'ease', // 动画方式
+  speed: 500, // 递增进度条的速度
+  showSpinner: false, // 是否显示加载ico
+  trickleSpeed: 200, // 自动递增间隔
+  minimum: 0.3 // 初始化时的最小百分比
+})
+
+Vue.config.productionTip = false
+Vue.prototype.constants = Constants
+Vue.prototype.PORTAL_URL = process.env.PORTAL_URL
+Vue.prototype.ADMIN_URL = process.env.ADMIN_URL
+Vue.prototype.HOME_URL = process.env.HOME_URL
+Vue.prototype.YUN_URL = process.env.YUN_URL
+Vue.prototype.OSS_URL = process.env.OSS_URL
+
+Vue.use(Layout)
+
+Vue.directive('focus', function (el) {
+  el.querySelector('input').focus()
+})
+
+/* eslint-disable no-new */
+new Vue({
+  el: '#app',
+  router,
+  components: { App },
+  template: '<App/>'
+})

+ 5 - 0
csair-vr-portal-ui/src/plugins/element-ui.js

@@ -0,0 +1,5 @@
+import Vue from 'vue'
+import ElementUI from 'element-ui'
+import 'element-ui/lib/theme-chalk/index.css'
+
+Vue.use(ElementUI, { size: 'mini', zIndex: 3000 })

+ 1 - 0
csair-vr-portal-ui/src/plugins/index.js

@@ -0,0 +1 @@
+import './element-ui'

+ 82 - 0
csair-vr-portal-ui/src/router/index.js

@@ -0,0 +1,82 @@
+import Vue from 'vue'
+import Router from 'vue-router'
+import NProgress from 'nprogress'
+
+Vue.use(Router)
+
+const router = new Router({
+  routes: [{
+    path: '/login',
+    name: 'Login',
+    component: () => import(`@/views/login`),
+    meta: {title: '登录'}
+  }, {
+    path: '/logout',
+    name: 'Logout',
+    component: () => import(`@/views/logout`),
+    meta: {title: '退出登录'}
+  }, {
+      path: '/403',
+      name: '403',
+      component: () => import(`@/views/exception/403`),
+      meta: {title: '403'}
+    }, {
+    path: '/',
+    redirect: '/index'
+  }, {
+    path: '/index',
+    name: 'index',
+    component: () => import(`@/views/index`),
+    meta: {title: '首页'}
+  }, {
+    path: '/main',
+    name: 'main',
+    component: () => import(`@/views/main`),
+    children: [{
+      path: '/search',
+      name: 'search',
+      component: () => import(`@/views/search`),
+      meta: {title: '搜索'}
+    }, {
+      path: '/main/new',
+      name: 'new',
+      component: () => import('@/views/new/index'),
+      meta: {title: '新闻资讯'}
+    }, {
+      path: '/main/new/:newId',
+      name: 'newDetail',
+      component: () => import('@/views/new/detail'),
+      meta: {title: '新闻资讯'}
+    }, {
+      path: '/main/vr',
+      name: 'vr',
+      component: () => import('@/views/vr/index'),
+      meta: {title: 'VR全景图'}
+    }, {
+      path: '/redit/:path/:searchValue',
+      name: 'redit',
+      component: () => import('@/views/redit')
+    }, {
+      path: '/main/course',
+      name: 'course',
+      component: () => import('@/views/course/index'),
+      meta: {title: '课程介绍'}
+    }, {
+      path: '/main/course/:courseId',
+      name: 'courseDetail',
+      component: () => import('@/views/course/detail'),
+      meta: {title: '课程详情'}
+    }]
+  }]
+})
+
+router.beforeEach((to, from, next) => {
+  next()
+  NProgress.start() // 每次切换页面时,调用进度条
+})
+
+router.afterEach(() => {
+  NProgress.done() // 在即将进入新的页面组件前,关闭掉进度条
+})
+
+export default router

+ 319 - 0
csair-vr-portal-ui/src/utils/date-kit.js

@@ -0,0 +1,319 @@
+/*eslint-disable*/
+// 把 YYYY-MM-DD 改成了 yyyy-MM-dd
+(function (main) {
+    'use strict';
+
+    /**
+     * Parse or format dates
+     * @class fecha
+     */
+    var fecha = {};
+    var token = /d{1,4}|M{1,4}|yy(?:yy)?|S{1,3}|Do|ZZ|([HhMsDm])\1?|[aA]|"[^"]*"|'[^']*'/g;
+    var twoDigits = /\d\d?/;
+    var threeDigits = /\d{3}/;
+    var fourDigits = /\d{4}/;
+    var word = /[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i;
+    var noop = function () {
+    };
+
+    function shorten(arr, sLen) {
+        var newArr = [];
+        for (var i = 0, len = arr.length; i < len; i++) {
+            newArr.push(arr[i].substr(0, sLen));
+        }
+        return newArr;
+    }
+
+    function monthUpdate(arrName) {
+        return function (d, v, i18n) {
+            var index = i18n[arrName].indexOf(v.charAt(0).toUpperCase() + v.substr(1).toLowerCase());
+            if (~index) {
+                d.month = index;
+            }
+        };
+    }
+
+    function pad(val, len) {
+        val = String(val);
+        len = len || 2;
+        while (val.length < len) {
+            val = '0' + val;
+        }
+        return val;
+    }
+
+    var dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
+    var monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
+    var monthNamesShort = shorten(monthNames, 3);
+    var dayNamesShort = shorten(dayNames, 3);
+    fecha.i18n = {
+        dayNamesShort: dayNamesShort,
+        dayNames: dayNames,
+        monthNamesShort: monthNamesShort,
+        monthNames: monthNames,
+        amPm: ['am', 'pm'],
+        DoFn: function DoFn(D) {
+            return D + ['th', 'st', 'nd', 'rd'][D % 10 > 3 ? 0 : (D - D % 10 !== 10) * D % 10];
+        }
+    };
+
+    var formatFlags = {
+        D: function (dateObj) {
+            return dateObj.getDay();
+        },
+        DD: function (dateObj) {
+            return pad(dateObj.getDay());
+        },
+        Do: function (dateObj, i18n) {
+            return i18n.DoFn(dateObj.getDate());
+        },
+        d: function (dateObj) {
+            return dateObj.getDate();
+        },
+        dd: function (dateObj) {
+            return pad(dateObj.getDate());
+        },
+        ddd: function (dateObj, i18n) {
+            return i18n.dayNamesShort[dateObj.getDay()];
+        },
+        dddd: function (dateObj, i18n) {
+            return i18n.dayNames[dateObj.getDay()];
+        },
+        M: function (dateObj) {
+            return dateObj.getMonth() + 1;
+        },
+        MM: function (dateObj) {
+            return pad(dateObj.getMonth() + 1);
+        },
+        MMM: function (dateObj, i18n) {
+            return i18n.monthNamesShort[dateObj.getMonth()];
+        },
+        MMMM: function (dateObj, i18n) {
+            return i18n.monthNames[dateObj.getMonth()];
+        },
+        yy: function (dateObj) {
+            return String(dateObj.getFullYear()).substr(2);
+        },
+        yyyy: function (dateObj) {
+            return dateObj.getFullYear();
+        },
+        h: function (dateObj) {
+            return dateObj.getHours() % 12 || 12;
+        },
+        hh: function (dateObj) {
+            return pad(dateObj.getHours() % 12 || 12);
+        },
+        H: function (dateObj) {
+            return dateObj.getHours();
+        },
+        HH: function (dateObj) {
+            return pad(dateObj.getHours());
+        },
+        m: function (dateObj) {
+            return dateObj.getMinutes();
+        },
+        mm: function (dateObj) {
+            return pad(dateObj.getMinutes());
+        },
+        s: function (dateObj) {
+            return dateObj.getSeconds();
+        },
+        ss: function (dateObj) {
+            return pad(dateObj.getSeconds());
+        },
+        S: function (dateObj) {
+            return Math.round(dateObj.getMilliseconds() / 100);
+        },
+        SS: function (dateObj) {
+            return pad(Math.round(dateObj.getMilliseconds() / 10), 2);
+        },
+        SSS: function (dateObj) {
+            return pad(dateObj.getMilliseconds(), 3);
+        },
+        a: function (dateObj, i18n) {
+            return dateObj.getHours() < 12 ? i18n.amPm[0] : i18n.amPm[1];
+        },
+        A: function (dateObj, i18n) {
+            return dateObj.getHours() < 12 ? i18n.amPm[0].toUpperCase() : i18n.amPm[1].toUpperCase();
+        },
+        ZZ: function (dateObj) {
+            var o = dateObj.getTimezoneOffset();
+            return (o > 0 ? '-' : '+') + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4);
+        }
+    };
+
+    var parseFlags = {
+        d: [twoDigits, function (d, v) {
+            d.day = v;
+        }],
+        M: [twoDigits, function (d, v) {
+            d.month = v - 1;
+        }],
+        yy: [twoDigits, function (d, v) {
+            var da = new Date(), cent = +('' + da.getFullYear()).substr(0, 2);
+            d.year = '' + (v > 68 ? cent - 1 : cent) + v;
+        }],
+        h: [twoDigits, function (d, v) {
+            d.hour = v;
+        }],
+        m: [twoDigits, function (d, v) {
+            d.minute = v;
+        }],
+        s: [twoDigits, function (d, v) {
+            d.second = v;
+        }],
+        yyyy: [fourDigits, function (d, v) {
+            d.year = v;
+        }],
+        S: [/\d/, function (d, v) {
+            d.millisecond = v * 100;
+        }],
+        SS: [/\d{2}/, function (d, v) {
+            d.millisecond = v * 10;
+        }],
+        SSS: [threeDigits, function (d, v) {
+            d.millisecond = v;
+        }],
+        D: [twoDigits, noop],
+        ddd: [word, noop],
+        MMM: [word, monthUpdate('monthNamesShort')],
+        MMMM: [word, monthUpdate('monthNames')],
+        a: [word, function (d, v, i18n) {
+            var val = v.toLowerCase();
+            if (val === i18n.amPm[0]) {
+                d.isPm = false;
+            } else if (val === i18n.amPm[1]) {
+                d.isPm = true;
+            }
+        }],
+        ZZ: [/[\+\-]\d\d:?\d\d/, function (d, v) {
+            var parts = (v + '').match(/([\+\-]|\d\d)/gi), minutes;
+
+            if (parts) {
+                minutes = +(parts[1] * 60) + parseInt(parts[2], 10);
+                d.timezoneOffset = parts[0] === '+' ? minutes : -minutes;
+            }
+        }]
+    };
+    parseFlags.DD = parseFlags.DD;
+    parseFlags.dddd = parseFlags.ddd;
+    parseFlags.Do = parseFlags.dd = parseFlags.d;
+    parseFlags.mm = parseFlags.m;
+    parseFlags.hh = parseFlags.H = parseFlags.HH = parseFlags.h;
+    parseFlags.MM = parseFlags.M;
+    parseFlags.ss = parseFlags.s;
+    parseFlags.A = parseFlags.a;
+
+
+    // Some common format strings
+    fecha.masks = {
+        'default': 'ddd MMM dd yyyy HH:mm:ss',
+        shortDate: 'M/D/yy',
+        mediumDate: 'MMM d, yyyy',
+        longDate: 'MMMM d, yyyy',
+        fullDate: 'dddd, MMMM d, yyyy',
+        shortTime: 'HH:mm',
+        mediumTime: 'HH:mm:ss',
+        longTime: 'HH:mm:ss.SSS'
+    };
+
+    /***
+     * Format a date
+     * @method format
+     * @param {Date|number} dateObj
+     * @param {string} mask Format of the date, i.e. 'mm-dd-yy' or 'shortDate'
+     */
+    fecha.format = function (dateObj, mask, i18nSettings) {
+        var i18n = i18nSettings || fecha.i18n;
+
+        if (typeof dateObj === 'number') {
+            dateObj = new Date(dateObj);
+        }
+
+        if (Object.prototype.toString.call(dateObj) !== '[object Date]' || isNaN(dateObj.getTime())) {
+            throw new Error('Invalid Date in fecha.format');
+        }
+
+        mask = fecha.masks[mask] || mask || fecha.masks['default'];
+
+        return mask.replace(token, function ($0) {
+            return $0 in formatFlags ? formatFlags[$0](dateObj, i18n) : $0.slice(1, $0.length - 1);
+        });
+    };
+
+    /**
+     * Parse a date string into an object, changes - into /
+     * @method parse
+     * @param {string} dateStr Date string
+     * @param {string} format Date parse format
+     * @returns {Date|boolean}
+     */
+    fecha.parse = function (dateStr, format, i18nSettings) {
+        var i18n = i18nSettings || fecha.i18n;
+
+        if (typeof format !== 'string') {
+            throw new Error('Invalid format in fecha.parse');
+        }
+
+        format = fecha.masks[format] || format;
+
+        // Avoid regular expression denial of service, fail early for really long strings
+        // https://www.owasp.org/index.php/Regular_expression_Denial_of_Service_-_ReDoS
+        if (dateStr.length > 1000) {
+            return false;
+        }
+
+        var isValid = true;
+        var dateInfo = {};
+        format.replace(token, function ($0) {
+            if (parseFlags[$0]) {
+                var info = parseFlags[$0];
+                var index = dateStr.search(info[0]);
+                if (!~index) {
+                    isValid = false;
+                } else {
+                    dateStr.replace(info[0], function (result) {
+                        info[1](dateInfo, result, i18n);
+                        dateStr = dateStr.substr(index + result.length);
+                        return result;
+                    });
+                }
+            }
+
+            return parseFlags[$0] ? '' : $0.slice(1, $0.length - 1);
+        });
+
+        if (!isValid) {
+            return false;
+        }
+
+        var today = new Date();
+        if (dateInfo.isPm === true && dateInfo.hour != null && +dateInfo.hour !== 12) {
+            dateInfo.hour = +dateInfo.hour + 12;
+        } else if (dateInfo.isPm === false && +dateInfo.hour === 12) {
+            dateInfo.hour = 0;
+        }
+
+        var date;
+        if (dateInfo.timezoneOffset != null) {
+            dateInfo.minute = +(dateInfo.minute || 0) - +dateInfo.timezoneOffset;
+            date = new Date(Date.UTC(dateInfo.year || today.getFullYear(), dateInfo.month || 0, dateInfo.day || 1,
+                dateInfo.hour || 0, dateInfo.minute || 0, dateInfo.second || 0, dateInfo.millisecond || 0));
+        } else {
+            date = new Date(dateInfo.year || today.getFullYear(), dateInfo.month || 0, dateInfo.day || 1,
+                dateInfo.hour || 0, dateInfo.minute || 0, dateInfo.second || 0, dateInfo.millisecond || 0);
+        }
+        return date;
+    };
+
+    /* istanbul ignore next */
+    if (typeof module !== 'undefined' && module.exports) {
+        module.exports = fecha;
+    } else if (typeof define === 'function' && define.amd) {
+        define(function () {
+            return fecha;
+        });
+    } else {
+        main.fecha = fecha;
+    }
+})(this);

+ 121 - 0
csair-vr-portal-ui/src/utils/http-kit.js

@@ -0,0 +1,121 @@
+import {Message} from 'element-ui'
+import NProgress from 'nprogress'
+import axios from 'axios'
+// import router from '../router'
+
+axios.defaults.baseURL = process.env.HOME_URL
+axios.defaults.timeout = 30 * 1000 // 设置接口响应时间
+// let loginUrl = '/login'
+
+/**
+ * 功能描述:Http Request 拦截器
+ */
+axios.interceptors.request.use((config) => {
+  NProgress.start() // 展示进度条
+
+  config.headers.common['authorization'] = 'Bearer ' + localStorage.getItem('token') // todo 设置真实token
+  config.headers.common['Content-Type'] = 'application/json;charset=UTF-8'
+
+  return config
+}, (error) => { // 请求错误时做些事(接口错误、超时等)
+  NProgress.done() // 隐藏进度条
+  Message.error(`请求参数错误`, 10)
+
+  /* 1.判断请求超时 */
+  if (error.code === 'ECONNABORTED' && error.message.indexOf('timeout') !== -1) {
+    // return service.request(originalRequest);//例如再重复请求一次
+  }
+
+  /* 2.需要重定向到错误页面 */
+  if (error.response) {
+    // error =errorInfo.data//页面那边catch的时候就能拿到详细的错误信息,看最下边的Promise.reject
+    // router.push({
+    //   path: `/error/${errorInfo.status}`// 404 403 500 ... 等
+    // })
+  }
+
+  return Promise.reject(error) // 在调用的那边可以拿到(catch)你想返回的错误信息
+})
+
+/**
+ * 功能描述:Http Response 拦截器
+ */
+axios.interceptors.response.use((res) => {
+  NProgress.done() // 隐藏进度条
+  let isTips = res.config.showMessage === undefined || res.config.showMessage === true
+  let data = res.data
+
+  /* 根据返回的code值来做不同的处理(和后端约定) */
+  switch (data.errorCode) {
+    case 0:
+    case 200:
+      return Promise.resolve(data)
+    default:
+      isTips && Message.error(`服务器返回异常:${data.message}`, 10)
+      return Promise.reject(res)
+  }
+}, (error) => {
+  Message.error(`服务器返回异常:${error}`, 10)
+
+  if (error && error.response) {
+    switch (error.response.status) {
+      case 400:
+        break
+      case 403:
+        window.location.href = `#/403`
+        break
+      default:
+        break
+    }
+  }
+
+  return Promise.reject(error)
+})
+
+export default {
+  get (url, data = {}) {
+    return new Promise((resolve, reject) => {
+      axios.get(url, data).then(response => {
+        resolve(response)
+      }).catch(err => {
+        reject(err)
+      })
+    })
+  },
+  delete (url, data = {}) {
+    return new Promise((resolve, reject) => {
+      axios.delete(url, data).then(response => {
+        resolve(response)
+      }).catch(err => {
+        reject(err)
+      })
+    })
+  },
+  post (url, data = {}) {
+    return new Promise((resolve, reject) => {
+      axios.post(url, data).then(response => {
+        resolve(response)
+      }, err => {
+        reject(err)
+      })
+    })
+  },
+  put (url, data = {}) {
+    return new Promise((resolve, reject) => {
+      axios.put(url, data).then(response => {
+        resolve(response)
+      }, err => {
+        reject(err)
+      })
+    })
+  },
+  patch (url, data = {}) {
+    return new Promise((resolve, reject) => {
+      axios.patch(url, data).then(response => {
+        resolve(response)
+      }, err => {
+        reject(err)
+      })
+    })
+  }
+}

+ 20 - 0
csair-vr-portal-ui/src/utils/str-kit.js

@@ -0,0 +1,20 @@
+export default {
+  /**
+   * 功能描述:计算文本的长度
+   * @param strKit
+   * @returns {number}
+   */
+  getByteLen (strKit) {
+    let len = 0
+    if (typeof strKit === 'string' && strKit.constructor === String) {
+      for (let i = 0; i < strKit.length; i++) {
+        if (strKit[i].match(/[^x00-xff]/ig)) { // 全角
+          len += 2
+        } else {
+          len += 1
+        }
+      }
+    }
+    return len
+  }
+}

+ 74 - 0
csair-vr-portal-ui/src/views/course/detail.vue

@@ -0,0 +1,74 @@
+<template>
+  <div>
+  <div style=" background-color: #fff;width: 1100px; margin: 0px auto;padding-left: 50px;padding-top: 59px;padding-right: 50px;padding-bottom: 200px">
+    <el-breadcrumb separator-class="el-icon-arrow-right">
+      <el-breadcrumb-item  :to="{ path: '/' }"><span style="color: #2a92de;font-size: 14px;font-weight: 400">首页</span></el-breadcrumb-item>
+      <el-breadcrumb-item :to="{path: '/main/course'}"><span style="color: #2a92de;font-size: 14px;font-weight: 400">课程介绍</span></el-breadcrumb-item>
+      <el-breadcrumb-item><span style="font-size: 14px;font-weight: 400">正文(课程)</span></el-breadcrumb-item>
+    </el-breadcrumb>
+
+    <el-row style="margin-top: 36px;text-align: left"><span style="width: 1000px;height: 47px;font-size: 48px;line-height: 64px;color: #333333;">{{coures.courseTitle}}</span></el-row>
+    <el-row style="margin-top: 19px;text-align: left"><span style="font-size: 12px;color: #9c9795;">{{coures.gmtModified | fmtDate('yyyy-MM-dd')}}</span></el-row>
+    <el-row style="margin-top: 60px;"><hr style="color: #b3b0ac;height: 1px;border:none; border-top:1px solid #f0f0f0;"></el-row>
+
+    <el-row style="margin-top: 60px;width: 1000px; height: 64px;background-color: #f0f0f0;border-radius: 4px;padding-top: 21px">
+      <span style="padding-left: 90px;width: 807px;height: 28px;font-family: MicrosoftYaHei;font-size: 16px;font-weight: normal;font-stretch: normal;line-height: 24px;letter-spacing: 0px;color: #787878;word-break: break-all;text-overflow: ellipsis;overflow: hidden;display: -webkit-box;-webkit-line-clamp: 1;-webkit-box-orient: vertical;">{{coures.intro}}</span>
+    </el-row>
+
+    <el-row class="ql-editor" style="margin-top: 60px;padding-bottom: 50px;font-family: MicrosoftYaHei; font-size: 18px; font-weight: normal; font-stretch: normal; line-height: 36px; letter-spacing: 0px; color: #333333;">
+      <div v-html="this.coures.content"></div>
+    </el-row>
+  </div>
+    <div>
+      <el-row style="text-align: center; height: 160px; background-color: #002063;">
+        <label style="font-family: 'Microsoft Yahei'; font-size: 12px; color: #fff; letter-spacing: 1px; line-height: 159px;">Copyright(C)1997-2019 中国南方航空股份有限公司 版权所有</label>
+      </el-row>
+    </div>
+  </div>
+</template>
+
+<script>
+  import couresApi from '@/api/couresApi'
+  export default {
+    name: 'CourseDetail',
+    components: {},
+    data () {
+      return {
+        courseId: '',
+        coures: {
+          courseTitle: '',
+          content: '',
+          gmtModified: ''
+        }
+      }
+    },
+    mounted () {
+      let main = document.getElementsByClassName('el-main')
+      main[0].style.padding = '0px 0px 0px 0px'
+      let s = document.getElementsByClassName('el-breadcrumb')
+      s[0].scrollIntoView()
+      this.courseId = this.$route.params.courseId
+      this.list()
+    },
+    methods: {
+      list () {
+        couresApi.findById(this.courseId).then(data => {
+          this.coures = data
+        })
+      }
+    }
+  }
+</script>
+<style>
+  .ql-editor el-row {
+    font-family: MicrosoftYaHei;
+    font-size: 18px;
+    font-weight: normal;
+    font-stretch: normal;
+    line-height: 36px;
+    letter-spacing: 0px;
+    color: #333333;
+  }
+</style>
+<style scoped>
+</style>

+ 104 - 0
csair-vr-portal-ui/src/views/course/index.vue

@@ -0,0 +1,104 @@
+<template>
+  <white-board style="width: 1100px; margin: 0px auto;padding-bottom: 0px">
+    <div v-infinite-scroll="loadMore">
+    <el-row style="text-align: left;padding: 59px 0px 0px 25px;"><span style="font-size: 30px; font-weight: bold; color: #008ed3;">课程介绍</span></el-row>
+
+    <div class="c" ref="c" style="overflow: auto">
+      <el-row style="width: 1050px;">
+        <el-col :span="8" v-for="(item, index) in tableData" :key="index" style="padding-top: 40px;">
+          <div class="ho" style="padding-left: 17px;">
+            <router-link :to="{name: 'courseDetail', params: {courseId: item.id}}">
+              <div @mouseover="ColorChange(index)" @mouseleave="ColorChangeBack(index)">
+              <el-row style="width: 314px;min-height: 354px;padding-top: 10px;background-color: #FAFCFE"   :id="'course' + index">
+                <el-row><img :src="OSS_URL + item.imageUrl" style="width: 294px;height: 204px;padding-left: 10px"></el-row>
+                <el-row><label style="width: 271px;line-height: 24px;text-align: left;padding-left: 23px;padding-right: 20px;font-size: 18px;margin-top: 15px;font-weight: bold;word-break: break-all;text-overflow: ellipsis;overflow: hidden;display: -webkit-box;-webkit-line-clamp: 1;-webkit-box-orient: vertical;">{{item.courseTitle}}</label>
+                </el-row>
+                <el-row>
+                  <label style="opacity: 0.8;line-height: 24px;color: #333333;width: 233px; height : 42px;min-height: 40px;padding-left: 38px;padding-right: 33px;text-align: left;margin-top: 12px;font-size: 12px;word-break: break-all;text-overflow: ellipsis;overflow: hidden;display: -webkit-box;-webkit-line-clamp: 2;-webkit-box-orient: vertical;">{{item.intro}}</label>
+                </el-row>
+                <el-row style="padding-top: 10px;width: 314px;text-align: right;"><label style="color: darkgrey;font-size: 10px;margin-top: 12px;padding-right: 10px">{{item.gmtCreated | fmtDate('yyyy-MM-dd')}}</label></el-row>
+              </el-row>
+              </div>
+            </router-link>
+          </div>
+        </el-col>
+      </el-row>
+    </div>
+    </div>
+    <el-row style="height: 50px;margin-left: 50px;margin-right: 59px;padding-top: 30px">
+      <hr style="opacity: 0.1;color: #999999;height: 1px;border:none; border-top:1px solid #999999;width: 953px">
+      <el-col :span="1"><div style="width:50px;height: 50px;border-left: solid 1px #999999;opacity: 0.1;"></div></el-col>
+      <el-col :span="22"> <div style="padding-left:  424px;padding-top: 13px;width: 100px; height: 16px;"><label style="  opacity: 0.5;letter-spacing: 0px;font-weight: normal;font-stretch: normal;font-size: 16px;line-height: 24px;color: #333333">加载中...</label></div></el-col>
+      <el-col :span="1"><div style="width:50px;height: 50px;border-right: solid 1px #999999;opacity: 0.1;"></div></el-col>
+    </el-row>
+  </white-board>
+</template>
+
+<script>
+  import couresApi from '@/api/couresApi'
+
+  export default {
+    name: 'CourseIndex',
+    data () {
+      return {
+        searchForm: {
+          page: 1,
+          pageSize: 6,
+          type: '2'
+        },
+        tableData: [],
+
+        wait: '',
+        timer: ''
+      }
+    },
+    mounted () {
+      const that = this
+      this.timer = setInterval(function () {
+        that.wait = 0
+      }, 1000)
+
+      this.list()
+    },
+    beforeDestroy () {
+      if (this.timer) {
+        clearInterval(this.timer)
+      }
+    },
+    methods: {
+      list () {
+          couresApi.page(this.searchForm).then(data => {
+            if (data.list != null) {
+              for (var l of data.list) {
+                this.tableData.push(l)
+               }
+              }
+          })
+      },
+      loadMore () {
+        if (this.wait === 0) {
+          this.searchForm.page = this.searchForm.page + 1
+          this.list()
+          this.wait = 1
+        }
+      },
+      ColorChange (index) {
+        let course = document.getElementById('course' + index)
+        course.style.color = '#1989fa'
+        course.style.backgroundColor = '#eaf4fd'
+      },
+      ColorChangeBack (index) {
+        let course = document.getElementById('course' + index)
+        course.style.color = '#333333'
+        course.style.backgroundColor = '#FAFCFE'
+      }
+    }
+  }
+</script>
+
+<style scoped>
+/*  /deep/.ho div:hover {
+    color: #1989fa;
+    background-color: #eaf4fd;
+  }*/
+</style>

+ 80 - 0
csair-vr-portal-ui/src/views/exception/403.vue

@@ -0,0 +1,80 @@
+<template>
+
+  <div>
+      <el-header style="height: 75px;padding-top: 5px">
+        <div style="width: 200px; height: 60px; float: left;margin-left: 300px">
+          <img src="../../assets/images/logo.png" width="223" height="47"
+               style="display: inline-block; vertical-align: middle; padding-left: 30px;"/>
+        </div>
+      </el-header>
+    <el-row style="min-height:100%;margin-bottom:-70px;">
+      <div style="background: url('/static/img/403bg.5c1ab2a.png'); height: 1080px;width: 1920px ;margin-bottom:130px;position:fixed;z-index:-1">
+        <div style=" position: fixed;top: 48%;left: 40%;">
+          <el-row style="padding: 15px 15px 15px 15px;background-color: white;box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04);">
+          <el-row>
+          <label style="color: red;font-size: 20px;">对不起,您没有权限进入本管理后台</label>
+          </el-row>
+          <el-row style="margin-top: 8px;color: #848484">
+            <span >页面将在 </span>{{this.total}}<span> 秒后跳转</span>
+          </el-row>
+          </el-row>
+        </div>
+      </div>
+    </el-row>
+
+    <el-footer height="100px" style="padding: 0 0 0 0 ">
+      <el-row style="text-align: center; height: 100px; background-color: #E4EFF5;">
+        <label
+          style="font-family: 'Microsoft Yahei'; font-size: 12px; color: #000; letter-spacing: 1px; line-height: 105px;">Copyright(C)1997-2019 中国南方航空股份有限公司 版权所有</label>
+      </el-row>
+    </el-footer>
+  </div>
+</template>
+
+<script>
+  export default {
+    name: 'page403',
+    components: {},
+    data () {
+      return {
+        total: 5,
+        timer: null
+      }
+    },
+    watch: {
+      total: (val) => {
+          if (val === 0) {
+            clearInterval(this.timer)
+            window.location.href = `/edu/#/login`
+          }
+      }
+    },
+    mounted () {
+      this.timer = window.setInterval(() => {
+        this.total--
+      }, 1000)
+    },
+    methods: {}
+  }
+</script>
+
+<style>
+  .el-header  {
+    background-color: #ffffff;
+    color: #333;
+    text-align: center;
+    line-height: 60px;
+    padding-left: 0px;
+    padding-right: 0px;
+  }
+
+  .el-footer {
+    position: absolute;
+    bottom: 0;
+    width:100%;
+  }
+  html,body{
+    height:100%;
+  }
+
+</style>

File diff suppressed because it is too large
+ 69 - 0
csair-vr-portal-ui/src/views/index.vue


+ 20 - 0
csair-vr-portal-ui/src/views/login.vue

@@ -0,0 +1,20 @@
+<template>
+  <div>login</div>
+</template>
+
+<script>
+  export default {
+    name: 'Login',
+    components: {},
+    data () {
+      return {}
+    },
+    mounted () {
+    },
+    methods: {}
+  }
+</script>
+
+<style scoped>
+
+</style>

+ 20 - 0
csair-vr-portal-ui/src/views/logout.vue

@@ -0,0 +1,20 @@
+<template>
+  <div>logout</div>
+</template>
+
+<script>
+  export default {
+    name: 'Logout',
+    components: {},
+    data () {
+      return {}
+    },
+    mounted () {
+    },
+    methods: {}
+  }
+</script>
+
+<style scoped>
+
+</style>

+ 227 - 0
csair-vr-portal-ui/src/views/main.vue

@@ -0,0 +1,227 @@
+<template>
+  <div>
+  <el-container>
+    <el-header style="background-color: #fff;height: 80px;width: 1920px;padding :0 0 0 0">
+      <el-row style="height: 80px;background-color: white;box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04);z-index: 999">
+        <el-col :span="6" style="text-align: left;"><img src="@/assets/images/logo.png" style="padding-top: 17px; margin-left: 6%;cursor: pointer;" @click="back"></el-col>
+        <el-col :span="6" :offset="8" >
+          <div @keyup.enter="search" style="padding-top: 9px;">
+          <el-input v-model.trim="searchKey" placeholder="课程介绍" ref="searchInput"  style=" width: 183px;height: 32px;font-size: 14px;" ><i slot="suffix" style="padding-top: 5px" class="el-input__icon el-icon-search" @click="search"></i></el-input>
+          </div>
+        </el-col>
+        <el-col :span="1">
+          <el-link :underline="false" style="font-size: 14px;line-height: 48px;padding-top: 15px">退出登录</el-link>
+        </el-col>
+        <el-col :span="3" style="padding-left: 34px">
+          <el-col :span="1">
+          <img src="@/assets/images/head-logo.png"  style="width: 47px;height: 47px;cursor: pointer;padding-top: 17px"/>
+          </el-col>
+          <el-col :span="20" style="padding-top:9px;padding-left: 10px">
+          <label style="width: 81px;color: #4a5162;height: 14px;font-size: 14px;line-height: 48px;">SHAW-小岩</label>
+          </el-col>
+        </el-col>
+      </el-row>
+    </el-header>
+
+    <el-container>
+      <el-main :style="{height: (screenHeight - 80) + 'px', padding: 0 + 'px ' + 10 + 'px', paddingTop: 0 + 'px'}">
+        <transition name="el-fade-in-linear">
+          <router-view />
+        </transition>
+      </el-main>
+    </el-container>
+
+    <el-backtop target=".el-main" :bottom="420" :right="50" >
+      <div
+        style="{
+          color: #1989fa;
+          width: 58px;
+          height: 58px;
+          background-color: #e9f4fc;
+          border-radius: 0px;
+          box-shadow: 0 0 6px rgba(0,0,0, .12);
+      }"
+        @mouseover="backTopChange"
+        @mouseleave="backTopChangeBack()"
+      >
+        <img v-if="this.backTopTemp === 0" src="../assets/images/backTop.png"/>
+        <img v-if="this.backTopTemp === 1" src="../assets/images/backTopBack.png"/>
+      </div>
+    </el-backtop>
+  </el-container>
+    <el-row></el-row>
+    <div class="fixed" id="searchBar"  @mouseover="backToIndexChange" @mouseleave="backToIndexChangeBack()" style="font-size: 16px;font-family: MicrosoftYaHei;font-weight: normal;font-stretch: normal;line-height: 20px;letter-spacing: 2px;color: #2a92de;box-shadow: 0 0 6px rgba(0,0,0,.12);cursor: pointer"><div style="margin-left:13px;margin-top:9px;width: 40px;height: 36px"  @click="back">回到首页</div></div>
+  </div>
+</template>
+
+<script>
+  export default {
+    name: 'Main',
+    components: {
+    },
+    data () {
+      return {
+        backTopTemp: 0,
+        timerForBackTop: null,
+        screenHeight: document.documentElement.clientHeight, // 获取可视屏幕高度
+        isAsideMenuCollapse: false,
+        searchKey: '',
+        searchValue: '',
+        path: '',
+        TIME_COUNT: null
+      }
+    },
+    watch: {
+      screenHeight (val) {
+        // 为了避免频繁触发resize函数导致页面卡顿,使用定时器
+        if (!this.timer) {
+          // 一旦监听到的screenWidth值改变,就将其重新赋给data里的screenWidth
+          this.screenHeight = val
+          this.timer = true
+          let that = this
+          setTimeout(function () {
+            that.timer = false
+          }, 400)
+        }
+      }
+    },
+    mounted () {
+      const that = this
+      window.onresize = () => {
+        return (() => {
+          window.screenHeight = document.documentElement.clientHeight
+          that.screenHeight = window.screenHeight
+        })()
+      }
+      var _type = this.$route.params.type
+      var _searchValue = this.$route.params.searchValue
+      if (_searchValue !== '' & _type === 1) {
+        this.searchKey = _searchValue
+        var _path = 'search'
+        this.$router.push({name: 'redit', params: {path: _path, searchValue: _searchValue}})
+      }
+      this.send()
+      window.addEventListener('scroll', this.handleScroll)
+    },
+    beforeDestroy () {
+      this.destroyed()
+    },
+    methods: {
+      backTopChange () {
+        this.backTopTemp = 1
+        debugger
+      },
+      backTopChangeBack () {
+        this.backTopTemp = 0
+      },
+      backToIndexChange () {
+        let backToIndex = document.getElementsByClassName('fixed')
+        backToIndex[0].style.color = '#fff'
+        backToIndex[0].style.backgroundColor = '#2a92de'
+      },
+      backToIndexChangeBack () {
+        let backToIndex = document.getElementsByClassName('fixed')
+        backToIndex[0].style.color = '#2a92de'
+        backToIndex[0].style.backgroundColor = '#e9f4fc'
+      },
+      handleScroll () {
+        var scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop
+        var offsetTop = document.querySelector('#searchBar').offsetTop
+
+        if (scrollTop <= 200) {
+          offsetTop = 400
+          document.querySelector('#searchBar').style.top = offsetTop + 'px'
+        } else {
+          document.querySelector('#searchBar').style.top = '100px'
+        }
+      },
+
+      destroyed () {
+        window.removeEventListener('scroll', this.handleScroll)
+      },
+      send () {
+        if (!this.timer) {
+          this.count = this.TIME_COUNT
+          this.show = false
+          this.timer = setInterval(() => {
+            if (this.count > 0 && this.count <= this.TIME_COUNT) {
+              this.count--
+            } else {
+              this.show = true
+              clearInterval(this.timer)// 清除定时器
+              this.timer = null
+            }
+          }, 1000)
+        }
+      },
+      back () {
+        this.$router.push({name: 'index'})
+      },
+      search () {
+        this.searchValue = this.$refs.searchInput.value
+        this.path = 'search'
+        this.$router.push({name: 'redit', params: {path: this.path, searchValue: this.searchValue}})
+      }
+    }
+  }
+</script>
+
+<style>
+  .el-header, .el-footer {
+    background-color: #409EFF;
+    color: #333;
+    text-align: center;
+    line-height: 60px;
+    padding-left: 0px;
+    padding-right: 0px;
+  }
+
+  .el-footer {
+    height: 40px !important;
+    line-height: 40px !important;
+  }
+
+  .el-aside {
+    background-color: #fff;
+    color: #333;
+    text-align: center;
+  }
+
+  .el-main {
+    background-color: #eee;
+    color: #333;
+    padding: 0px;
+  }
+
+  body > .el-container {
+    margin-bottom: 40px;
+  }
+
+  .el-container:nth-child(5) .el-aside,
+  .el-container:nth-child(6) .el-aside {
+    line-height: 260px;
+  }
+
+  .el-container:nth-child(7) .el-aside {
+    line-height: 320px;
+  }
+  .fixed {
+    position: fixed;
+    bottom: 100px;
+    top: 400px;
+    left: 1820px;
+    border: solid 1px #ffffff;
+    width: 60px;
+    box-sizing: border-box;
+    z-index: 2;
+    background-color: #e9f4fc;
+    height: 60px;
+  }
+</style>
+<style scoped>
+  /deep/ .el-input--mini .el-input__inner{
+    background-color: #f4f4f4;
+    height: 32px;
+    border: 1px solid #fff;
+  }
+</style>

+ 65 - 0
csair-vr-portal-ui/src/views/new/detail.vue

@@ -0,0 +1,65 @@
+<template>
+  <div>
+  <div style="background-color: #fff;width: 1100px; margin: 0px auto;padding-left: 50px;padding-top: 59px;padding-right: 50px;padding-bottom: 200px">
+    <el-breadcrumb separator-class="el-icon-arrow-right">
+      <el-breadcrumb-item  :to="{ path: '/' }"><span style="line-height: 24px;;color: #2a92de;font-size: 14px;font-weight: 400;line-height: 24px;">首页</span></el-breadcrumb-item>
+      <el-breadcrumb-item :to="{path: '/main/new'}"><span style="line-height: 24px;;color: #2a92de;font-size: 14px;font-weight: 400;line-height: 24px;">新闻资讯</span></el-breadcrumb-item>
+      <el-breadcrumb-item><span style="line-height: 24px;;font-size: 14px;font-weight: 400;line-height: 24px;">正文(新闻)</span></el-breadcrumb-item>
+    </el-breadcrumb>
+    <el-row style="margin-top: 36px;text-align: left"><span style="width: 1000px;height: 47px;font-size: 48px;line-height: 64px;color: #333333;">{{news.newsTitle}}</span></el-row>
+    <el-row style="margin-top: 19px;text-align: left"><span style="font-size: 12px;color: #9c9795;">{{news.gmtModified | fmtDate('yyyy-MM-dd')}}</span></el-row>
+    <el-row style="margin-top: 60px;"><hr style="color: #b3b0ac;height: 1px;border:none; border-top:1px solid #f0f0f0;"></el-row>
+
+    <el-row style="margin-top: 60px;width: 1000px; height: 64px;background-color: #f0f0f0;border-radius: 4px;padding-top: 21px">
+      <span style="padding-left: 90px;width: 807px;height: 28px;font-family: MicrosoftYaHei;font-size: 16px;font-weight: normal;font-stretch: normal;line-height: 24px;letter-spacing: 0px;color: #787878;word-break: break-all;text-overflow: ellipsis;overflow: hidden;display: -webkit-box;-webkit-line-clamp: 1;-webkit-box-orient: vertical;">{{news.intro}}</span>
+    </el-row>
+    <el-row class="ql-editor" style="margin-top: 60px;padding-bottom: 50px;font-family: MicrosoftYaHei;font-size: 18px;font-weight: normal;font-stretch: normal;line-height: 36px;letter-spacing: 0px;color: #333333;">
+      <div v-html="this.news.content"></div>
+    </el-row>
+
+  </div>
+  <div>
+    <el-row style="text-align: center; height: 160px; background-color: #002063;">
+      <label style="font-family: 'Microsoft Yahei'; font-size: 12px; color: #fff; letter-spacing: 1px; line-height: 159px;">Copyright(C)1997-2019 中国南方航空股份有限公司 版权所有</label>
+    </el-row>
+  </div>
+  </div>
+</template>
+
+<script>
+  import newsApi from '@/api/newsApi'
+
+  export default {
+    name: 'newDetail',
+    components: {},
+    data () {
+      return {
+        newId: '',
+        news: {
+          newsTitle: '',
+          gmtModified: '',
+          content: ''
+        }
+      }
+    },
+    watch: {},
+    mounted () {
+      let main = document.getElementsByClassName('el-main')
+      main[0].style.padding = '0px 0px 0px 0px'
+    let s = document.getElementsByClassName('el-breadcrumb')
+    s[0].scrollIntoView()
+      this.newId = this.$route.params.newId
+      this.list()
+    },
+    methods: {
+      list () {
+        newsApi.findById(this.newId).then(data => {
+          this.news = data
+        })
+      }
+    }
+  }
+</script>
+
+<style scoped>
+</style>

+ 96 - 0
csair-vr-portal-ui/src/views/new/index.vue

@@ -0,0 +1,96 @@
+<template>
+  <white-board style="width: 1100px; margin: 0px auto;padding: 10px 0px 0px 0px !important;">
+    <el-row  style="text-align: left;padding: 59px 0px 10px 0px;margin-left: 50px"><span style="font-size: 30px; font-weight: bold; color: #008ed3;">新闻资讯</span></el-row>
+    <div v-infinite-scroll="loadMore">
+      <el-row class="ho" style="height: 205px;" v-for="(item,index) in tableData" :key="index" :ref="item.content">
+        <router-link :to="{name: 'newDetail', params: {newId: item.id}}">
+          <el-row style="height: 205px;padding-top: 30px">
+            <el-col :span="6"><img :src="OSS_URL + item.imageUrl" style="width: 220px;height: 144px;margin-left: 50px;border-radius: 4px"></el-col>
+            <el-col :span="18" style="padding-left: 25px">
+              <el-row style="margin-top: 10px;margin-left: 0px">
+                <el-col :span="1.5">
+                  <div style="height: 24px;width: 45px;background-color: #409EFF;border-radius:4px ; text-align: center" v-if="item.classtype === 1"><span style="color: white;font-size: 17px">新闻</span></div>
+                  <div style="height: 24px;width: 45px;background-color: #002350;border-radius:4px ; text-align: center" v-if="item.classtype === 2"><span style="color: white;font-size: 17px">行业</span></div>
+                </el-col>
+                <el-col :span="16.5">
+                  <label style="cursor: pointer;line-height: 24px;margin-left: 15px;font-size: 24px;word-break: break-all;text-overflow: ellipsis;overflow: hidden;display: -webkit-box;-webkit-line-clamp: 1;-webkit-box-orient: vertical;width: 700px">{{item.newsTitle}}</label>
+                </el-col>
+              </el-row>
+              <el-row style="text-align: left;padding-top: 85px" >
+                <label style="font-size: 12px;margin-left: 63px;color: #999999;line-height: 24px">{{item.gmtCreated | fmtDate('yyyy-MM-dd')}}</label>
+              </el-row>
+            </el-col>
+          </el-row>
+          <el-row style="margin-top: 0px;"><hr style="margin: 0px 50px 0px 50px ;color: #b3b0ac;height: 1px;border:none; border-top:1px solid #eeeeee;"></el-row>
+        </router-link>
+      </el-row>
+    </div>
+    <el-row style="height: 50px;margin-left: 50px;margin-right: 59px">
+      <el-col :span="1"><div style="width:50px;height: 50px;border-left: solid 1px #999999;opacity: 0.1;"></div></el-col>
+      <el-col :span="22"> <div style="padding-left:  424px;padding-top: 14px;width: 100px;  height: 16px;"><label style="  opacity: 0.5;letter-spacing: 0px;font-weight: normal;font-stretch: normal;font-size: 16px;line-height: 24px;color: #333333">加载中...</label></div></el-col>
+      <el-col :span="1"><div style="width:50px;height: 50px;border-right: solid 1px #999999;opacity: 0.1;"></div></el-col>
+    </el-row>
+
+  </white-board>
+</template>
+
+<script>
+  import newsApi from '@/api/newsApi'
+
+  export default {
+    name: 'NewIndex',
+    data () {
+      return {
+        searchForm: {
+          page: 1,
+          pageSize: 5,
+          searchValue: '',
+          newstype: 1,
+          type: 1
+        },
+        tableData: [],
+
+        wait: 1,
+        timer: ''
+      }
+    },
+
+    mounted () {
+      const that = this
+      this.timer = setInterval(function () {
+        that.wait = 0
+      }, 1000)
+      this.list()
+    },
+    beforeDestroy () {
+      if (this.timer) {
+        clearInterval(this.timer)
+      }
+    },
+    methods: {
+      list () {
+          newsApi.page(this.searchForm).then(data => {
+              if (data.list != null) {
+              for (var l of data.list) {
+                this.tableData.push(l)
+               }
+              }
+          })
+      },
+      loadMore () {
+        if (this.wait === 0) {
+          this.searchForm['page'] = this.searchForm['page'] + 1
+          this.list()
+          this.wait = 1
+        }
+      }
+      }
+  }
+</script>
+
+<style scoped>
+  /deep/ .ho div:hover {
+    color: #1989fa;
+    background-color: #eaf4fd;
+  }
+</style>

+ 15 - 0
csair-vr-portal-ui/src/views/redit.vue

@@ -0,0 +1,15 @@
+<template>
+  <div></div>
+</template>
+
+<script>
+  export default {
+    name: '',
+    mounted () {
+      this.$router.push({name: this.$route.params.path, query: { searchValue: this.$route.params.searchValue }})
+    }
+  }
+</script>
+
+<style scoped>
+</style>

+ 198 - 0
csair-vr-portal-ui/src/views/search.vue

@@ -0,0 +1,198 @@
+<template>
+  <white-board style="width: 1100px; margin: 0px auto;padding: 10px 0px 0px 0px !important;">
+
+    <el-breadcrumb separator-class="el-icon-arrow-right" style="line-height: 24px;text-align: left;padding: 49px 0px 0px 0px;margin-left: 50px;">
+      <el-breadcrumb-item :to="{ path: 'index' }" ><label style="line-height: 24px;;color: #2a92de;font-size: 14px;font-weight: 400;line-height: 24px;cursor: pointer;">首页</label></el-breadcrumb-item>
+      <el-breadcrumb-item><label style="line-height: 24px;;color: #2a92de;font-size: 14px;font-weight: 400;line-height: 24px;cursor: pointer;" @click="backToNews">新闻</label></el-breadcrumb-item>
+      <el-breadcrumb-item><label style="line-height: 24px;;color: #666666;font-size: 14px;font-weight: 400;line-height: 24px;">搜索</label></el-breadcrumb-item>
+    </el-breadcrumb>
+
+    <el-row style="text-align: left;padding: 20px 0px 10px 0px;margin-left: 50px">
+      <el-col v-for="(item,index) in optionData" :span="3" :key="item.id">
+        <el-button type="text" :id=" 'option'+ index" :key="index" style="width: 90px;height: 40px;font-size: 18px;color: #008ed3;padding-left: 7px;padding-right: 7px" @click="optionChange(index,item)" >
+          {{item}}
+        </el-button>
+      </el-col>
+    </el-row>
+    <el-row ><hr style="margin: 0px 50px 0px 50px ;color: #b3b0ac;height: 1px;border:none; border-top:1px solid #eeeeee;"></el-row>
+    <el-row style="padding: 25px 40px 10px 0px"><span style="color: #b3b0ac;padding-left: 50px;font-size: 14px">共找到</span><label style="color: #b3b0ac;font-size: 14px">{{this.totalCount}}</label><span style="color: #b3b0ac;font-size: 14px">条相关结果</span></el-row>
+    <div v-infinite-scroll="loadMore">
+      <el-row class="ho" style="height: 242px ;" v-for="(item, index) in tableData" :key="index" :ref="item.content">
+        <div @click="jumpToDetail(item)" style="line-height: 24px;cursor: pointer;">
+          <el-row style="padding-top: 18px">
+              <el-row style="margin-top: 10px;margin-left: 50px;" >
+                <el-col :span="1.5" v-if="item.classtype !== ''" style="margin-right: 14px">
+                  <div style="height: 24px;width: 45px;background-color: #409EFF;border-radius:4px ; text-align: center" v-if="item.classtype === '1'"><span style="color: white;font-size: 17px">新闻</span></div>
+                  <div style="height: 24px;width: 45px;background-color: #002350;border-radius:4px ; text-align: center" v-if="item.classtype === '2'"><span style="color: white;font-size: 17px">行业</span></div>
+                </el-col>
+                <el-col :span="16.5" >
+                 <!-- <label style="font-size: 24px;line-height: 24px;color: #002350;border-bottom: 1px solid black;cursor: pointer;">{{item.title}}</label>-->
+                  <label v-html="item.title" style="height: 25px;font-size: 24px;line-height: 24px;color: #002350;border-bottom: 1px solid black;"></label>
+                  <label v-if="item.classtype !== ''" style="width:86px;color: #999999;padding-left: 12px;line-height: 24px;width: 86px;height: 13px;font-family: MicrosoftYaHei;font-size: 16px;">{{item.gmtCreated | fmtDate('yyyy-MM-dd')}}</label>
+                </el-col>
+              </el-row>
+                <el-row style="margin-top: 13px">
+                  <el-col :span="6">
+                    <div class="el-image">
+                  <img :src="OSS_URL + item.imageUrl" style="width: 220px;height: 144px;margin-left: 50px;border-radius: 4px"/>
+                    </div>
+                  </el-col>
+                  <el-col  :span="18">
+                <div style="height: 120px">
+                  <div style="height: 100%;margin-right:50px;text-align: left;padding-left: 27px;width: 750px">
+<!--                <label style="letter-spacing: 0px; font-stretch: normal;font-weight: normal;font-family: MicrosoftYaHei;line-height: 30px;font-size: 14px ;display: -webkit-box;-webkit-box-orient: vertical;-webkit-line-clamp: 3;overflow: hidden;">{{item.intro}}</label>-->
+                    <div v-html="item.intro" style="letter-spacing: 0px; font-stretch: normal;font-weight: normal;font-family: MicrosoftYaHei;line-height: 30px;font-size: 14px ;display: -webkit-box;-webkit-box-orient: vertical;-webkit-line-clamp: 3;overflow: hidden;"></div>
+
+                  </div>
+                  <div style="height: 100%;margin-right:50px;text-align: left;padding-left: 27px;width: 750px;height: 30px" v-if="item.classtype === ''">
+                    <label  style="width: 86px;height: 13px;font-family: MicrosoftYaHei;font-size: 16px;font-weight: normal;font-stretch: normal;line-height: 24px;letter-spacing: 0px;color: #999999;">{{item.gmtCreated | fmtDate('yyyy-MM-dd')}}</label>
+                  </div>
+
+                </div>
+                  </el-col>
+              </el-row>
+            <el-row style="margin-top: 0px;"><hr style="margin: 15px 50px 0px 50px ;color: #b3b0ac;height: 1px;border:none; border-top:1px solid #eeeeee;"></el-row>
+
+          </el-row>
+        </div>
+
+      </el-row>
+    </div>
+    <el-row style="height: 50px;margin-left: 50px;margin-right: 59px;">
+      <el-col :span="1"><div style="width:50px;height: 50px;border-left: solid 1px #999999;opacity: 0.1;"></div></el-col>
+      <el-col :span="22"> <div style="padding-left:  424px;padding-top: 14px;width: 100px; height: 16px;"><label style="  opacity: 0.5;letter-spacing: 0px;font-weight: normal;font-stretch: normal;font-size:  16px;line-height: 24px;color: #333333">加载中...</label></div></el-col>
+      <el-col :span="1"><div style="width:50px;height: 50px;border-right: solid 1px #999999;opacity: 0.1;"></div></el-col>
+    </el-row>
+  </white-board>
+</template>
+
+<script>
+  import searchApi from '@/api/searchApi'
+
+  const Course = 'course'
+  const News = 'newspaper'
+  const contentTypeOptions = ['全部', '新闻资讯', '行业信息', 'VR全景图', '课程介绍']
+  const contentTypeOptionsMap = {
+    '全部': '0',
+    '新闻资讯': '1',
+    '行业信息': '2',
+    'VR全景图': '3',
+    '课程介绍': '4'
+  }
+
+  export default {
+    name: 'NewIndex',
+    data () {
+      return {
+        searchForm: {
+          crux: '',
+          type: '1',
+          page: 1,
+          pageSize: 5
+        },
+        tableData: [],
+        optionData: contentTypeOptions,
+        wait: 1,
+        timer: '',
+        temp: '',
+        onceForOption: 0,
+        totalCount: '',
+        updateTemp: 0
+      }
+    },
+    mounted () {
+      this.optionChange(0)
+      this.searchForm.crux = this.$route.query.searchValue
+      const that = this
+      this.timer = setInterval(function () {
+        that.wait = 0
+      }, 1000)
+      this.list()
+    },
+    updated () {
+      if (this.updateTemp === 0) {
+        var colorBack = document.getElementById('option' + '0')
+        colorBack.style.color = '#fff'
+        colorBack.style.backgroundColor = '#008ed3'
+        this.updateTemp = 1
+      }
+    },
+    beforeDestroy () {
+      if (this.timer) {
+        clearInterval(this.timer)
+      }
+    },
+    methods: {
+      backToNews () {
+      let value = document.getElementsByClassName('el-input__inner')
+        value[0].value = ''
+        this.$router.push({name: 'new'})
+      },
+      list () {
+        searchApi.page(this.searchForm).then(data => {
+          this.totalCount = data.totalCount
+          if (data.list != null) {
+            for (let l of data.list) {
+              if (this.searchForm.crux !== '' && this.searchForm.crux !== ' ') {
+                var result = '<label style="color: red;">' + this.searchForm.crux + '</label>'
+                var reg = new RegExp(this.searchForm.crux, 'g')
+                if (l.intro.indexOf(this.searchForm.crux) !== -1) {
+                  l.intro = l.intro.replace(reg, result)
+                }
+                if (l.title.indexOf(this.searchForm.crux) !== -1) {
+                  l.title = l.title.replace(reg, result)
+                }
+              }
+              this.tableData.push(l)
+            }
+          }
+        })
+      },
+      jumpToDetail (val) {
+        if (val.hardCode === News) {
+          this.$router.push({name: 'newDetail', params: { newId: val.id }})
+        }
+        if (val.hardCode === Course) {
+          this.$router.push({name: 'courseDetail', params: { courseId: val.id }})
+        }
+      },
+      loadMore () {
+        if (this.wait === 0) {
+          this.searchForm['page'] = this.searchForm['page'] + 1
+          this.list()
+          this.wait = 1
+        }
+      },
+      optionChange (index, value) {
+        if (index === 0) {
+          this.searchForm.type = ''
+        } else {
+          this.searchForm.type = contentTypeOptionsMap[value]
+        }
+        for (var i = 0; i < 5; i++) {
+          var color = document.getElementById('option' + i)
+          color.style.color = '#008ed3'
+          color.style.backgroundColor = '#fff'
+        }
+        var colorBack = document.getElementById('option' + index)
+        colorBack.style.color = '#fff'
+        colorBack.style.backgroundColor = '#008ed3'
+        if (this.onceForOption === 0) {
+          this.onceForOption = 1
+        } else {
+          this.tableData = []
+          this.searchForm.page = 1
+          this.list()
+        }
+      }
+    }
+  }
+</script>
+<style>
+</style>
+<style scoped>
+  /deep/ .ho div:hover {
+    color: #1989fa;
+    background-color: #eaf4fd;
+  }
+</style>

+ 146 - 0
csair-vr-portal-ui/src/views/vr/index.vue

@@ -0,0 +1,146 @@
+<template>
+  <white-board style="width: 1100px; margin: 0px auto;padding: 0px 0px 0px 0px !important;">
+    <el-row style="text-align: left;padding: 69px 0px 0px 50px;margin-bottom: 40px">
+      <span style="font-size: 30px; font-weight: bold; color: #008ed3;">VR全景图</span>
+    </el-row>
+    <el-row style="text-align: left;padding:0px 0px;height: 550px">
+      <el-carousel indicator-position="outside" style="overflow: hidden;">
+        <el-carousel-item v-for="(item, index) in this.tableData" :key="index" v-if="index < 3">
+            <el-image :src="OSS_URL + item.imageUrl" :alt="item.vrTitle" fit="fill" style="width: 100%;height: 500px;"></el-image>
+           <div style="margin-top: 12px"> <label  style="padding-left: 50px;padding-top: 11px;line-height: 24px;font-size: 24px;font-weight: bold;">{{item.vrTitle}}</label></div>
+        </el-carousel-item>
+      </el-carousel>
+    </el-row>
+    <div v-infinite-scroll="loadMore" style="background-color: #F5F5F5;padding-top: 20px;padding-bottom: 20px">
+      <el-row style="padding-top: 15px">
+        <div style="height: 20px;background-color: #F5F5F5"></div>
+      </el-row>
+      <el-row :gutter="22.5">
+        <el-col :span="8" v-for="(item, index) in tableData" :key="index" style="padding-top: 20px;">
+          <div @mouseover="qrcordChange(item)" @mouseleave="qrcordBack(item)">
+          <el-row class="ho" style="width: 352px;height: 176px;cursor: pointer;background-color: white">
+            <el-col :span="12" style="height: 176px">
+              <img :src="OSS_URL + item.imageUrl" style="width: 176px;height: 176px" v-if="item.temp === 0">
+              <img :src="OSS_URL + item.qrcodeUrl" style="width: 176px;height: 176px" v-if="item.temp === 1">
+            </el-col>
+            <el-col :span="12">
+              <el-row><label style="width :134px;line-height: 24px;padding-left: 22px;font-size: 16px;margin-top: 45px;font-weight: bold;word-break: break-all;text-overflow: ellipsis;overflow: hidden;display: -webkit-box;-webkit-line-clamp: 2;-webkit-box-orient: vertical">{{item.vrTitle}}</label></el-row>
+                <el-row><label style="opacity: 0.8;;width: 125px;height: 42px;line-height: 24px;padding-left: 22px;padding-right: 20px;font-size: 12px;margin-top: 20px;word-break: break-all;text-overflow: ellipsis;overflow: hidden;display: -webkit-box;-webkit-line-clamp: 2;-webkit-box-orient: vertical;">{{item.intro}}</label></el-row>
+            </el-col>
+          </el-row>
+          </div>
+        </el-col>
+      </el-row>
+    </div>
+    <el-row><div style="width: 1100px;border-top:1px solid #333333;opacity: 0.2;height: 1px"></div></el-row>
+    <div style="background-color: #F5F5F5">
+    <el-row style="height: 50px;margin-left: 50px;margin-right: 59px;background-color: #F5F5F5;">
+      <el-col :span="1"><div style="width:50px;height: 50px"></div></el-col>
+      <el-col :span="22"> <div style="padding-left:  424px;padding-top: 14px;width: 100px; height: 16px;">
+        <label style="  opacity: 0.5;letter-spacing: 0px;font-weight: normal;font-stretch: normal;font-size: 16px;line-height: 24px;color: #333333">加载中...</label></div></el-col>
+      <el-col :span="1"><div style="width:50px;height: 50px;"></div></el-col>
+    </el-row>
+    </div>
+  </white-board>
+</template>
+<script>
+  import vrpanoramicApi from '@/api/vrpanoramicApi'
+
+  export default {
+    name: 'vrIndex',
+    data () {
+      return {
+        searchForm: {
+          page: 1,
+          pageSize: 5,
+          type: '3'
+        },
+        tableData: [],
+        wait: '',
+        timer: ''
+      }
+    },
+    mounted () {
+      const that = this
+      this.timer = setInterval(function () {
+        that.wait = 0
+      }, 1000)
+
+      this.list()
+    },
+    beforeDestroy () {
+      if (this.timer) {
+        clearInterval(this.timer)
+      }
+    },
+    methods: {
+      list () {
+          vrpanoramicApi.page(this.searchForm).then(data => {
+            if (data.list != null) {
+              data.list.map(item => {
+                item.temp = 0
+              })
+              for (var l of data.list) {
+                this.tableData.push(l)
+              }
+            }
+          })
+      },
+      loadMore () {
+        if (this.wait === 0) {
+          this.searchForm.page = this.searchForm.page + 1
+          this.list()
+          this.wait = 1
+        }
+      },
+      qrcordChange (row) {
+        row.temp = 1
+      },
+      qrcordBack (row) {
+        row.temp = 0
+      }
+    }
+  }
+</script>
+
+<style>
+/*  .el-carousel__container {
+    height: 550px;
+  }
+  .el-carousel__indicators--outside{
+    background-color: #eeeeee;
+    padding-bottom: 2px
+  }*/
+</style>
+<style scoped>
+  /deep/ .ho:hover {
+    color: #ffffff !important;
+    background-color: #1989fa !important;
+  }
+  /deep/ .el-carousel__container {
+    height: 550px;
+  }
+  /deep/ .el-carousel__indicators--outside{
+    background-color: #eeeeee;
+    padding-bottom: 2px;
+  }
+  /deep/ .el-carousel__indicators--outside button {
+    opacity: 1;
+  }
+  /deep/ .el-carousel__button {
+    width: 12px;
+    height: 12px;
+    border-radius: 100px;
+    box-shadow: inset 0px 1px 1px 0px
+    rgba(0, 0, 0, 0.2);
+    color: #d2d2d2;
+  }
+  /deep/ .el-carousel__indicators.el-carousel__indicators--horizontal {
+    padding-bottom: 0px
+  }
+  /deep/ .el-carousel__indicator.is-active button {
+    background-color: #e20026;
+    box-shadow: inset 0px 1px 1px 0px
+    rgba(0, 0, 0, 0.6);
+  }
+</style>

+ 0 - 0
csair-vr-portal-ui/static/.gitkeep


+ 952 - 0
csair-vr-portal-ui/static/css/quill/quill.bubble.css

@@ -0,0 +1,952 @@
+/*!
+ * Quill Editor v1.3.6
+ * https://quilljs.com/
+ * Copyright (c) 2014, Jason Chen
+ * Copyright (c) 2013, salesforce.com
+ */
+.ql-container {
+  box-sizing: border-box;
+  font-family: Helvetica, Arial, sans-serif;
+  font-size: 13px;
+  height: 100%;
+  margin: 0px;
+  position: relative;
+}
+.ql-container.ql-disabled .ql-tooltip {
+  visibility: hidden;
+}
+.ql-container.ql-disabled .ql-editor ul[data-checked] > li::before {
+  pointer-events: none;
+}
+.ql-clipboard {
+  left: -100000px;
+  height: 1px;
+  overflow-y: hidden;
+  position: absolute;
+  top: 50%;
+}
+.ql-clipboard p {
+  margin: 0;
+  padding: 0;
+}
+.ql-editor {
+  box-sizing: border-box;
+  line-height: 1.42;
+  height: 100%;
+  outline: none;
+  overflow-y: auto;
+  padding: 12px 15px;
+  tab-size: 4;
+  -moz-tab-size: 4;
+  text-align: left;
+  white-space: pre-wrap;
+  word-wrap: break-word;
+}
+.ql-editor > * {
+  cursor: text;
+}
+.ql-editor p,
+.ql-editor ol,
+.ql-editor ul,
+.ql-editor pre,
+.ql-editor blockquote,
+.ql-editor h1,
+.ql-editor h2,
+.ql-editor h3,
+.ql-editor h4,
+.ql-editor h5,
+.ql-editor h6 {
+  margin: 0;
+  padding: 0;
+  counter-reset: list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;
+}
+.ql-editor ol,
+.ql-editor ul {
+  padding-left: 1.5em;
+}
+.ql-editor ol > li,
+.ql-editor ul > li {
+  list-style-type: none;
+}
+.ql-editor ul > li::before {
+  content: '\2022';
+}
+.ql-editor ul[data-checked=true],
+.ql-editor ul[data-checked=false] {
+  pointer-events: none;
+}
+.ql-editor ul[data-checked=true] > li *,
+.ql-editor ul[data-checked=false] > li * {
+  pointer-events: all;
+}
+.ql-editor ul[data-checked=true] > li::before,
+.ql-editor ul[data-checked=false] > li::before {
+  color: #777;
+  cursor: pointer;
+  pointer-events: all;
+}
+.ql-editor ul[data-checked=true] > li::before {
+  content: '\2611';
+}
+.ql-editor ul[data-checked=false] > li::before {
+  content: '\2610';
+}
+.ql-editor li::before {
+  display: inline-block;
+  white-space: nowrap;
+  width: 1.2em;
+}
+.ql-editor li:not(.ql-direction-rtl)::before {
+  margin-left: -1.5em;
+  margin-right: 0.3em;
+  text-align: right;
+}
+.ql-editor li.ql-direction-rtl::before {
+  margin-left: 0.3em;
+  margin-right: -1.5em;
+}
+.ql-editor ol li:not(.ql-direction-rtl),
+.ql-editor ul li:not(.ql-direction-rtl) {
+  padding-left: 1.5em;
+}
+.ql-editor ol li.ql-direction-rtl,
+.ql-editor ul li.ql-direction-rtl {
+  padding-right: 1.5em;
+}
+.ql-editor ol li {
+  counter-reset: list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;
+  counter-increment: list-0;
+}
+.ql-editor ol li:before {
+  content: counter(list-0, decimal) '. ';
+}
+.ql-editor ol li.ql-indent-1 {
+  counter-increment: list-1;
+}
+.ql-editor ol li.ql-indent-1:before {
+  content: counter(list-1, lower-alpha) '. ';
+}
+.ql-editor ol li.ql-indent-1 {
+  counter-reset: list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;
+}
+.ql-editor ol li.ql-indent-2 {
+  counter-increment: list-2;
+}
+.ql-editor ol li.ql-indent-2:before {
+  content: counter(list-2, lower-roman) '. ';
+}
+.ql-editor ol li.ql-indent-2 {
+  counter-reset: list-3 list-4 list-5 list-6 list-7 list-8 list-9;
+}
+.ql-editor ol li.ql-indent-3 {
+  counter-increment: list-3;
+}
+.ql-editor ol li.ql-indent-3:before {
+  content: counter(list-3, decimal) '. ';
+}
+.ql-editor ol li.ql-indent-3 {
+  counter-reset: list-4 list-5 list-6 list-7 list-8 list-9;
+}
+.ql-editor ol li.ql-indent-4 {
+  counter-increment: list-4;
+}
+.ql-editor ol li.ql-indent-4:before {
+  content: counter(list-4, lower-alpha) '. ';
+}
+.ql-editor ol li.ql-indent-4 {
+  counter-reset: list-5 list-6 list-7 list-8 list-9;
+}
+.ql-editor ol li.ql-indent-5 {
+  counter-increment: list-5;
+}
+.ql-editor ol li.ql-indent-5:before {
+  content: counter(list-5, lower-roman) '. ';
+}
+.ql-editor ol li.ql-indent-5 {
+  counter-reset: list-6 list-7 list-8 list-9;
+}
+.ql-editor ol li.ql-indent-6 {
+  counter-increment: list-6;
+}
+.ql-editor ol li.ql-indent-6:before {
+  content: counter(list-6, decimal) '. ';
+}
+.ql-editor ol li.ql-indent-6 {
+  counter-reset: list-7 list-8 list-9;
+}
+.ql-editor ol li.ql-indent-7 {
+  counter-increment: list-7;
+}
+.ql-editor ol li.ql-indent-7:before {
+  content: counter(list-7, lower-alpha) '. ';
+}
+.ql-editor ol li.ql-indent-7 {
+  counter-reset: list-8 list-9;
+}
+.ql-editor ol li.ql-indent-8 {
+  counter-increment: list-8;
+}
+.ql-editor ol li.ql-indent-8:before {
+  content: counter(list-8, lower-roman) '. ';
+}
+.ql-editor ol li.ql-indent-8 {
+  counter-reset: list-9;
+}
+.ql-editor ol li.ql-indent-9 {
+  counter-increment: list-9;
+}
+.ql-editor ol li.ql-indent-9:before {
+  content: counter(list-9, decimal) '. ';
+}
+.ql-editor .ql-indent-1:not(.ql-direction-rtl) {
+  padding-left: 3em;
+}
+.ql-editor li.ql-indent-1:not(.ql-direction-rtl) {
+  padding-left: 4.5em;
+}
+.ql-editor .ql-indent-1.ql-direction-rtl.ql-align-right {
+  padding-right: 3em;
+}
+.ql-editor li.ql-indent-1.ql-direction-rtl.ql-align-right {
+  padding-right: 4.5em;
+}
+.ql-editor .ql-indent-2:not(.ql-direction-rtl) {
+  padding-left: 6em;
+}
+.ql-editor li.ql-indent-2:not(.ql-direction-rtl) {
+  padding-left: 7.5em;
+}
+.ql-editor .ql-indent-2.ql-direction-rtl.ql-align-right {
+  padding-right: 6em;
+}
+.ql-editor li.ql-indent-2.ql-direction-rtl.ql-align-right {
+  padding-right: 7.5em;
+}
+.ql-editor .ql-indent-3:not(.ql-direction-rtl) {
+  padding-left: 9em;
+}
+.ql-editor li.ql-indent-3:not(.ql-direction-rtl) {
+  padding-left: 10.5em;
+}
+.ql-editor .ql-indent-3.ql-direction-rtl.ql-align-right {
+  padding-right: 9em;
+}
+.ql-editor li.ql-indent-3.ql-direction-rtl.ql-align-right {
+  padding-right: 10.5em;
+}
+.ql-editor .ql-indent-4:not(.ql-direction-rtl) {
+  padding-left: 12em;
+}
+.ql-editor li.ql-indent-4:not(.ql-direction-rtl) {
+  padding-left: 13.5em;
+}
+.ql-editor .ql-indent-4.ql-direction-rtl.ql-align-right {
+  padding-right: 12em;
+}
+.ql-editor li.ql-indent-4.ql-direction-rtl.ql-align-right {
+  padding-right: 13.5em;
+}
+.ql-editor .ql-indent-5:not(.ql-direction-rtl) {
+  padding-left: 15em;
+}
+.ql-editor li.ql-indent-5:not(.ql-direction-rtl) {
+  padding-left: 16.5em;
+}
+.ql-editor .ql-indent-5.ql-direction-rtl.ql-align-right {
+  padding-right: 15em;
+}
+.ql-editor li.ql-indent-5.ql-direction-rtl.ql-align-right {
+  padding-right: 16.5em;
+}
+.ql-editor .ql-indent-6:not(.ql-direction-rtl) {
+  padding-left: 18em;
+}
+.ql-editor li.ql-indent-6:not(.ql-direction-rtl) {
+  padding-left: 19.5em;
+}
+.ql-editor .ql-indent-6.ql-direction-rtl.ql-align-right {
+  padding-right: 18em;
+}
+.ql-editor li.ql-indent-6.ql-direction-rtl.ql-align-right {
+  padding-right: 19.5em;
+}
+.ql-editor .ql-indent-7:not(.ql-direction-rtl) {
+  padding-left: 21em;
+}
+.ql-editor li.ql-indent-7:not(.ql-direction-rtl) {
+  padding-left: 22.5em;
+}
+.ql-editor .ql-indent-7.ql-direction-rtl.ql-align-right {
+  padding-right: 21em;
+}
+.ql-editor li.ql-indent-7.ql-direction-rtl.ql-align-right {
+  padding-right: 22.5em;
+}
+.ql-editor .ql-indent-8:not(.ql-direction-rtl) {
+  padding-left: 24em;
+}
+.ql-editor li.ql-indent-8:not(.ql-direction-rtl) {
+  padding-left: 25.5em;
+}
+.ql-editor .ql-indent-8.ql-direction-rtl.ql-align-right {
+  padding-right: 24em;
+}
+.ql-editor li.ql-indent-8.ql-direction-rtl.ql-align-right {
+  padding-right: 25.5em;
+}
+.ql-editor .ql-indent-9:not(.ql-direction-rtl) {
+  padding-left: 27em;
+}
+.ql-editor li.ql-indent-9:not(.ql-direction-rtl) {
+  padding-left: 28.5em;
+}
+.ql-editor .ql-indent-9.ql-direction-rtl.ql-align-right {
+  padding-right: 27em;
+}
+.ql-editor li.ql-indent-9.ql-direction-rtl.ql-align-right {
+  padding-right: 28.5em;
+}
+.ql-editor .ql-video {
+  display: block;
+  max-width: 100%;
+}
+.ql-editor .ql-video.ql-align-center {
+  margin: 0 auto;
+}
+.ql-editor .ql-video.ql-align-right {
+  margin: 0 0 0 auto;
+}
+.ql-editor .ql-bg-black {
+  background-color: #000;
+}
+.ql-editor .ql-bg-red {
+  background-color: #e60000;
+}
+.ql-editor .ql-bg-orange {
+  background-color: #f90;
+}
+.ql-editor .ql-bg-yellow {
+  background-color: #ff0;
+}
+.ql-editor .ql-bg-green {
+  background-color: #008a00;
+}
+.ql-editor .ql-bg-blue {
+  background-color: #06c;
+}
+.ql-editor .ql-bg-purple {
+  background-color: #93f;
+}
+.ql-editor .ql-color-white {
+  color: #fff;
+}
+.ql-editor .ql-color-red {
+  color: #e60000;
+}
+.ql-editor .ql-color-orange {
+  color: #f90;
+}
+.ql-editor .ql-color-yellow {
+  color: #ff0;
+}
+.ql-editor .ql-color-green {
+  color: #008a00;
+}
+.ql-editor .ql-color-blue {
+  color: #06c;
+}
+.ql-editor .ql-color-purple {
+  color: #93f;
+}
+.ql-editor .ql-font-serif {
+  font-family: Georgia, Times New Roman, serif;
+}
+.ql-editor .ql-font-monospace {
+  font-family: Monaco, Courier New, monospace;
+}
+.ql-editor .ql-size-small {
+  font-size: 0.75em;
+}
+.ql-editor .ql-size-large {
+  font-size: 1.5em;
+}
+.ql-editor .ql-size-huge {
+  font-size: 2.5em;
+}
+.ql-editor .ql-direction-rtl {
+  direction: rtl;
+  text-align: inherit;
+}
+.ql-editor .ql-align-center {
+  text-align: center;
+}
+.ql-editor .ql-align-justify {
+  text-align: justify;
+}
+.ql-editor .ql-align-right {
+  text-align: right;
+}
+.ql-editor.ql-blank::before {
+  color: rgba(0,0,0,0.6);
+  content: attr(data-placeholder);
+  font-style: italic;
+  left: 15px;
+  pointer-events: none;
+  position: absolute;
+  right: 15px;
+}
+.ql-bubble.ql-toolbar:after,
+.ql-bubble .ql-toolbar:after {
+  clear: both;
+  content: '';
+  display: table;
+}
+.ql-bubble.ql-toolbar button,
+.ql-bubble .ql-toolbar button {
+  background: none;
+  border: none;
+  cursor: pointer;
+  display: inline-block;
+  float: left;
+  height: 24px;
+  padding: 3px 5px;
+  width: 28px;
+}
+.ql-bubble.ql-toolbar button svg,
+.ql-bubble .ql-toolbar button svg {
+  float: left;
+  height: 100%;
+}
+.ql-bubble.ql-toolbar button:active:hover,
+.ql-bubble .ql-toolbar button:active:hover {
+  outline: none;
+}
+.ql-bubble.ql-toolbar input.ql-image[type=file],
+.ql-bubble .ql-toolbar input.ql-image[type=file] {
+  display: none;
+}
+.ql-bubble.ql-toolbar button:hover,
+.ql-bubble .ql-toolbar button:hover,
+.ql-bubble.ql-toolbar button:focus,
+.ql-bubble .ql-toolbar button:focus,
+.ql-bubble.ql-toolbar button.ql-active,
+.ql-bubble .ql-toolbar button.ql-active,
+.ql-bubble.ql-toolbar .ql-picker-label:hover,
+.ql-bubble .ql-toolbar .ql-picker-label:hover,
+.ql-bubble.ql-toolbar .ql-picker-label.ql-active,
+.ql-bubble .ql-toolbar .ql-picker-label.ql-active,
+.ql-bubble.ql-toolbar .ql-picker-item:hover,
+.ql-bubble .ql-toolbar .ql-picker-item:hover,
+.ql-bubble.ql-toolbar .ql-picker-item.ql-selected,
+.ql-bubble .ql-toolbar .ql-picker-item.ql-selected {
+  color: #fff;
+}
+.ql-bubble.ql-toolbar button:hover .ql-fill,
+.ql-bubble .ql-toolbar button:hover .ql-fill,
+.ql-bubble.ql-toolbar button:focus .ql-fill,
+.ql-bubble .ql-toolbar button:focus .ql-fill,
+.ql-bubble.ql-toolbar button.ql-active .ql-fill,
+.ql-bubble .ql-toolbar button.ql-active .ql-fill,
+.ql-bubble.ql-toolbar .ql-picker-label:hover .ql-fill,
+.ql-bubble .ql-toolbar .ql-picker-label:hover .ql-fill,
+.ql-bubble.ql-toolbar .ql-picker-label.ql-active .ql-fill,
+.ql-bubble .ql-toolbar .ql-picker-label.ql-active .ql-fill,
+.ql-bubble.ql-toolbar .ql-picker-item:hover .ql-fill,
+.ql-bubble .ql-toolbar .ql-picker-item:hover .ql-fill,
+.ql-bubble.ql-toolbar .ql-picker-item.ql-selected .ql-fill,
+.ql-bubble .ql-toolbar .ql-picker-item.ql-selected .ql-fill,
+.ql-bubble.ql-toolbar button:hover .ql-stroke.ql-fill,
+.ql-bubble .ql-toolbar button:hover .ql-stroke.ql-fill,
+.ql-bubble.ql-toolbar button:focus .ql-stroke.ql-fill,
+.ql-bubble .ql-toolbar button:focus .ql-stroke.ql-fill,
+.ql-bubble.ql-toolbar button.ql-active .ql-stroke.ql-fill,
+.ql-bubble .ql-toolbar button.ql-active .ql-stroke.ql-fill,
+.ql-bubble.ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill,
+.ql-bubble .ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill,
+.ql-bubble.ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill,
+.ql-bubble .ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill,
+.ql-bubble.ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill,
+.ql-bubble .ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill,
+.ql-bubble.ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill,
+.ql-bubble .ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill {
+  fill: #fff;
+}
+.ql-bubble.ql-toolbar button:hover .ql-stroke,
+.ql-bubble .ql-toolbar button:hover .ql-stroke,
+.ql-bubble.ql-toolbar button:focus .ql-stroke,
+.ql-bubble .ql-toolbar button:focus .ql-stroke,
+.ql-bubble.ql-toolbar button.ql-active .ql-stroke,
+.ql-bubble .ql-toolbar button.ql-active .ql-stroke,
+.ql-bubble.ql-toolbar .ql-picker-label:hover .ql-stroke,
+.ql-bubble .ql-toolbar .ql-picker-label:hover .ql-stroke,
+.ql-bubble.ql-toolbar .ql-picker-label.ql-active .ql-stroke,
+.ql-bubble .ql-toolbar .ql-picker-label.ql-active .ql-stroke,
+.ql-bubble.ql-toolbar .ql-picker-item:hover .ql-stroke,
+.ql-bubble .ql-toolbar .ql-picker-item:hover .ql-stroke,
+.ql-bubble.ql-toolbar .ql-picker-item.ql-selected .ql-stroke,
+.ql-bubble .ql-toolbar .ql-picker-item.ql-selected .ql-stroke,
+.ql-bubble.ql-toolbar button:hover .ql-stroke-miter,
+.ql-bubble .ql-toolbar button:hover .ql-stroke-miter,
+.ql-bubble.ql-toolbar button:focus .ql-stroke-miter,
+.ql-bubble .ql-toolbar button:focus .ql-stroke-miter,
+.ql-bubble.ql-toolbar button.ql-active .ql-stroke-miter,
+.ql-bubble .ql-toolbar button.ql-active .ql-stroke-miter,
+.ql-bubble.ql-toolbar .ql-picker-label:hover .ql-stroke-miter,
+.ql-bubble .ql-toolbar .ql-picker-label:hover .ql-stroke-miter,
+.ql-bubble.ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter,
+.ql-bubble .ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter,
+.ql-bubble.ql-toolbar .ql-picker-item:hover .ql-stroke-miter,
+.ql-bubble .ql-toolbar .ql-picker-item:hover .ql-stroke-miter,
+.ql-bubble.ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter,
+.ql-bubble .ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter {
+  stroke: #fff;
+}
+@media (pointer: coarse) {
+  .ql-bubble.ql-toolbar button:hover:not(.ql-active),
+  .ql-bubble .ql-toolbar button:hover:not(.ql-active) {
+    color: #ccc;
+  }
+  .ql-bubble.ql-toolbar button:hover:not(.ql-active) .ql-fill,
+  .ql-bubble .ql-toolbar button:hover:not(.ql-active) .ql-fill,
+  .ql-bubble.ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill,
+  .ql-bubble .ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill {
+    fill: #ccc;
+  }
+  .ql-bubble.ql-toolbar button:hover:not(.ql-active) .ql-stroke,
+  .ql-bubble .ql-toolbar button:hover:not(.ql-active) .ql-stroke,
+  .ql-bubble.ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter,
+  .ql-bubble .ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter {
+    stroke: #ccc;
+  }
+}
+.ql-bubble {
+  box-sizing: border-box;
+}
+.ql-bubble * {
+  box-sizing: border-box;
+}
+.ql-bubble .ql-hidden {
+  display: none;
+}
+.ql-bubble .ql-out-bottom,
+.ql-bubble .ql-out-top {
+  visibility: hidden;
+}
+.ql-bubble .ql-tooltip {
+  position: absolute;
+  transform: translateY(10px);
+}
+.ql-bubble .ql-tooltip a {
+  cursor: pointer;
+  text-decoration: none;
+}
+.ql-bubble .ql-tooltip.ql-flip {
+  transform: translateY(-10px);
+}
+.ql-bubble .ql-formats {
+  display: inline-block;
+  vertical-align: middle;
+}
+.ql-bubble .ql-formats:after {
+  clear: both;
+  content: '';
+  display: table;
+}
+.ql-bubble .ql-stroke {
+  fill: none;
+  stroke: #ccc;
+  stroke-linecap: round;
+  stroke-linejoin: round;
+  stroke-width: 2;
+}
+.ql-bubble .ql-stroke-miter {
+  fill: none;
+  stroke: #ccc;
+  stroke-miterlimit: 10;
+  stroke-width: 2;
+}
+.ql-bubble .ql-fill,
+.ql-bubble .ql-stroke.ql-fill {
+  fill: #ccc;
+}
+.ql-bubble .ql-empty {
+  fill: none;
+}
+.ql-bubble .ql-even {
+  fill-rule: evenodd;
+}
+.ql-bubble .ql-thin,
+.ql-bubble .ql-stroke.ql-thin {
+  stroke-width: 1;
+}
+.ql-bubble .ql-transparent {
+  opacity: 0.4;
+}
+.ql-bubble .ql-direction svg:last-child {
+  display: none;
+}
+.ql-bubble .ql-direction.ql-active svg:last-child {
+  display: inline;
+}
+.ql-bubble .ql-direction.ql-active svg:first-child {
+  display: none;
+}
+.ql-bubble .ql-editor h1 {
+  font-size: 2em;
+}
+.ql-bubble .ql-editor h2 {
+  font-size: 1.5em;
+}
+.ql-bubble .ql-editor h3 {
+  font-size: 1.17em;
+}
+.ql-bubble .ql-editor h4 {
+  font-size: 1em;
+}
+.ql-bubble .ql-editor h5 {
+  font-size: 0.83em;
+}
+.ql-bubble .ql-editor h6 {
+  font-size: 0.67em;
+}
+.ql-bubble .ql-editor a {
+  text-decoration: underline;
+}
+.ql-bubble .ql-editor blockquote {
+  border-left: 4px solid #ccc;
+  margin-bottom: 5px;
+  margin-top: 5px;
+  padding-left: 16px;
+}
+.ql-bubble .ql-editor code,
+.ql-bubble .ql-editor pre {
+  background-color: #f0f0f0;
+  border-radius: 3px;
+}
+.ql-bubble .ql-editor pre {
+  white-space: pre-wrap;
+  margin-bottom: 5px;
+  margin-top: 5px;
+  padding: 5px 10px;
+}
+.ql-bubble .ql-editor code {
+  font-size: 85%;
+  padding: 2px 4px;
+}
+.ql-bubble .ql-editor pre.ql-syntax {
+  background-color: #23241f;
+  color: #f8f8f2;
+  overflow: visible;
+}
+.ql-bubble .ql-editor img {
+  max-width: 100%;
+}
+.ql-bubble .ql-picker {
+  color: #ccc;
+  display: inline-block;
+  float: left;
+  font-size: 14px;
+  font-weight: 500;
+  height: 24px;
+  position: relative;
+  vertical-align: middle;
+}
+.ql-bubble .ql-picker-label {
+  cursor: pointer;
+  display: inline-block;
+  height: 100%;
+  padding-left: 8px;
+  padding-right: 2px;
+  position: relative;
+  width: 100%;
+}
+.ql-bubble .ql-picker-label::before {
+  display: inline-block;
+  line-height: 22px;
+}
+.ql-bubble .ql-picker-options {
+  background-color: #444;
+  display: none;
+  min-width: 100%;
+  padding: 4px 8px;
+  position: absolute;
+  white-space: nowrap;
+}
+.ql-bubble .ql-picker-options .ql-picker-item {
+  cursor: pointer;
+  display: block;
+  padding-bottom: 5px;
+  padding-top: 5px;
+}
+.ql-bubble .ql-picker.ql-expanded .ql-picker-label {
+  color: #777;
+  z-index: 2;
+}
+.ql-bubble .ql-picker.ql-expanded .ql-picker-label .ql-fill {
+  fill: #777;
+}
+.ql-bubble .ql-picker.ql-expanded .ql-picker-label .ql-stroke {
+  stroke: #777;
+}
+.ql-bubble .ql-picker.ql-expanded .ql-picker-options {
+  display: block;
+  margin-top: -1px;
+  top: 100%;
+  z-index: 1;
+}
+.ql-bubble .ql-color-picker,
+.ql-bubble .ql-icon-picker {
+  width: 28px;
+}
+.ql-bubble .ql-color-picker .ql-picker-label,
+.ql-bubble .ql-icon-picker .ql-picker-label {
+  padding: 2px 4px;
+}
+.ql-bubble .ql-color-picker .ql-picker-label svg,
+.ql-bubble .ql-icon-picker .ql-picker-label svg {
+  right: 4px;
+}
+.ql-bubble .ql-icon-picker .ql-picker-options {
+  padding: 4px 0px;
+}
+.ql-bubble .ql-icon-picker .ql-picker-item {
+  height: 24px;
+  width: 24px;
+  padding: 2px 4px;
+}
+.ql-bubble .ql-color-picker .ql-picker-options {
+  padding: 3px 5px;
+  width: 152px;
+}
+.ql-bubble .ql-color-picker .ql-picker-item {
+  border: 1px solid transparent;
+  float: left;
+  height: 16px;
+  margin: 2px;
+  padding: 0px;
+  width: 16px;
+}
+.ql-bubble .ql-picker:not(.ql-color-picker):not(.ql-icon-picker) svg {
+  position: absolute;
+  margin-top: -9px;
+  right: 0;
+  top: 50%;
+  width: 18px;
+}
+.ql-bubble .ql-picker.ql-header .ql-picker-label[data-label]:not([data-label=''])::before,
+.ql-bubble .ql-picker.ql-font .ql-picker-label[data-label]:not([data-label=''])::before,
+.ql-bubble .ql-picker.ql-size .ql-picker-label[data-label]:not([data-label=''])::before,
+.ql-bubble .ql-picker.ql-header .ql-picker-item[data-label]:not([data-label=''])::before,
+.ql-bubble .ql-picker.ql-font .ql-picker-item[data-label]:not([data-label=''])::before,
+.ql-bubble .ql-picker.ql-size .ql-picker-item[data-label]:not([data-label=''])::before {
+  content: attr(data-label);
+}
+.ql-bubble .ql-picker.ql-header {
+  width: 98px;
+}
+.ql-bubble .ql-picker.ql-header .ql-picker-label::before,
+.ql-bubble .ql-picker.ql-header .ql-picker-item::before {
+  content: 'Normal';
+}
+.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="1"]::before,
+.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="1"]::before {
+  content: 'Heading 1';
+}
+.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="2"]::before,
+.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="2"]::before {
+  content: 'Heading 2';
+}
+.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="3"]::before,
+.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="3"]::before {
+  content: 'Heading 3';
+}
+.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="4"]::before,
+.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="4"]::before {
+  content: 'Heading 4';
+}
+.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="5"]::before,
+.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="5"]::before {
+  content: 'Heading 5';
+}
+.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="6"]::before,
+.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="6"]::before {
+  content: 'Heading 6';
+}
+.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="1"]::before {
+  font-size: 2em;
+}
+.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="2"]::before {
+  font-size: 1.5em;
+}
+.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="3"]::before {
+  font-size: 1.17em;
+}
+.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="4"]::before {
+  font-size: 1em;
+}
+.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="5"]::before {
+  font-size: 0.83em;
+}
+.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="6"]::before {
+  font-size: 0.67em;
+}
+.ql-bubble .ql-picker.ql-font {
+  width: 108px;
+}
+.ql-bubble .ql-picker.ql-font .ql-picker-label::before,
+.ql-bubble .ql-picker.ql-font .ql-picker-item::before {
+  content: 'Sans Serif';
+}
+.ql-bubble .ql-picker.ql-font .ql-picker-label[data-value=serif]::before,
+.ql-bubble .ql-picker.ql-font .ql-picker-item[data-value=serif]::before {
+  content: 'Serif';
+}
+.ql-bubble .ql-picker.ql-font .ql-picker-label[data-value=monospace]::before,
+.ql-bubble .ql-picker.ql-font .ql-picker-item[data-value=monospace]::before {
+  content: 'Monospace';
+}
+.ql-bubble .ql-picker.ql-font .ql-picker-item[data-value=serif]::before {
+  font-family: Georgia, Times New Roman, serif;
+}
+.ql-bubble .ql-picker.ql-font .ql-picker-item[data-value=monospace]::before {
+  font-family: Monaco, Courier New, monospace;
+}
+.ql-bubble .ql-picker.ql-size {
+  width: 98px;
+}
+.ql-bubble .ql-picker.ql-size .ql-picker-label::before,
+.ql-bubble .ql-picker.ql-size .ql-picker-item::before {
+  content: 'Normal';
+}
+.ql-bubble .ql-picker.ql-size .ql-picker-label[data-value=small]::before,
+.ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=small]::before {
+  content: 'Small';
+}
+.ql-bubble .ql-picker.ql-size .ql-picker-label[data-value=large]::before,
+.ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=large]::before {
+  content: 'Large';
+}
+.ql-bubble .ql-picker.ql-size .ql-picker-label[data-value=huge]::before,
+.ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=huge]::before {
+  content: 'Huge';
+}
+.ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=small]::before {
+  font-size: 10px;
+}
+.ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=large]::before {
+  font-size: 18px;
+}
+.ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=huge]::before {
+  font-size: 32px;
+}
+.ql-bubble .ql-color-picker.ql-background .ql-picker-item {
+  background-color: #fff;
+}
+.ql-bubble .ql-color-picker.ql-color .ql-picker-item {
+  background-color: #000;
+}
+.ql-bubble .ql-toolbar .ql-formats {
+  margin: 8px 12px 8px 0px;
+}
+.ql-bubble .ql-toolbar .ql-formats:first-child {
+  margin-left: 12px;
+}
+.ql-bubble .ql-color-picker svg {
+  margin: 1px;
+}
+.ql-bubble .ql-color-picker .ql-picker-item.ql-selected,
+.ql-bubble .ql-color-picker .ql-picker-item:hover {
+  border-color: #fff;
+}
+.ql-bubble .ql-tooltip {
+  background-color: #444;
+  border-radius: 25px;
+  color: #fff;
+}
+.ql-bubble .ql-tooltip-arrow {
+  border-left: 6px solid transparent;
+  border-right: 6px solid transparent;
+  content: " ";
+  display: block;
+  left: 50%;
+  margin-left: -6px;
+  position: absolute;
+}
+.ql-bubble .ql-tooltip:not(.ql-flip) .ql-tooltip-arrow {
+  border-bottom: 6px solid #444;
+  top: -6px;
+}
+.ql-bubble .ql-tooltip.ql-flip .ql-tooltip-arrow {
+  border-top: 6px solid #444;
+  bottom: -6px;
+}
+.ql-bubble .ql-tooltip.ql-editing .ql-tooltip-editor {
+  display: block;
+}
+.ql-bubble .ql-tooltip.ql-editing .ql-formats {
+  visibility: hidden;
+}
+.ql-bubble .ql-tooltip-editor {
+  display: none;
+}
+.ql-bubble .ql-tooltip-editor input[type=text] {
+  background: transparent;
+  border: none;
+  color: #fff;
+  font-size: 13px;
+  height: 100%;
+  outline: none;
+  padding: 10px 20px;
+  position: absolute;
+  width: 100%;
+}
+.ql-bubble .ql-tooltip-editor a {
+  top: 10px;
+  position: absolute;
+  right: 20px;
+}
+.ql-bubble .ql-tooltip-editor a:before {
+  color: #ccc;
+  content: "\D7";
+  font-size: 16px;
+  font-weight: bold;
+}
+.ql-container.ql-bubble:not(.ql-disabled) a {
+  position: relative;
+  white-space: nowrap;
+}
+.ql-container.ql-bubble:not(.ql-disabled) a::before {
+  background-color: #444;
+  border-radius: 15px;
+  top: -5px;
+  font-size: 12px;
+  color: #fff;
+  content: attr(href);
+  font-weight: normal;
+  overflow: hidden;
+  padding: 5px 15px;
+  text-decoration: none;
+  z-index: 1;
+}
+.ql-container.ql-bubble:not(.ql-disabled) a::after {
+  border-top: 6px solid #444;
+  border-left: 6px solid transparent;
+  border-right: 6px solid transparent;
+  top: 0;
+  content: " ";
+  height: 0;
+  width: 0;
+}
+.ql-container.ql-bubble:not(.ql-disabled) a::before,
+.ql-container.ql-bubble:not(.ql-disabled) a::after {
+  left: 0;
+  margin-left: 50%;
+  position: absolute;
+  transform: translate(-50%, -100%);
+  transition: visibility 0s ease 200ms;
+  visibility: hidden;
+}
+.ql-container.ql-bubble:not(.ql-disabled) a:hover::before,
+.ql-container.ql-bubble:not(.ql-disabled) a:hover::after {
+  visibility: visible;
+}

+ 397 - 0
csair-vr-portal-ui/static/css/quill/quill.core.css

@@ -0,0 +1,397 @@
+/*!
+ * Quill Editor v1.3.6
+ * https://quilljs.com/
+ * Copyright (c) 2014, Jason Chen
+ * Copyright (c) 2013, salesforce.com
+ */
+.ql-container {
+  box-sizing: border-box;
+  font-family: Helvetica, Arial, sans-serif;
+  font-size: 13px;
+  height: 100%;
+  margin: 0px;
+  position: relative;
+}
+.ql-container.ql-disabled .ql-tooltip {
+  visibility: hidden;
+}
+.ql-container.ql-disabled .ql-editor ul[data-checked] > li::before {
+  pointer-events: none;
+}
+.ql-clipboard {
+  left: -100000px;
+  height: 1px;
+  overflow-y: hidden;
+  position: absolute;
+  top: 50%;
+}
+.ql-clipboard p {
+  margin: 0;
+  padding: 0;
+}
+.ql-editor {
+  box-sizing: border-box;
+  line-height: 1.42;
+  height: 100%;
+  outline: none;
+  overflow-y: auto;
+  padding: 12px 15px;
+  tab-size: 4;
+  -moz-tab-size: 4;
+  text-align: left;
+  white-space: pre-wrap;
+  word-wrap: break-word;
+}
+.ql-editor > * {
+  cursor: text;
+}
+.ql-editor p,
+.ql-editor ol,
+.ql-editor ul,
+.ql-editor pre,
+.ql-editor blockquote,
+.ql-editor h1,
+.ql-editor h2,
+.ql-editor h3,
+.ql-editor h4,
+.ql-editor h5,
+.ql-editor h6 {
+  margin: 0;
+  padding: 0;
+  counter-reset: list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;
+}
+.ql-editor ol,
+.ql-editor ul {
+  padding-left: 1.5em;
+}
+.ql-editor ol > li,
+.ql-editor ul > li {
+  list-style-type: none;
+}
+.ql-editor ul > li::before {
+  content: '\2022';
+}
+.ql-editor ul[data-checked=true],
+.ql-editor ul[data-checked=false] {
+  pointer-events: none;
+}
+.ql-editor ul[data-checked=true] > li *,
+.ql-editor ul[data-checked=false] > li * {
+  pointer-events: all;
+}
+.ql-editor ul[data-checked=true] > li::before,
+.ql-editor ul[data-checked=false] > li::before {
+  color: #777;
+  cursor: pointer;
+  pointer-events: all;
+}
+.ql-editor ul[data-checked=true] > li::before {
+  content: '\2611';
+}
+.ql-editor ul[data-checked=false] > li::before {
+  content: '\2610';
+}
+.ql-editor li::before {
+  display: inline-block;
+  white-space: nowrap;
+  width: 1.2em;
+}
+.ql-editor li:not(.ql-direction-rtl)::before {
+  margin-left: -1.5em;
+  margin-right: 0.3em;
+  text-align: right;
+}
+.ql-editor li.ql-direction-rtl::before {
+  margin-left: 0.3em;
+  margin-right: -1.5em;
+}
+.ql-editor ol li:not(.ql-direction-rtl),
+.ql-editor ul li:not(.ql-direction-rtl) {
+  padding-left: 1.5em;
+}
+.ql-editor ol li.ql-direction-rtl,
+.ql-editor ul li.ql-direction-rtl {
+  padding-right: 1.5em;
+}
+.ql-editor ol li {
+  counter-reset: list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;
+  counter-increment: list-0;
+}
+.ql-editor ol li:before {
+  content: counter(list-0, decimal) '. ';
+}
+.ql-editor ol li.ql-indent-1 {
+  counter-increment: list-1;
+}
+.ql-editor ol li.ql-indent-1:before {
+  content: counter(list-1, lower-alpha) '. ';
+}
+.ql-editor ol li.ql-indent-1 {
+  counter-reset: list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;
+}
+.ql-editor ol li.ql-indent-2 {
+  counter-increment: list-2;
+}
+.ql-editor ol li.ql-indent-2:before {
+  content: counter(list-2, lower-roman) '. ';
+}
+.ql-editor ol li.ql-indent-2 {
+  counter-reset: list-3 list-4 list-5 list-6 list-7 list-8 list-9;
+}
+.ql-editor ol li.ql-indent-3 {
+  counter-increment: list-3;
+}
+.ql-editor ol li.ql-indent-3:before {
+  content: counter(list-3, decimal) '. ';
+}
+.ql-editor ol li.ql-indent-3 {
+  counter-reset: list-4 list-5 list-6 list-7 list-8 list-9;
+}
+.ql-editor ol li.ql-indent-4 {
+  counter-increment: list-4;
+}
+.ql-editor ol li.ql-indent-4:before {
+  content: counter(list-4, lower-alpha) '. ';
+}
+.ql-editor ol li.ql-indent-4 {
+  counter-reset: list-5 list-6 list-7 list-8 list-9;
+}
+.ql-editor ol li.ql-indent-5 {
+  counter-increment: list-5;
+}
+.ql-editor ol li.ql-indent-5:before {
+  content: counter(list-5, lower-roman) '. ';
+}
+.ql-editor ol li.ql-indent-5 {
+  counter-reset: list-6 list-7 list-8 list-9;
+}
+.ql-editor ol li.ql-indent-6 {
+  counter-increment: list-6;
+}
+.ql-editor ol li.ql-indent-6:before {
+  content: counter(list-6, decimal) '. ';
+}
+.ql-editor ol li.ql-indent-6 {
+  counter-reset: list-7 list-8 list-9;
+}
+.ql-editor ol li.ql-indent-7 {
+  counter-increment: list-7;
+}
+.ql-editor ol li.ql-indent-7:before {
+  content: counter(list-7, lower-alpha) '. ';
+}
+.ql-editor ol li.ql-indent-7 {
+  counter-reset: list-8 list-9;
+}
+.ql-editor ol li.ql-indent-8 {
+  counter-increment: list-8;
+}
+.ql-editor ol li.ql-indent-8:before {
+  content: counter(list-8, lower-roman) '. ';
+}
+.ql-editor ol li.ql-indent-8 {
+  counter-reset: list-9;
+}
+.ql-editor ol li.ql-indent-9 {
+  counter-increment: list-9;
+}
+.ql-editor ol li.ql-indent-9:before {
+  content: counter(list-9, decimal) '. ';
+}
+.ql-editor .ql-indent-1:not(.ql-direction-rtl) {
+  padding-left: 3em;
+}
+.ql-editor li.ql-indent-1:not(.ql-direction-rtl) {
+  padding-left: 4.5em;
+}
+.ql-editor .ql-indent-1.ql-direction-rtl.ql-align-right {
+  padding-right: 3em;
+}
+.ql-editor li.ql-indent-1.ql-direction-rtl.ql-align-right {
+  padding-right: 4.5em;
+}
+.ql-editor .ql-indent-2:not(.ql-direction-rtl) {
+  padding-left: 6em;
+}
+.ql-editor li.ql-indent-2:not(.ql-direction-rtl) {
+  padding-left: 7.5em;
+}
+.ql-editor .ql-indent-2.ql-direction-rtl.ql-align-right {
+  padding-right: 6em;
+}
+.ql-editor li.ql-indent-2.ql-direction-rtl.ql-align-right {
+  padding-right: 7.5em;
+}
+.ql-editor .ql-indent-3:not(.ql-direction-rtl) {
+  padding-left: 9em;
+}
+.ql-editor li.ql-indent-3:not(.ql-direction-rtl) {
+  padding-left: 10.5em;
+}
+.ql-editor .ql-indent-3.ql-direction-rtl.ql-align-right {
+  padding-right: 9em;
+}
+.ql-editor li.ql-indent-3.ql-direction-rtl.ql-align-right {
+  padding-right: 10.5em;
+}
+.ql-editor .ql-indent-4:not(.ql-direction-rtl) {
+  padding-left: 12em;
+}
+.ql-editor li.ql-indent-4:not(.ql-direction-rtl) {
+  padding-left: 13.5em;
+}
+.ql-editor .ql-indent-4.ql-direction-rtl.ql-align-right {
+  padding-right: 12em;
+}
+.ql-editor li.ql-indent-4.ql-direction-rtl.ql-align-right {
+  padding-right: 13.5em;
+}
+.ql-editor .ql-indent-5:not(.ql-direction-rtl) {
+  padding-left: 15em;
+}
+.ql-editor li.ql-indent-5:not(.ql-direction-rtl) {
+  padding-left: 16.5em;
+}
+.ql-editor .ql-indent-5.ql-direction-rtl.ql-align-right {
+  padding-right: 15em;
+}
+.ql-editor li.ql-indent-5.ql-direction-rtl.ql-align-right {
+  padding-right: 16.5em;
+}
+.ql-editor .ql-indent-6:not(.ql-direction-rtl) {
+  padding-left: 18em;
+}
+.ql-editor li.ql-indent-6:not(.ql-direction-rtl) {
+  padding-left: 19.5em;
+}
+.ql-editor .ql-indent-6.ql-direction-rtl.ql-align-right {
+  padding-right: 18em;
+}
+.ql-editor li.ql-indent-6.ql-direction-rtl.ql-align-right {
+  padding-right: 19.5em;
+}
+.ql-editor .ql-indent-7:not(.ql-direction-rtl) {
+  padding-left: 21em;
+}
+.ql-editor li.ql-indent-7:not(.ql-direction-rtl) {
+  padding-left: 22.5em;
+}
+.ql-editor .ql-indent-7.ql-direction-rtl.ql-align-right {
+  padding-right: 21em;
+}
+.ql-editor li.ql-indent-7.ql-direction-rtl.ql-align-right {
+  padding-right: 22.5em;
+}
+.ql-editor .ql-indent-8:not(.ql-direction-rtl) {
+  padding-left: 24em;
+}
+.ql-editor li.ql-indent-8:not(.ql-direction-rtl) {
+  padding-left: 25.5em;
+}
+.ql-editor .ql-indent-8.ql-direction-rtl.ql-align-right {
+  padding-right: 24em;
+}
+.ql-editor li.ql-indent-8.ql-direction-rtl.ql-align-right {
+  padding-right: 25.5em;
+}
+.ql-editor .ql-indent-9:not(.ql-direction-rtl) {
+  padding-left: 27em;
+}
+.ql-editor li.ql-indent-9:not(.ql-direction-rtl) {
+  padding-left: 28.5em;
+}
+.ql-editor .ql-indent-9.ql-direction-rtl.ql-align-right {
+  padding-right: 27em;
+}
+.ql-editor li.ql-indent-9.ql-direction-rtl.ql-align-right {
+  padding-right: 28.5em;
+}
+.ql-editor .ql-video {
+  display: block;
+  max-width: 100%;
+}
+.ql-editor .ql-video.ql-align-center {
+  margin: 0 auto;
+}
+.ql-editor .ql-video.ql-align-right {
+  margin: 0 0 0 auto;
+}
+.ql-editor .ql-bg-black {
+  background-color: #000;
+}
+.ql-editor .ql-bg-red {
+  background-color: #e60000;
+}
+.ql-editor .ql-bg-orange {
+  background-color: #f90;
+}
+.ql-editor .ql-bg-yellow {
+  background-color: #ff0;
+}
+.ql-editor .ql-bg-green {
+  background-color: #008a00;
+}
+.ql-editor .ql-bg-blue {
+  background-color: #06c;
+}
+.ql-editor .ql-bg-purple {
+  background-color: #93f;
+}
+.ql-editor .ql-color-white {
+  color: #fff;
+}
+.ql-editor .ql-color-red {
+  color: #e60000;
+}
+.ql-editor .ql-color-orange {
+  color: #f90;
+}
+.ql-editor .ql-color-yellow {
+  color: #ff0;
+}
+.ql-editor .ql-color-green {
+  color: #008a00;
+}
+.ql-editor .ql-color-blue {
+  color: #06c;
+}
+.ql-editor .ql-color-purple {
+  color: #93f;
+}
+.ql-editor .ql-font-serif {
+  font-family: Georgia, Times New Roman, serif;
+}
+.ql-editor .ql-font-monospace {
+  font-family: Monaco, Courier New, monospace;
+}
+.ql-editor .ql-size-small {
+  font-size: 0.75em;
+}
+.ql-editor .ql-size-large {
+  font-size: 1.5em;
+}
+.ql-editor .ql-size-huge {
+  font-size: 2.5em;
+}
+.ql-editor .ql-direction-rtl {
+  direction: rtl;
+  text-align: inherit;
+}
+.ql-editor .ql-align-center {
+  text-align: center;
+}
+.ql-editor .ql-align-justify {
+  text-align: justify;
+}
+.ql-editor .ql-align-right {
+  text-align: right;
+}
+.ql-editor.ql-blank::before {
+  color: rgba(0,0,0,0.6);
+  content: attr(data-placeholder);
+  font-style: italic;
+  left: 15px;
+  pointer-events: none;
+  position: absolute;
+  right: 15px;
+}

+ 945 - 0
csair-vr-portal-ui/static/css/quill/quill.snow.css

@@ -0,0 +1,945 @@
+/*!
+ * Quill Editor v1.3.6
+ * https://quilljs.com/
+ * Copyright (c) 2014, Jason Chen
+ * Copyright (c) 2013, salesforce.com
+ */
+.ql-container {
+  box-sizing: border-box;
+  font-family: Helvetica, Arial, sans-serif;
+  font-size: 13px;
+  height: 100%;
+  margin: 0px;
+  position: relative;
+}
+.ql-container.ql-disabled .ql-tooltip {
+  visibility: hidden;
+}
+.ql-container.ql-disabled .ql-editor ul[data-checked] > li::before {
+  pointer-events: none;
+}
+.ql-clipboard {
+  left: -100000px;
+  height: 1px;
+  overflow-y: hidden;
+  position: absolute;
+  top: 50%;
+}
+.ql-clipboard p {
+  margin: 0;
+  padding: 0;
+}
+.ql-editor {
+  box-sizing: border-box;
+  line-height: 1.42;
+  height: 100%;
+  outline: none;
+  overflow-y: auto;
+  padding: 12px 15px;
+  tab-size: 4;
+  -moz-tab-size: 4;
+  text-align: left;
+  white-space: pre-wrap;
+  word-wrap: break-word;
+}
+.ql-editor > * {
+  cursor: text;
+}
+.ql-editor p,
+.ql-editor ol,
+.ql-editor ul,
+.ql-editor pre,
+.ql-editor blockquote,
+.ql-editor h1,
+.ql-editor h2,
+.ql-editor h3,
+.ql-editor h4,
+.ql-editor h5,
+.ql-editor h6 {
+  margin: 0;
+  padding: 0;
+  counter-reset: list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;
+}
+.ql-editor ol,
+.ql-editor ul {
+  padding-left: 1.5em;
+}
+.ql-editor ol > li,
+.ql-editor ul > li {
+  list-style-type: none;
+}
+.ql-editor ul > li::before {
+  content: '\2022';
+}
+.ql-editor ul[data-checked=true],
+.ql-editor ul[data-checked=false] {
+  pointer-events: none;
+}
+.ql-editor ul[data-checked=true] > li *,
+.ql-editor ul[data-checked=false] > li * {
+  pointer-events: all;
+}
+.ql-editor ul[data-checked=true] > li::before,
+.ql-editor ul[data-checked=false] > li::before {
+  color: #777;
+  cursor: pointer;
+  pointer-events: all;
+}
+.ql-editor ul[data-checked=true] > li::before {
+  content: '\2611';
+}
+.ql-editor ul[data-checked=false] > li::before {
+  content: '\2610';
+}
+.ql-editor li::before {
+  display: inline-block;
+  white-space: nowrap;
+  width: 1.2em;
+}
+.ql-editor li:not(.ql-direction-rtl)::before {
+  margin-left: -1.5em;
+  margin-right: 0.3em;
+  text-align: right;
+}
+.ql-editor li.ql-direction-rtl::before {
+  margin-left: 0.3em;
+  margin-right: -1.5em;
+}
+.ql-editor ol li:not(.ql-direction-rtl),
+.ql-editor ul li:not(.ql-direction-rtl) {
+  padding-left: 1.5em;
+}
+.ql-editor ol li.ql-direction-rtl,
+.ql-editor ul li.ql-direction-rtl {
+  padding-right: 1.5em;
+}
+.ql-editor ol li {
+  counter-reset: list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;
+  counter-increment: list-0;
+}
+.ql-editor ol li:before {
+  content: counter(list-0, decimal) '. ';
+}
+.ql-editor ol li.ql-indent-1 {
+  counter-increment: list-1;
+}
+.ql-editor ol li.ql-indent-1:before {
+  content: counter(list-1, lower-alpha) '. ';
+}
+.ql-editor ol li.ql-indent-1 {
+  counter-reset: list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;
+}
+.ql-editor ol li.ql-indent-2 {
+  counter-increment: list-2;
+}
+.ql-editor ol li.ql-indent-2:before {
+  content: counter(list-2, lower-roman) '. ';
+}
+.ql-editor ol li.ql-indent-2 {
+  counter-reset: list-3 list-4 list-5 list-6 list-7 list-8 list-9;
+}
+.ql-editor ol li.ql-indent-3 {
+  counter-increment: list-3;
+}
+.ql-editor ol li.ql-indent-3:before {
+  content: counter(list-3, decimal) '. ';
+}
+.ql-editor ol li.ql-indent-3 {
+  counter-reset: list-4 list-5 list-6 list-7 list-8 list-9;
+}
+.ql-editor ol li.ql-indent-4 {
+  counter-increment: list-4;
+}
+.ql-editor ol li.ql-indent-4:before {
+  content: counter(list-4, lower-alpha) '. ';
+}
+.ql-editor ol li.ql-indent-4 {
+  counter-reset: list-5 list-6 list-7 list-8 list-9;
+}
+.ql-editor ol li.ql-indent-5 {
+  counter-increment: list-5;
+}
+.ql-editor ol li.ql-indent-5:before {
+  content: counter(list-5, lower-roman) '. ';
+}
+.ql-editor ol li.ql-indent-5 {
+  counter-reset: list-6 list-7 list-8 list-9;
+}
+.ql-editor ol li.ql-indent-6 {
+  counter-increment: list-6;
+}
+.ql-editor ol li.ql-indent-6:before {
+  content: counter(list-6, decimal) '. ';
+}
+.ql-editor ol li.ql-indent-6 {
+  counter-reset: list-7 list-8 list-9;
+}
+.ql-editor ol li.ql-indent-7 {
+  counter-increment: list-7;
+}
+.ql-editor ol li.ql-indent-7:before {
+  content: counter(list-7, lower-alpha) '. ';
+}
+.ql-editor ol li.ql-indent-7 {
+  counter-reset: list-8 list-9;
+}
+.ql-editor ol li.ql-indent-8 {
+  counter-increment: list-8;
+}
+.ql-editor ol li.ql-indent-8:before {
+  content: counter(list-8, lower-roman) '. ';
+}
+.ql-editor ol li.ql-indent-8 {
+  counter-reset: list-9;
+}
+.ql-editor ol li.ql-indent-9 {
+  counter-increment: list-9;
+}
+.ql-editor ol li.ql-indent-9:before {
+  content: counter(list-9, decimal) '. ';
+}
+.ql-editor .ql-indent-1:not(.ql-direction-rtl) {
+  padding-left: 3em;
+}
+.ql-editor li.ql-indent-1:not(.ql-direction-rtl) {
+  padding-left: 4.5em;
+}
+.ql-editor .ql-indent-1.ql-direction-rtl.ql-align-right {
+  padding-right: 3em;
+}
+.ql-editor li.ql-indent-1.ql-direction-rtl.ql-align-right {
+  padding-right: 4.5em;
+}
+.ql-editor .ql-indent-2:not(.ql-direction-rtl) {
+  padding-left: 6em;
+}
+.ql-editor li.ql-indent-2:not(.ql-direction-rtl) {
+  padding-left: 7.5em;
+}
+.ql-editor .ql-indent-2.ql-direction-rtl.ql-align-right {
+  padding-right: 6em;
+}
+.ql-editor li.ql-indent-2.ql-direction-rtl.ql-align-right {
+  padding-right: 7.5em;
+}
+.ql-editor .ql-indent-3:not(.ql-direction-rtl) {
+  padding-left: 9em;
+}
+.ql-editor li.ql-indent-3:not(.ql-direction-rtl) {
+  padding-left: 10.5em;
+}
+.ql-editor .ql-indent-3.ql-direction-rtl.ql-align-right {
+  padding-right: 9em;
+}
+.ql-editor li.ql-indent-3.ql-direction-rtl.ql-align-right {
+  padding-right: 10.5em;
+}
+.ql-editor .ql-indent-4:not(.ql-direction-rtl) {
+  padding-left: 12em;
+}
+.ql-editor li.ql-indent-4:not(.ql-direction-rtl) {
+  padding-left: 13.5em;
+}
+.ql-editor .ql-indent-4.ql-direction-rtl.ql-align-right {
+  padding-right: 12em;
+}
+.ql-editor li.ql-indent-4.ql-direction-rtl.ql-align-right {
+  padding-right: 13.5em;
+}
+.ql-editor .ql-indent-5:not(.ql-direction-rtl) {
+  padding-left: 15em;
+}
+.ql-editor li.ql-indent-5:not(.ql-direction-rtl) {
+  padding-left: 16.5em;
+}
+.ql-editor .ql-indent-5.ql-direction-rtl.ql-align-right {
+  padding-right: 15em;
+}
+.ql-editor li.ql-indent-5.ql-direction-rtl.ql-align-right {
+  padding-right: 16.5em;
+}
+.ql-editor .ql-indent-6:not(.ql-direction-rtl) {
+  padding-left: 18em;
+}
+.ql-editor li.ql-indent-6:not(.ql-direction-rtl) {
+  padding-left: 19.5em;
+}
+.ql-editor .ql-indent-6.ql-direction-rtl.ql-align-right {
+  padding-right: 18em;
+}
+.ql-editor li.ql-indent-6.ql-direction-rtl.ql-align-right {
+  padding-right: 19.5em;
+}
+.ql-editor .ql-indent-7:not(.ql-direction-rtl) {
+  padding-left: 21em;
+}
+.ql-editor li.ql-indent-7:not(.ql-direction-rtl) {
+  padding-left: 22.5em;
+}
+.ql-editor .ql-indent-7.ql-direction-rtl.ql-align-right {
+  padding-right: 21em;
+}
+.ql-editor li.ql-indent-7.ql-direction-rtl.ql-align-right {
+  padding-right: 22.5em;
+}
+.ql-editor .ql-indent-8:not(.ql-direction-rtl) {
+  padding-left: 24em;
+}
+.ql-editor li.ql-indent-8:not(.ql-direction-rtl) {
+  padding-left: 25.5em;
+}
+.ql-editor .ql-indent-8.ql-direction-rtl.ql-align-right {
+  padding-right: 24em;
+}
+.ql-editor li.ql-indent-8.ql-direction-rtl.ql-align-right {
+  padding-right: 25.5em;
+}
+.ql-editor .ql-indent-9:not(.ql-direction-rtl) {
+  padding-left: 27em;
+}
+.ql-editor li.ql-indent-9:not(.ql-direction-rtl) {
+  padding-left: 28.5em;
+}
+.ql-editor .ql-indent-9.ql-direction-rtl.ql-align-right {
+  padding-right: 27em;
+}
+.ql-editor li.ql-indent-9.ql-direction-rtl.ql-align-right {
+  padding-right: 28.5em;
+}
+.ql-editor .ql-video {
+  display: block;
+  max-width: 100%;
+}
+.ql-editor .ql-video.ql-align-center {
+  margin: 0 auto;
+}
+.ql-editor .ql-video.ql-align-right {
+  margin: 0 0 0 auto;
+}
+.ql-editor .ql-bg-black {
+  background-color: #000;
+}
+.ql-editor .ql-bg-red {
+  background-color: #e60000;
+}
+.ql-editor .ql-bg-orange {
+  background-color: #f90;
+}
+.ql-editor .ql-bg-yellow {
+  background-color: #ff0;
+}
+.ql-editor .ql-bg-green {
+  background-color: #008a00;
+}
+.ql-editor .ql-bg-blue {
+  background-color: #06c;
+}
+.ql-editor .ql-bg-purple {
+  background-color: #93f;
+}
+.ql-editor .ql-color-white {
+  color: #fff;
+}
+.ql-editor .ql-color-red {
+  color: #e60000;
+}
+.ql-editor .ql-color-orange {
+  color: #f90;
+}
+.ql-editor .ql-color-yellow {
+  color: #ff0;
+}
+.ql-editor .ql-color-green {
+  color: #008a00;
+}
+.ql-editor .ql-color-blue {
+  color: #06c;
+}
+.ql-editor .ql-color-purple {
+  color: #93f;
+}
+.ql-editor .ql-font-serif {
+  font-family: Georgia, Times New Roman, serif;
+}
+.ql-editor .ql-font-monospace {
+  font-family: Monaco, Courier New, monospace;
+}
+.ql-editor .ql-size-small {
+  font-size: 0.75em;
+}
+.ql-editor .ql-size-large {
+  font-size: 1.5em;
+}
+.ql-editor .ql-size-huge {
+  font-size: 2.5em;
+}
+.ql-editor .ql-direction-rtl {
+  direction: rtl;
+  text-align: inherit;
+}
+.ql-editor .ql-align-center {
+  text-align: center;
+}
+.ql-editor .ql-align-justify {
+  text-align: justify;
+}
+.ql-editor .ql-align-right {
+  text-align: right;
+}
+.ql-editor.ql-blank::before {
+  color: rgba(0,0,0,0.6);
+  content: attr(data-placeholder);
+  font-style: italic;
+  left: 15px;
+  pointer-events: none;
+  position: absolute;
+  right: 15px;
+}
+.ql-snow.ql-toolbar:after,
+.ql-snow .ql-toolbar:after {
+  clear: both;
+  content: '';
+  display: table;
+}
+.ql-snow.ql-toolbar button,
+.ql-snow .ql-toolbar button {
+  background: none;
+  border: none;
+  cursor: pointer;
+  display: inline-block;
+  float: left;
+  height: 24px;
+  padding: 3px 5px;
+  width: 28px;
+}
+.ql-snow.ql-toolbar button svg,
+.ql-snow .ql-toolbar button svg {
+  float: left;
+  height: 100%;
+}
+.ql-snow.ql-toolbar button:active:hover,
+.ql-snow .ql-toolbar button:active:hover {
+  outline: none;
+}
+.ql-snow.ql-toolbar input.ql-image[type=file],
+.ql-snow .ql-toolbar input.ql-image[type=file] {
+  display: none;
+}
+.ql-snow.ql-toolbar button:hover,
+.ql-snow .ql-toolbar button:hover,
+.ql-snow.ql-toolbar button:focus,
+.ql-snow .ql-toolbar button:focus,
+.ql-snow.ql-toolbar button.ql-active,
+.ql-snow .ql-toolbar button.ql-active,
+.ql-snow.ql-toolbar .ql-picker-label:hover,
+.ql-snow .ql-toolbar .ql-picker-label:hover,
+.ql-snow.ql-toolbar .ql-picker-label.ql-active,
+.ql-snow .ql-toolbar .ql-picker-label.ql-active,
+.ql-snow.ql-toolbar .ql-picker-item:hover,
+.ql-snow .ql-toolbar .ql-picker-item:hover,
+.ql-snow.ql-toolbar .ql-picker-item.ql-selected,
+.ql-snow .ql-toolbar .ql-picker-item.ql-selected {
+  color: #06c;
+}
+.ql-snow.ql-toolbar button:hover .ql-fill,
+.ql-snow .ql-toolbar button:hover .ql-fill,
+.ql-snow.ql-toolbar button:focus .ql-fill,
+.ql-snow .ql-toolbar button:focus .ql-fill,
+.ql-snow.ql-toolbar button.ql-active .ql-fill,
+.ql-snow .ql-toolbar button.ql-active .ql-fill,
+.ql-snow.ql-toolbar .ql-picker-label:hover .ql-fill,
+.ql-snow .ql-toolbar .ql-picker-label:hover .ql-fill,
+.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-fill,
+.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-fill,
+.ql-snow.ql-toolbar .ql-picker-item:hover .ql-fill,
+.ql-snow .ql-toolbar .ql-picker-item:hover .ql-fill,
+.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-fill,
+.ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-fill,
+.ql-snow.ql-toolbar button:hover .ql-stroke.ql-fill,
+.ql-snow .ql-toolbar button:hover .ql-stroke.ql-fill,
+.ql-snow.ql-toolbar button:focus .ql-stroke.ql-fill,
+.ql-snow .ql-toolbar button:focus .ql-stroke.ql-fill,
+.ql-snow.ql-toolbar button.ql-active .ql-stroke.ql-fill,
+.ql-snow .ql-toolbar button.ql-active .ql-stroke.ql-fill,
+.ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill,
+.ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill,
+.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill,
+.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill,
+.ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill,
+.ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill,
+.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill,
+.ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill {
+  fill: #06c;
+}
+.ql-snow.ql-toolbar button:hover .ql-stroke,
+.ql-snow .ql-toolbar button:hover .ql-stroke,
+.ql-snow.ql-toolbar button:focus .ql-stroke,
+.ql-snow .ql-toolbar button:focus .ql-stroke,
+.ql-snow.ql-toolbar button.ql-active .ql-stroke,
+.ql-snow .ql-toolbar button.ql-active .ql-stroke,
+.ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke,
+.ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke,
+.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke,
+.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke,
+.ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke,
+.ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke,
+.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke,
+.ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke,
+.ql-snow.ql-toolbar button:hover .ql-stroke-miter,
+.ql-snow .ql-toolbar button:hover .ql-stroke-miter,
+.ql-snow.ql-toolbar button:focus .ql-stroke-miter,
+.ql-snow .ql-toolbar button:focus .ql-stroke-miter,
+.ql-snow.ql-toolbar button.ql-active .ql-stroke-miter,
+.ql-snow .ql-toolbar button.ql-active .ql-stroke-miter,
+.ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke-miter,
+.ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke-miter,
+.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter,
+.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter,
+.ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke-miter,
+.ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke-miter,
+.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter,
+.ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter {
+  stroke: #06c;
+}
+@media (pointer: coarse) {
+  .ql-snow.ql-toolbar button:hover:not(.ql-active),
+  .ql-snow .ql-toolbar button:hover:not(.ql-active) {
+    color: #444;
+  }
+  .ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-fill,
+  .ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-fill,
+  .ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill,
+  .ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill {
+    fill: #444;
+  }
+  .ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-stroke,
+  .ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-stroke,
+  .ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter,
+  .ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter {
+    stroke: #444;
+  }
+}
+.ql-snow {
+  box-sizing: border-box;
+}
+.ql-snow * {
+  box-sizing: border-box;
+}
+.ql-snow .ql-hidden {
+  display: none;
+}
+.ql-snow .ql-out-bottom,
+.ql-snow .ql-out-top {
+  visibility: hidden;
+}
+.ql-snow .ql-tooltip {
+  position: absolute;
+  transform: translateY(10px);
+}
+.ql-snow .ql-tooltip a {
+  cursor: pointer;
+  text-decoration: none;
+}
+.ql-snow .ql-tooltip.ql-flip {
+  transform: translateY(-10px);
+}
+.ql-snow .ql-formats {
+  display: inline-block;
+  vertical-align: middle;
+}
+.ql-snow .ql-formats:after {
+  clear: both;
+  content: '';
+  display: table;
+}
+.ql-snow .ql-stroke {
+  fill: none;
+  stroke: #444;
+  stroke-linecap: round;
+  stroke-linejoin: round;
+  stroke-width: 2;
+}
+.ql-snow .ql-stroke-miter {
+  fill: none;
+  stroke: #444;
+  stroke-miterlimit: 10;
+  stroke-width: 2;
+}
+.ql-snow .ql-fill,
+.ql-snow .ql-stroke.ql-fill {
+  fill: #444;
+}
+.ql-snow .ql-empty {
+  fill: none;
+}
+.ql-snow .ql-even {
+  fill-rule: evenodd;
+}
+.ql-snow .ql-thin,
+.ql-snow .ql-stroke.ql-thin {
+  stroke-width: 1;
+}
+.ql-snow .ql-transparent {
+  opacity: 0.4;
+}
+.ql-snow .ql-direction svg:last-child {
+  display: none;
+}
+.ql-snow .ql-direction.ql-active svg:last-child {
+  display: inline;
+}
+.ql-snow .ql-direction.ql-active svg:first-child {
+  display: none;
+}
+.ql-snow .ql-editor h1 {
+  font-size: 2em;
+}
+.ql-snow .ql-editor h2 {
+  font-size: 1.5em;
+}
+.ql-snow .ql-editor h3 {
+  font-size: 1.17em;
+}
+.ql-snow .ql-editor h4 {
+  font-size: 1em;
+}
+.ql-snow .ql-editor h5 {
+  font-size: 0.83em;
+}
+.ql-snow .ql-editor h6 {
+  font-size: 0.67em;
+}
+.ql-snow .ql-editor a {
+  text-decoration: underline;
+}
+.ql-snow .ql-editor blockquote {
+  border-left: 4px solid #ccc;
+  margin-bottom: 5px;
+  margin-top: 5px;
+  padding-left: 16px;
+}
+.ql-snow .ql-editor code,
+.ql-snow .ql-editor pre {
+  background-color: #f0f0f0;
+  border-radius: 3px;
+}
+.ql-snow .ql-editor pre {
+  white-space: pre-wrap;
+  margin-bottom: 5px;
+  margin-top: 5px;
+  padding: 5px 10px;
+}
+.ql-snow .ql-editor code {
+  font-size: 85%;
+  padding: 2px 4px;
+}
+.ql-snow .ql-editor pre.ql-syntax {
+  background-color: #23241f;
+  color: #f8f8f2;
+  overflow: visible;
+}
+.ql-snow .ql-editor img {
+  max-width: 100%;
+}
+.ql-snow .ql-picker {
+  color: #444;
+  display: inline-block;
+  float: left;
+  font-size: 14px;
+  font-weight: 500;
+  height: 24px;
+  position: relative;
+  vertical-align: middle;
+}
+.ql-snow .ql-picker-label {
+  cursor: pointer;
+  display: inline-block;
+  height: 100%;
+  padding-left: 8px;
+  padding-right: 2px;
+  position: relative;
+  width: 100%;
+}
+.ql-snow .ql-picker-label::before {
+  display: inline-block;
+  line-height: 22px;
+}
+.ql-snow .ql-picker-options {
+  background-color: #fff;
+  display: none;
+  min-width: 100%;
+  padding: 4px 8px;
+  position: absolute;
+  white-space: nowrap;
+}
+.ql-snow .ql-picker-options .ql-picker-item {
+  cursor: pointer;
+  display: block;
+  padding-bottom: 5px;
+  padding-top: 5px;
+}
+.ql-snow .ql-picker.ql-expanded .ql-picker-label {
+  color: #ccc;
+  z-index: 2;
+}
+.ql-snow .ql-picker.ql-expanded .ql-picker-label .ql-fill {
+  fill: #ccc;
+}
+.ql-snow .ql-picker.ql-expanded .ql-picker-label .ql-stroke {
+  stroke: #ccc;
+}
+.ql-snow .ql-picker.ql-expanded .ql-picker-options {
+  display: block;
+  margin-top: -1px;
+  top: 100%;
+  z-index: 1;
+}
+.ql-snow .ql-color-picker,
+.ql-snow .ql-icon-picker {
+  width: 28px;
+}
+.ql-snow .ql-color-picker .ql-picker-label,
+.ql-snow .ql-icon-picker .ql-picker-label {
+  padding: 2px 4px;
+}
+.ql-snow .ql-color-picker .ql-picker-label svg,
+.ql-snow .ql-icon-picker .ql-picker-label svg {
+  right: 4px;
+}
+.ql-snow .ql-icon-picker .ql-picker-options {
+  padding: 4px 0px;
+}
+.ql-snow .ql-icon-picker .ql-picker-item {
+  height: 24px;
+  width: 24px;
+  padding: 2px 4px;
+}
+.ql-snow .ql-color-picker .ql-picker-options {
+  padding: 3px 5px;
+  width: 152px;
+}
+.ql-snow .ql-color-picker .ql-picker-item {
+  border: 1px solid transparent;
+  float: left;
+  height: 16px;
+  margin: 2px;
+  padding: 0px;
+  width: 16px;
+}
+.ql-snow .ql-picker:not(.ql-color-picker):not(.ql-icon-picker) svg {
+  position: absolute;
+  margin-top: -9px;
+  right: 0;
+  top: 50%;
+  width: 18px;
+}
+.ql-snow .ql-picker.ql-header .ql-picker-label[data-label]:not([data-label=''])::before,
+.ql-snow .ql-picker.ql-font .ql-picker-label[data-label]:not([data-label=''])::before,
+.ql-snow .ql-picker.ql-size .ql-picker-label[data-label]:not([data-label=''])::before,
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-label]:not([data-label=''])::before,
+.ql-snow .ql-picker.ql-font .ql-picker-item[data-label]:not([data-label=''])::before,
+.ql-snow .ql-picker.ql-size .ql-picker-item[data-label]:not([data-label=''])::before {
+  content: attr(data-label);
+}
+.ql-snow .ql-picker.ql-header {
+  width: 98px;
+}
+.ql-snow .ql-picker.ql-header .ql-picker-label::before,
+.ql-snow .ql-picker.ql-header .ql-picker-item::before {
+  content: 'Normal';
+}
+.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"]::before,
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before {
+  content: 'Heading 1';
+}
+.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="2"]::before,
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before {
+  content: 'Heading 2';
+}
+.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"]::before,
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before {
+  content: 'Heading 3';
+}
+.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="4"]::before,
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before {
+  content: 'Heading 4';
+}
+.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="5"]::before,
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before {
+  content: 'Heading 5';
+}
+.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="6"]::before,
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before {
+  content: 'Heading 6';
+}
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before {
+  font-size: 2em;
+}
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before {
+  font-size: 1.5em;
+}
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before {
+  font-size: 1.17em;
+}
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before {
+  font-size: 1em;
+}
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before {
+  font-size: 0.83em;
+}
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before {
+  font-size: 0.67em;
+}
+.ql-snow .ql-picker.ql-font {
+  width: 108px;
+}
+.ql-snow .ql-picker.ql-font .ql-picker-label::before,
+.ql-snow .ql-picker.ql-font .ql-picker-item::before {
+  content: 'Sans Serif';
+}
+.ql-snow .ql-picker.ql-font .ql-picker-label[data-value=serif]::before,
+.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=serif]::before {
+  content: 'Serif';
+}
+.ql-snow .ql-picker.ql-font .ql-picker-label[data-value=monospace]::before,
+.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=monospace]::before {
+  content: 'Monospace';
+}
+.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=serif]::before {
+  font-family: Georgia, Times New Roman, serif;
+}
+.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=monospace]::before {
+  font-family: Monaco, Courier New, monospace;
+}
+.ql-snow .ql-picker.ql-size {
+  width: 98px;
+}
+.ql-snow .ql-picker.ql-size .ql-picker-label::before,
+.ql-snow .ql-picker.ql-size .ql-picker-item::before {
+  content: 'Normal';
+}
+.ql-snow .ql-picker.ql-size .ql-picker-label[data-value=small]::before,
+.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=small]::before {
+  content: 'Small';
+}
+.ql-snow .ql-picker.ql-size .ql-picker-label[data-value=large]::before,
+.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=large]::before {
+  content: 'Large';
+}
+.ql-snow .ql-picker.ql-size .ql-picker-label[data-value=huge]::before,
+.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=huge]::before {
+  content: 'Huge';
+}
+.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=small]::before {
+  font-size: 10px;
+}
+.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=large]::before {
+  font-size: 18px;
+}
+.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=huge]::before {
+  font-size: 32px;
+}
+.ql-snow .ql-color-picker.ql-background .ql-picker-item {
+  background-color: #fff;
+}
+.ql-snow .ql-color-picker.ql-color .ql-picker-item {
+  background-color: #000;
+}
+.ql-toolbar.ql-snow {
+  border: 1px solid #ccc;
+  box-sizing: border-box;
+  font-family: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
+  padding: 8px;
+}
+.ql-toolbar.ql-snow .ql-formats {
+  margin-right: 15px;
+}
+.ql-toolbar.ql-snow .ql-picker-label {
+  border: 1px solid transparent;
+}
+.ql-toolbar.ql-snow .ql-picker-options {
+  border: 1px solid transparent;
+  box-shadow: rgba(0,0,0,0.2) 0 2px 8px;
+}
+.ql-toolbar.ql-snow .ql-picker.ql-expanded .ql-picker-label {
+  border-color: #ccc;
+}
+.ql-toolbar.ql-snow .ql-picker.ql-expanded .ql-picker-options {
+  border-color: #ccc;
+}
+.ql-toolbar.ql-snow .ql-color-picker .ql-picker-item.ql-selected,
+.ql-toolbar.ql-snow .ql-color-picker .ql-picker-item:hover {
+  border-color: #000;
+}
+.ql-toolbar.ql-snow + .ql-container.ql-snow {
+  border-top: 0px;
+}
+.ql-snow .ql-tooltip {
+  background-color: #fff;
+  border: 1px solid #ccc;
+  box-shadow: 0px 0px 5px #ddd;
+  color: #444;
+  padding: 5px 12px;
+  white-space: nowrap;
+}
+.ql-snow .ql-tooltip::before {
+  content: "Visit URL:";
+  line-height: 26px;
+  margin-right: 8px;
+}
+.ql-snow .ql-tooltip input[type=text] {
+  display: none;
+  border: 1px solid #ccc;
+  font-size: 13px;
+  height: 26px;
+  margin: 0px;
+  padding: 3px 5px;
+  width: 170px;
+}
+.ql-snow .ql-tooltip a.ql-preview {
+  display: inline-block;
+  max-width: 200px;
+  overflow-x: hidden;
+  text-overflow: ellipsis;
+  vertical-align: top;
+}
+.ql-snow .ql-tooltip a.ql-action::after {
+  border-right: 1px solid #ccc;
+  content: 'Edit';
+  margin-left: 16px;
+  padding-right: 8px;
+}
+.ql-snow .ql-tooltip a.ql-remove::before {
+  content: 'Remove';
+  margin-left: 8px;
+}
+.ql-snow .ql-tooltip a {
+  line-height: 26px;
+}
+.ql-snow .ql-tooltip.ql-editing a.ql-preview,
+.ql-snow .ql-tooltip.ql-editing a.ql-remove {
+  display: none;
+}
+.ql-snow .ql-tooltip.ql-editing input[type=text] {
+  display: inline-block;
+}
+.ql-snow .ql-tooltip.ql-editing a.ql-action::after {
+  border-right: 0px;
+  content: 'Save';
+  padding-right: 0px;
+}
+.ql-snow .ql-tooltip[data-mode=link]::before {
+  content: "Enter link:";
+}
+.ql-snow .ql-tooltip[data-mode=formula]::before {
+  content: "Enter formula:";
+}
+.ql-snow .ql-tooltip[data-mode=video]::before {
+  content: "Enter video:";
+}
+.ql-snow a {
+  color: #06c;
+}
+.ql-container.ql-snow {
+  border: 1px solid #ccc;
+}

BIN
csair-vr-portal-ui/static/img/vr-panorama.png


BIN
lib/dom4j-2.1.1.jar


BIN
lib/tomcat-embed-websocket-9.0.22.jar


+ 222 - 0
linghang.iml

@@ -0,0 +1,222 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module org.jetbrains.idea.maven.project.MavenProjectsManager.isMavenModule="true" version="4">
+  <component name="FacetManager">
+    <facet type="jpa" name="JPA">
+      <configuration>
+        <setting name="validation-enabled" value="true" />
+        <setting name="provider-name" value="Hibernate" />
+        <datasource-mapping />
+        <naming-strategy-map />
+      </configuration>
+    </facet>
+    <facet type="Spring" name="Spring">
+      <configuration />
+    </facet>
+    <facet type="web" name="Web">
+      <configuration>
+        <webroots />
+      </configuration>
+    </facet>
+  </component>
+  <component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8">
+    <output url="file://$MODULE_DIR$/target/classes" />
+    <output-test url="file://$MODULE_DIR$/target/test-classes" />
+    <content url="file://$MODULE_DIR$">
+      <sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
+      <excludeFolder url="file://$MODULE_DIR$/target" />
+    </content>
+    <orderEntry type="inheritedJdk" />
+    <orderEntry type="sourceFolder" forTests="false" />
+    <orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-web:2.1.10.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter:2.1.10.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-logging:2.1.10.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: ch.qos.logback:logback-classic:1.2.3" level="project" />
+    <orderEntry type="library" name="Maven: ch.qos.logback:logback-core:1.2.3" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.logging.log4j:log4j-to-slf4j:2.11.2" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.logging.log4j:log4j-api:2.11.2" level="project" />
+    <orderEntry type="library" name="Maven: org.slf4j:jul-to-slf4j:1.7.29" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-json:2.1.10.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.9.10" level="project" />
+    <orderEntry type="library" name="Maven: com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.9.10" level="project" />
+    <orderEntry type="library" name="Maven: com.fasterxml.jackson.module:jackson-module-parameter-names:2.9.10" level="project" />
+    <orderEntry type="library" name="Maven: org.hibernate.validator:hibernate-validator:6.0.18.Final" level="project" />
+    <orderEntry type="library" name="Maven: javax.validation:validation-api:2.0.1.Final" level="project" />
+    <orderEntry type="library" name="Maven: org.jboss.logging:jboss-logging:3.3.3.Final" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework:spring-web:5.1.11.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework:spring-beans:5.1.11.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework:spring-webmvc:5.1.11.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework:spring-context:5.1.11.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework:spring-expression:5.1.11.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-data-jpa:2.1.10.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-aop:2.1.10.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.aspectj:aspectjweaver:1.9.4" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-jdbc:2.1.10.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: com.zaxxer:HikariCP:3.2.0" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework:spring-jdbc:5.1.11.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: javax.transaction:javax.transaction-api:1.3" level="project" />
+    <orderEntry type="library" name="Maven: javax.xml.bind:jaxb-api:2.3.1" level="project" />
+    <orderEntry type="library" name="Maven: javax.activation:javax.activation-api:1.2.0" level="project" />
+    <orderEntry type="library" name="Maven: org.hibernate:hibernate-core:5.3.13.Final" level="project" />
+    <orderEntry type="library" name="Maven: javax.persistence:javax.persistence-api:2.2" level="project" />
+    <orderEntry type="library" name="Maven: org.javassist:javassist:3.23.2-GA" level="project" />
+    <orderEntry type="library" name="Maven: net.bytebuddy:byte-buddy:1.9.16" level="project" />
+    <orderEntry type="library" name="Maven: antlr:antlr:2.7.7" level="project" />
+    <orderEntry type="library" name="Maven: org.jboss:jandex:2.0.5.Final" level="project" />
+    <orderEntry type="library" name="Maven: org.dom4j:dom4j:2.1.1" level="project" />
+    <orderEntry type="library" name="Maven: org.hibernate.common:hibernate-commons-annotations:5.0.4.Final" level="project" />
+    <orderEntry type="library" name="Maven: org.glassfish.jaxb:jaxb-runtime:2.3.1" level="project" />
+    <orderEntry type="library" name="Maven: org.glassfish.jaxb:txw2:2.3.1" level="project" />
+    <orderEntry type="library" name="Maven: com.sun.istack:istack-commons-runtime:3.0.7" level="project" />
+    <orderEntry type="library" name="Maven: org.jvnet.staxex:stax-ex:1.8" level="project" />
+    <orderEntry type="library" name="Maven: com.sun.xml.fastinfoset:FastInfoset:1.2.15" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.data:spring-data-jpa:2.1.12.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.data:spring-data-commons:2.1.12.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework:spring-orm:5.1.11.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework:spring-tx:5.1.11.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework:spring-aspects:5.1.11.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-tomcat:2.1.10.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: javax.annotation:javax.annotation-api:1.3.2" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.tomcat.embed:tomcat-embed-el:9.0.27" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-amqp:2.1.10.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework:spring-messaging:5.1.11.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.amqp:spring-rabbit:2.1.12.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: com.rabbitmq:amqp-client:5.4.3" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.amqp:spring-amqp:2.1.12.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.retry:spring-retry:1.2.4.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-security:2.1.10.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework:spring-aop:5.1.11.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.security:spring-security-config:5.1.7.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.security:spring-security-web:5.1.7.RELEASE" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.springframework.boot:spring-boot-starter-test:2.1.10.RELEASE" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.springframework.boot:spring-boot-test:2.1.10.RELEASE" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.springframework.boot:spring-boot-test-autoconfigure:2.1.10.RELEASE" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: com.jayway.jsonpath:json-path:2.4.0" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: net.minidev:json-smart:2.3" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: net.minidev:accessors-smart:1.2" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.ow2.asm:asm:5.0.4" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: junit:junit:4.12" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.mockito:mockito-core:2.23.4" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: net.bytebuddy:byte-buddy-agent:1.9.16" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.objenesis:objenesis:2.6" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.hamcrest:hamcrest-core:1.3" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.hamcrest:hamcrest-library:1.3" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.skyscreamer:jsonassert:1.5.0" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: com.vaadin.external.google:android-json:0.0.20131108.vaadin1" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework:spring-core:5.1.11.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework:spring-jcl:5.1.11.RELEASE" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.springframework:spring-test:5.1.11.RELEASE" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.xmlunit:xmlunit-core:2.6.3" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.springframework.security:spring-security-test:5.1.7.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.security:spring-security-core:5.1.7.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-reactor-netty:2.1.4.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: io.projectreactor.netty:reactor-netty:0.8.13.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: io.netty:netty-codec-http:4.1.43.Final" level="project" />
+    <orderEntry type="library" name="Maven: io.netty:netty-common:4.1.43.Final" level="project" />
+    <orderEntry type="library" name="Maven: io.netty:netty-buffer:4.1.43.Final" level="project" />
+    <orderEntry type="library" name="Maven: io.netty:netty-transport:4.1.43.Final" level="project" />
+    <orderEntry type="library" name="Maven: io.netty:netty-resolver:4.1.43.Final" level="project" />
+    <orderEntry type="library" name="Maven: io.netty:netty-codec:4.1.43.Final" level="project" />
+    <orderEntry type="library" name="Maven: io.netty:netty-codec-http2:4.1.43.Final" level="project" />
+    <orderEntry type="library" name="Maven: io.netty:netty-handler:4.1.43.Final" level="project" />
+    <orderEntry type="library" name="Maven: io.netty:netty-handler-proxy:4.1.43.Final" level="project" />
+    <orderEntry type="library" name="Maven: io.netty:netty-codec-socks:4.1.43.Final" level="project" />
+    <orderEntry type="library" name="Maven: io.netty:netty-transport-native-epoll:linux-x86_64:4.1.43.Final" level="project" />
+    <orderEntry type="library" name="Maven: io.netty:netty-transport-native-unix-common:4.1.43.Final" level="project" />
+    <orderEntry type="library" name="Maven: io.projectreactor:reactor-core:3.2.12.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.reactivestreams:reactive-streams:1.0.3" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-autoconfigure:2.1.10.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.boot:spring-boot:2.1.10.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-websocket:2.1.10.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework:spring-websocket:5.1.11.RELEASE" level="project" />
+    <orderEntry type="library" scope="RUNTIME" name="Maven: org.springframework.boot:spring-boot-devtools:2.1.10.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-actuator:2.1.10.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-actuator-autoconfigure:2.1.10.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-actuator:2.1.10.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: io.micrometer:micrometer-core:1.1.8" level="project" />
+    <orderEntry type="library" name="Maven: org.hdrhistogram:HdrHistogram:2.1.9" level="project" />
+    <orderEntry type="library" name="Maven: org.latencyutils:LatencyUtils:2.0.3" level="project" />
+    <orderEntry type="library" name="Maven: de.codecentric:spring-boot-admin-starter-client:2.1.1" level="project" />
+    <orderEntry type="library" name="Maven: de.codecentric:spring-boot-admin-client:2.1.1" level="project" />
+    <orderEntry type="library" name="Maven: org.jolokia:jolokia-core:1.6.2" level="project" />
+    <orderEntry type="library" name="Maven: com.googlecode.json-simple:json-simple:1.1.1" level="project" />
+    <orderEntry type="library" name="Maven: de.codecentric:spring-boot-admin-server:2.1.1" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-webflux:2.1.10.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework:spring-webflux:5.1.11.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.synchronoss.cloud:nio-multipart-parser:1.1.0" level="project" />
+    <orderEntry type="library" name="Maven: org.synchronoss.cloud:nio-stream-storage:1.1.3" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-thymeleaf:2.1.10.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.thymeleaf:thymeleaf-spring5:3.0.11.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.thymeleaf:thymeleaf:3.0.11.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.attoparser:attoparser:2.0.5.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.unbescape:unbescape:1.1.6.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.thymeleaf.extras:thymeleaf-extras-java8time:3.0.4.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: io.projectreactor.addons:reactor-extra:3.2.3.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: com.google.code.findbugs:jsr305:3.0.2" level="project" />
+    <orderEntry type="library" name="Maven: de.codecentric:spring-boot-admin-server-ui:2.1.1" level="project" />
+    <orderEntry type="library" name="Maven: io.springfox:springfox-swagger2:2.9.2" level="project" />
+    <orderEntry type="library" name="Maven: io.swagger:swagger-annotations:1.5.20" level="project" />
+    <orderEntry type="library" name="Maven: io.springfox:springfox-spi:2.9.2" level="project" />
+    <orderEntry type="library" name="Maven: io.springfox:springfox-core:2.9.2" level="project" />
+    <orderEntry type="library" name="Maven: io.springfox:springfox-schema:2.9.2" level="project" />
+    <orderEntry type="library" name="Maven: io.springfox:springfox-swagger-common:2.9.2" level="project" />
+    <orderEntry type="library" name="Maven: io.springfox:springfox-spring-web:2.9.2" level="project" />
+    <orderEntry type="library" name="Maven: com.fasterxml:classmate:1.4.0" level="project" />
+    <orderEntry type="library" name="Maven: org.slf4j:slf4j-api:1.7.29" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.plugin:spring-plugin-core:1.2.0.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.plugin:spring-plugin-metadata:1.2.0.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.mapstruct:mapstruct:1.2.0.Final" level="project" />
+    <orderEntry type="library" name="Maven: io.springfox:springfox-swagger-ui:2.9.2" level="project" />
+    <orderEntry type="library" name="Maven: io.swagger:swagger-models:1.5.21" level="project" />
+    <orderEntry type="library" name="Maven: com.fasterxml.jackson.core:jackson-annotations:2.9.10" level="project" />
+    <orderEntry type="library" name="Maven: org.projectlombok:lombok:1.18.10" level="project" />
+    <orderEntry type="library" scope="RUNTIME" name="Maven: mysql:mysql-connector-java:8.0.18" level="project" />
+    <orderEntry type="library" name="Maven: org.liquibase:liquibase-core:3.6.1" level="project" />
+    <orderEntry type="library" name="Maven: org.yaml:snakeyaml:1.23" level="project" />
+    <orderEntry type="library" name="Maven: com.github.ulisesbocchio:jasypt-spring-boot-starter:2.1.0" level="project" />
+    <orderEntry type="library" name="Maven: com.github.ulisesbocchio:jasypt-spring-boot:2.1.0" level="project" />
+    <orderEntry type="library" name="Maven: org.jasypt:jasypt:1.9.2" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.assertj:assertj-core:3.11.1" level="project" />
+    <orderEntry type="library" name="Maven: com.h2database:h2:1.4.200" level="project" />
+    <orderEntry type="library" name="Maven: io.netty:netty-all:4.1.34.Final" level="project" />
+    <orderEntry type="library" name="Maven: com.alibaba:fastjson:1.2.62" level="project" />
+    <orderEntry type="library" name="Maven: io.jsonwebtoken:jjwt:0.9.0" level="project" />
+    <orderEntry type="library" name="Maven: com.fasterxml.jackson.core:jackson-databind:2.9.10.1" level="project" />
+    <orderEntry type="library" name="Maven: com.fasterxml.jackson.core:jackson-core:2.9.10" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.poi:poi:3.17" level="project" />
+    <orderEntry type="library" name="Maven: commons-codec:commons-codec:1.11" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.commons:commons-collections4:4.1" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.poi:poi-ooxml:3.17" level="project" />
+    <orderEntry type="library" name="Maven: com.github.virtuald:curvesapi:1.04" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.poi:poi-ooxml-schemas:3.17" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.commons:commons-compress:1.19" level="project" />
+    <orderEntry type="library" name="Maven: commons-io:commons-io:2.6" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.xmlbeans:xmlbeans:2.3.0" level="project" />
+    <orderEntry type="library" name="Maven: stax:stax-api:1.0.1" level="project" />
+    <orderEntry type="library" name="Maven: commons-httpclient:commons-httpclient:3.1" level="project" />
+    <orderEntry type="library" name="Maven: commons-logging:commons-logging:1.0.4" level="project" />
+    <orderEntry type="library" name="Maven: com.github.axet:kaptcha:0.0.9" level="project" />
+    <orderEntry type="library" name="Maven: com.jhlabs:filters:2.0.235" level="project" />
+    <orderEntry type="module-library">
+      <library name="Maven: dom4j:dom4j:2.1.1">
+        <CLASSES>
+          <root url="jar://$MODULE_DIR$/lib/dom4j-2.1.1.jar!/" />
+        </CLASSES>
+        <JAVADOC />
+        <SOURCES />
+      </library>
+    </orderEntry>
+    <orderEntry type="library" name="Maven: commons-collections:commons-collections:3.2.2" level="project" />
+    <orderEntry type="library" name="Maven: commons-beanutils:commons-beanutils:1.9.4" level="project" />
+    <orderEntry type="library" name="Maven: com.google.guava:guava:28.0-jre" level="project" />
+    <orderEntry type="library" name="Maven: com.google.guava:failureaccess:1.0.1" level="project" />
+    <orderEntry type="library" name="Maven: com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava" level="project" />
+    <orderEntry type="library" name="Maven: org.checkerframework:checker-qual:2.8.1" level="project" />
+    <orderEntry type="library" name="Maven: com.google.errorprone:error_prone_annotations:2.3.2" level="project" />
+    <orderEntry type="library" name="Maven: com.google.j2objc:j2objc-annotations:1.3" level="project" />
+    <orderEntry type="library" name="Maven: org.codehaus.mojo:animal-sniffer-annotations:1.17" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.tomcat.embed:tomcat-embed-core:9.0.29" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.tomcat:tomcat-annotations-api:9.0.27" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.tomcat.embed:tomcat-embed-websocket:9.0.29" level="project" />
+  </component>
+</module>

+ 348 - 0
pom.xml

@@ -0,0 +1,348 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-parent</artifactId>
+        <version>2.1.10.RELEASE</version>
+        <relativePath/> <!-- lookup parent from repository -->
+    </parent>
+    <groupId>cn.com.sailfish</groupId>
+    <artifactId>linghang</artifactId>
+    <version>1.0.0</version>
+    <name>linghang</name>
+    <packaging>jar</packaging>
+    <description>Ling Hang project for Spring Boot</description>
+
+    <properties>
+        <java.version>1.8</java.version>
+        <start-class>cn.com.sailfish.linghang.LinghangApplication</start-class>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+        <liquibase.version>3.6.1</liquibase.version>
+        <springfox.version>2.9.2</springfox.version>
+        <SBA.version>2.1.1</SBA.version>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-jpa</artifactId>
+        </dependency>
+        <!-- war need to disable embed tomcat -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-tomcat</artifactId>
+            <scope>compile</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-amqp</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-security</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.security</groupId>
+            <artifactId>spring-security-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-reactor-netty</artifactId>
+            <version>2.1.4.RELEASE</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-autoconfigure</artifactId>
+            <version>2.1.10.RELEASE</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-websocket</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-devtools</artifactId>
+            <scope>runtime</scope>
+        </dependency>
+
+        <!-- 监控模块 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-actuator</artifactId>
+        </dependency>
+        <!-- 监控界面 -->
+        <dependency>
+            <groupId>de.codecentric</groupId>
+            <artifactId>spring-boot-admin-starter-client</artifactId>
+            <version>${SBA.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>de.codecentric</groupId>
+            <artifactId>spring-boot-admin-server</artifactId>
+            <version>${SBA.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>de.codecentric</groupId>
+            <artifactId>spring-boot-admin-server-ui</artifactId>
+            <version>${SBA.version}</version>
+        </dependency>
+
+        <!-- swagger -->
+        <dependency>
+            <groupId>io.springfox</groupId>
+            <artifactId>springfox-swagger2</artifactId>
+            <version>${springfox.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.springfox</groupId>
+            <artifactId>springfox-swagger-ui</artifactId>
+            <version>${springfox.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>io.swagger</groupId>
+                    <artifactId>swagger-models</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <!-- io.springfox:springfox-swagger2:2.9.2中依赖了swagger-models的1.5.20版本,通过排除springfox-swagger2中的swagger-models依赖,
+导入io.swagger:swagger-models的1.5.21版本.解决io.swagger.models.parameters.AbstractSerializableParameter实例化参数时example为
+空字符串""而报错的问题.因为1.5.20的example只判断是否为null,1.5.21判断了是否为null和""-->
+        <dependency>
+            <groupId>io.swagger</groupId>
+            <artifactId>swagger-models</artifactId>
+            <version>1.5.21</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+            <scope>runtime</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.liquibase</groupId>
+            <artifactId>liquibase-core</artifactId>
+            <version>${liquibase.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.github.ulisesbocchio</groupId>
+            <artifactId>jasypt-spring-boot-starter</artifactId>
+            <version>2.1.0</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.assertj</groupId>
+            <artifactId>assertj-core</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <!-- H2 Database for test and dev -->
+        <dependency>
+            <groupId>com.h2database</groupId>
+            <artifactId>h2</artifactId>
+        </dependency>
+
+        <!--创建通道-->
+        <dependency>
+            <groupId>io.netty</groupId>
+            <artifactId>netty-all</artifactId>
+            <version>4.1.34.Final</version>
+        </dependency>
+
+        <!-- JSON -->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>fastjson</artifactId>
+            <version>1.2.62</version>
+        </dependency>
+        <dependency>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt</artifactId>
+            <version>0.9.0</version>
+        </dependency>
+
+        <!-- poi excel文件信息的导入-->
+        <dependency>
+            <groupId>org.apache.poi</groupId>
+            <artifactId>poi</artifactId>
+            <version>3.17</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.poi</groupId>
+            <artifactId>poi-ooxml</artifactId>
+            <version>3.17</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.poi</groupId>
+            <artifactId>poi-ooxml-schemas</artifactId>
+            <version>3.17</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-compress</artifactId>
+            <version>1.19</version>
+        </dependency>
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+            <version>2.6</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.xmlbeans</groupId>
+            <artifactId>xmlbeans</artifactId>
+            <version>2.3.0</version>
+        </dependency>
+        <dependency>
+            <groupId>commons-httpclient</groupId>
+            <artifactId>commons-httpclient</artifactId>
+            <version>3.1</version>
+        </dependency>
+        <dependency>
+            <groupId>com.github.axet</groupId>
+            <artifactId>kaptcha</artifactId>
+            <version>0.0.9</version>
+        </dependency>
+        <dependency>
+            <groupId>dom4j</groupId>
+            <artifactId>dom4j</artifactId>
+            <version>2.1.1</version>
+            <scope>system</scope>
+            <systemPath>${project.basedir}/lib/dom4j-2.1.1.jar</systemPath>
+        </dependency>
+        <dependency>
+            <groupId>commons-collections</groupId>
+            <artifactId>commons-collections</artifactId>
+            <version>3.2.2</version>
+        </dependency>
+        <dependency>
+            <groupId>commons-beanutils</groupId>
+            <artifactId>commons-beanutils</artifactId>
+            <version>1.9.4</version>
+        </dependency>
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>28.0-jre</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.tomcat.embed</groupId>
+            <artifactId>tomcat-embed-core</artifactId>
+            <version>9.0.29</version>
+            <!--      <scope>provided</scope>-->
+        </dependency>
+        <dependency>
+            <groupId>org.apache.tomcat.embed</groupId>
+            <artifactId>tomcat-embed-websocket</artifactId>
+<!--            <scope>provided</scope>-->
+            <version>9.0.29</version>
+<!--            <scope>system</scope>
+            <systemPath>${project.basedir}/lib/tomcat-embed-websocket-9.0.22.jar</systemPath>-->
+        </dependency>
+    </dependencies>
+
+    <build>
+        <defaultGoal>spring-boot:run</defaultGoal>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <configuration>
+                    <includeSystemScope>true</includeSystemScope>
+                </configuration>
+                <executions>
+                    <!-- 构建信息 -->
+                    <execution>
+                        <goals>
+                            <goal>build-info</goal>
+                        </goals>
+                        <configuration>
+                            <additionalProperties>
+                                <encoding.source>UTF-8</encoding.source>
+                                <encoding.reporting>UTF-8</encoding.reporting>
+                                <java.source>${maven.compiler.source}</java.source>
+                                <java.target>${maven.compiler.target}</java.target>
+                            </additionalProperties>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <!-- git 的版本信息 -->
+            <plugin>
+                <groupId>pl.project13.maven</groupId>
+                <artifactId>git-commit-id-plugin</artifactId>
+            </plugin>
+            <plugin>
+                <groupId>org.liquibase</groupId>
+                <artifactId>liquibase-maven-plugin</artifactId>
+                <version>${liquibase.version}</version>
+                <configuration>
+                    <propertyFile>src/main/resources/liquibase.properties</propertyFile>
+                </configuration>
+            </plugin>
+            <!-- 单元测试覆盖率 -->
+            <plugin>
+                <groupId>org.jacoco</groupId>
+                <artifactId>jacoco-maven-plugin</artifactId>
+                <version>0.8.0</version>
+                <executions>
+                    <execution>
+                        <id>default-prepare-agent</id>
+                        <goals>
+                            <goal>prepare-agent</goal>
+                        </goals>
+                    </execution>
+                    <execution>
+                        <id>default-report</id>
+                        <phase>prepare-package</phase>
+                        <goals>
+                            <goal>report</goal>
+                        </goals>
+                    </execution>
+                    <execution>
+                        <id>jacoco-check</id>
+                        <goals>
+                            <goal>check</goal>
+                        </goals>
+                        <configuration>
+                            <rules>
+                                <rule>
+                                    <element>PACKAGE</element>
+                                    <limits>
+                                        <limit>
+                                            <counter>LINE</counter>
+                                            <value>COVEREDRATIO</value>
+                                            <minimum>0.85</minimum>
+                                        </limit>
+                                    </limits>
+                                </rule>
+                            </rules>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 6 - 0
sql/20200409-create_view.sql

@@ -0,0 +1,6 @@
+CREATE OR REPLACE VIEW v_select_all_from_crux AS
+SELECT c.id, c.course_title AS title, c.image_url, c.gmt_created, c.intro, "" AS classtype , "" AS content, "" AS qrcode_url , "course" AS hard_code FROM course_introduce c WHERE c.is_del = '0'
+UNION ALL
+SELECT n.id, n.new_title AS title, n.image_url, n.gmt_created, n.intro, n.classtype, n.content, "" AS qrcode_url , "newspaper" AS hard_code FROM new_papers n WHERE n.is_del = '0'
+UNION ALL
+SELECT v.id, v.vr_title AS title, v.image_url, v.gmt_created, "" AS intro, "" AS classtype, "" AS content, v.qrcode_url , "vrdetails" AS hard_code FROM vr_details v WHERE v.is_del = '0'

+ 198 - 0
src/main/java/cn/com/sailfish/linghang/GenTable.java

@@ -0,0 +1,198 @@
+package cn.com.sailfish.linghang;
+
+import io.swagger.annotations.ApiModelProperty;
+
+import javax.persistence.Id;
+import javax.persistence.Table;
+import java.beans.Transient;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.List;
+
+/**
+ * @author liangjunxing
+ * @date 2022/7/6 0006
+ */
+public class GenTable {
+    /***
+     * 根据实体类自动生成Mysql建表sql
+     * @param beanName  实体类路径
+     * @param isConvert  是否需要转换驼峰格式
+     * @return
+     */
+    public String generateTableMysql(String beanName,boolean isConvert) {
+        StringBuilder sqlSb = new StringBuilder();
+
+        if (null != beanName && !"".equals(beanName)) {
+            Object obj = null;
+            try {
+                obj = Class.forName(beanName).newInstance();
+            } catch (InstantiationException e) {
+                e.printStackTrace();
+            } catch (IllegalAccessException e) {
+                e.printStackTrace();
+            } catch (ClassNotFoundException e) {
+                e.printStackTrace();
+            }
+            // 拿到该类
+            Class<?> clz = obj.getClass();
+            // 获取实体类的所有属性,返回Field数组
+            Field[] fields = clz.getDeclaredFields();
+            if (fields != null) {
+
+                //获取实体类的Table注解表名(导入persistence包)
+                Table tableClass = clz.getAnnotation(Table.class);
+                String tableName = "";
+                if (tableClass!=null){
+                    System.out.println("表名:"+tableClass.name());
+                    tableName=tableClass.name();
+                }
+                sqlSb.append("drop table if exists "+tableName+"; \n");
+
+                sqlSb.append("create table "+tableName+"( \n");
+
+                String idKey = "";
+
+                for (Field field : fields) {
+                    if (!field.isAccessible()) {
+                        field.setAccessible(true);
+                    }
+                    String type = field.getGenericType().toString();
+                    System.out.println("类的属性类型全称:" + type);
+                    if (type.indexOf(".") == -1 || type.indexOf("java.") != -1) {//过滤实体对象
+                        //截取最后一个.后面的字符
+                        type = type.substring(type.lastIndexOf(".") + 1);
+                        System.out.println("对象属性类型:" + type);
+                        if (!field.getType().equals(List.class)) {// 不匹配list类型
+                            //字段名称
+                            String name = field.getName();
+
+                            Method doSomeMethod = null;
+                            try {
+                                //获取get方法
+                                doSomeMethod = clz.getDeclaredMethod("get" + name.substring(0, 1).toUpperCase() + name.substring(1));
+                            } catch (NoSuchMethodException e) {
+                                e.printStackTrace();
+                            }
+                            boolean isExist = true;
+                            //判断该方法上是否存在这个注解
+                            /*if (doSomeMethod.isAnnotationPresent(Transient.class)) {
+                                Transient aTransient = doSomeMethod.getAnnotation(Transient.class);
+                                System.out.println(name+"==>对象不属于表字段:"+aTransient);
+                                isExist =false;
+                            }
+                            //判断该字段上是否存在这个注解
+                            if (field.isAnnotationPresent(Transient.class)) {
+                                Transient aTransient = field.getAnnotation(Transient.class);
+                                System.out.println(name+"==>对象不属于表字段:"+aTransient);
+                                isExist =false;
+                            }*/
+                            //(Transient注解是导入persistence包)
+                            if (field.isAnnotationPresent(Transient.class) ||(null!=doSomeMethod  && doSomeMethod.isAnnotationPresent(Transient.class))) {
+                                System.out.println(name+"==>对象不属于表字段");
+                                isExist =false;
+                            }
+                            //属于表字段
+                            if(isExist){
+                                StringBuilder convertName = new StringBuilder();
+                                convertName.append(name);
+                                //如果需要转换驼峰格式
+                                if (isConvert) {
+                                    convertName = new StringBuilder();
+                                    for (int i = 0; i < name.length(); i++) {
+                                        //如果是大写前面先加一个_
+                                        if(isUpperCase(name.charAt(i))){
+                                            convertName.append("_");
+                                        }
+                                        convertName.append(name.charAt(i));
+                                    }
+                                }
+                                name=convertName.toString();
+                                sqlSb.append("    "+name.toUpperCase()+" ");
+                                //java 数据类型转换成 Oracle 字段数据类型
+                                sqlSb.append(" "+societyMysql(type));
+                                //判断该字段是否是Id主键(导入persistence包)
+                                if(field.isAnnotationPresent(Id.class)){
+                                    Id id = field.getAnnotation(Id.class);
+                                    idKey=name.toUpperCase();//id主键字段
+                                    System.out.println("id主键字段:"+idKey);
+                                    sqlSb.append("  NOT NULL  " );
+                                }
+                                sqlSb.append(" comment " );
+                                //字段属性说明(没有ApiModelProperty包的把这段代码注释掉)
+                                if(field.isAnnotationPresent(ApiModelProperty.class)){
+                                    ApiModelProperty explain = field.getAnnotation(ApiModelProperty.class);
+                                    System.out.println("字段说明:"+explain.value());
+                                    sqlSb.append(" '"+explain.value()+"',\n" );
+                                }else{
+                                    sqlSb.append(" '',\n" );
+                                }
+
+                                /**
+                                 * 字段属性说明
+                                 * 没有ApiModelProperty包的把上面那段代码注释掉,用这个
+                                 */
+                                /*
+                                sqlSb.append(" '',\n" );
+                                */
+
+                            }
+
+                        }
+                    }
+
+                }
+                if(null!=idKey && !"".equals(idKey) ){
+                    sqlSb.append(" primary key ("+idKey+") \n" );
+                }else{
+                    String lastStr = sqlSb.substring(0, sqlSb.length() - 1);
+                    //如果最后一个字符是逗号结尾
+                    if(lastStr.equals(",")){
+                        //删除最后一个字符
+                        sqlSb=sqlSb.deleteCharAt(sqlSb.length() - 1);
+                    }
+                }
+                sqlSb.append(" ); \n" );
+
+            }
+        }
+
+        return sqlSb.toString();
+    }
+
+    //字母是否是大写
+    public boolean isUpperCase(char c) {
+        return c >=65 && c <= 90;
+    }
+
+    public String societyMysql(String javaType){
+        String oracleType ="";
+        if(javaType.equals("String")){
+            oracleType="varchar(255)";
+        }
+        //不区分大小写
+        else if(javaType.equalsIgnoreCase("BigDecimal") ||javaType.equalsIgnoreCase("short")
+                ||javaType.equalsIgnoreCase("boolean")||javaType.equalsIgnoreCase("long"))
+        {
+            oracleType="decimal(6)";
+        }else if(javaType.equals("Date")){
+            oracleType="date";
+        }else if(javaType.equalsIgnoreCase("Timestamp")){
+            oracleType="datetime";
+        }else if(javaType.equals("byte[]")){
+            oracleType="blob";
+        }else if(javaType.equals("int")){
+            oracleType="int";
+        }else if(javaType.equals("Integer")){
+            oracleType="integer";
+        }else if(javaType.equalsIgnoreCase("double")){
+            oracleType="double";
+        }else if(javaType.equalsIgnoreCase("float")){
+            oracleType="float";
+        }
+        return oracleType;
+    }
+
+
+
+}

+ 190 - 0
src/main/java/cn/com/sailfish/linghang/GetGen.java

@@ -0,0 +1,190 @@
+package cn.com.sailfish.linghang;
+
+import liquibase.sqlgenerator.SqlGenerator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author liangjunxing
+ * @date 2022/7/6 0006
+ */
+public class GetGen {
+
+    /**
+     * 根据 java 类自动生成sql 语句 - mysql
+     */
+
+    public static Map javaProperty2SqlColumnMap = new HashMap();
+
+    static {
+        javaProperty2SqlColumnMap.put("Integer", "int");
+        javaProperty2SqlColumnMap.put("Short", "tinyint(4)");
+        javaProperty2SqlColumnMap.put("Long", "bigint");
+        javaProperty2SqlColumnMap.put("BigDecimal", "decimal(20,4)");
+        javaProperty2SqlColumnMap.put("Double", "double precision not null");
+        javaProperty2SqlColumnMap.put("Float", "float");
+        javaProperty2SqlColumnMap.put("boolean", "tinyint(4)");
+        javaProperty2SqlColumnMap.put("Timestamp", "datetime");
+        javaProperty2SqlColumnMap.put("Date", "datetime");
+        javaProperty2SqlColumnMap.put("String", "varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL");
+    }
+
+    private static final Logger logger = LoggerFactory.getLogger(SqlGenerator.class);
+
+    public static void main(String[] args) throws ClassNotFoundException {
+        //实体类所在的package在磁盘上的绝对路径
+        String packageName = "C:\\Users\\Administrator\\Desktop\\原动力\\高新\\代码\\实训平台\\csair\\csair\\csair-vr-portal-back-end\\src\\main\\java\\cn\\com\\sailfish\\linghang\\domain";
+        //生成sql的文件夹
+        //String filePath = "G:\\SQL";
+        //项目中实体类的路径
+        String prefix = "cn.com.sailfish.linghang.domain.";
+        String className = "";
+
+        StringBuffer sqls = new StringBuffer();
+        //获取包下的所有类名称
+        List<String> list = getAllClasses(packageName);
+        for (int i = 0; i<list.size(); i++) {
+            String str = list.get(i);
+            if (str.indexOf(".") != -1) {
+                className = prefix + str.substring(0, str.lastIndexOf("."));
+                String sql = generateSql(className, "");
+                sqls.append(sql);
+            }
+        }
+        System.out.println(sqls.toString());
+        //StringToSql(sqls.toString(), filePath + "report.sql");
+
+    }
+
+    /**
+     * 根据实体类生成建表语句
+     *
+     * @param className 全类名
+     * @param filePath  磁盘路径  如 : d:/workspace/
+     * @author
+     * @date
+     */
+    public static String generateSql(String className, String tableName) throws ClassNotFoundException {
+        Class v = Class.forName(className);
+        try {
+            Class clz = Class.forName(className);
+            className = clz.getSimpleName();
+            Field[] fields = clz.getDeclaredFields();
+            String param = "";
+            String column = "";
+            StringBuilder sql = null;
+            sql = new StringBuilder(50);
+            if (tableName == null || ("").equals(tableName)) {
+                tableName = clz.getName();
+                tableName = tableName.substring(tableName.lastIndexOf(".") + 1);
+            }
+
+            sql.append("\n\n/*========= " + tableName + " ==========*/\n");
+            sql.append("DROP TABLE IF EXISTS `" + className + "`; \n");
+            sql.append("CREATE TABLE `").append(tableName).append("` ( \n");
+
+            String keyField = "";
+            for (Field f : fields) {
+                column = f.getName();
+                if (("serialVersionUID").equals(column)) {
+                    continue;
+                }
+                param = f.getType().getSimpleName();
+
+                //将大写字母转小写,并添加下划线
+                for (int i = 0; i < column.length(); i++) {
+                    char c = column.charAt(i);
+                    if (Character.isUpperCase(c) && i > 0) {
+                        column = column.replaceAll(
+                                Character.toString(c), "_" + Character.toLowerCase(c));
+                    }
+                }
+
+                sql.append("`" + column + "`");
+                sql.append(" ").append(javaProperty2SqlColumnMap.get(param));
+                //默认第一个是主键
+                if ("".equals(keyField)) {
+                    keyField = column;
+                }
+                sql.append(",\n");
+            }
+            sql.append("PRIMARY KEY (`" + keyField + "`) USING BTREE,");
+            sql.append("\nINDEX `" + keyField + "`(`" + keyField + "`) USING BTREE ) ");
+            sql.append("\nENGINE = INNODB DEFAULT CHARSET= utf8;");
+
+            return sql.toString();
+
+        } catch (ClassNotFoundException e) {
+            logger.debug("该类未找到!");
+            return null;
+        }
+    }
+
+    /**
+     * 获取包下的所有类名称,获取的结果类似于 XXX.java
+     *
+     * @param packageName
+     * @return
+     * @author
+     * @date
+     */
+    public static List getAllClasses(String packageName) {
+        List classList = new ArrayList();
+        String className = "";
+        File f = new File(packageName);
+        if (f.exists() && f.isDirectory()) {
+            File[] files = f.listFiles();
+            for (File file : files) {
+                className = file.getName();
+                classList.add(className);
+            }
+            return classList;
+        } else {
+            logger.debug("包路径未找到!");
+            return null;
+        }
+    }
+
+    /**
+     * 将string 写入sql文件
+     *
+     * @param str
+     * @param path
+     * @author
+     * @date
+     */
+    public static void StringToSql(String str, String path) {
+        byte[] sourceByte = str.getBytes();
+        if (null != sourceByte) {
+            try {
+                //文件路径(路径+文件名)
+                File file = new File(path);
+                //文件不存在则创建文件,先创建目录
+                if (!file.exists()) {
+                    File dir = new File(file.getParent());
+                    dir.mkdirs();
+                    file.createNewFile();
+                }
+                //文件输出流用于将数据写入文件
+                FileOutputStream outStream = new FileOutputStream(file);
+                outStream.write(sourceByte);
+                outStream.flush();
+                outStream.close();
+                System.out.println("生成成功");
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+    }
+}
+
+
+

+ 67 - 0
src/main/java/cn/com/sailfish/linghang/LinghangApplication.java

@@ -0,0 +1,67 @@
+package cn.com.sailfish.linghang;
+
+import de.codecentric.boot.admin.server.config.EnableAdminServer;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.boot.web.servlet.MultipartConfigFactory;
+import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.env.Environment;
+
+import javax.servlet.MultipartConfigElement;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+@Slf4j
+@EnableAdminServer
+@Configuration
+@SpringBootApplication
+public class LinghangApplication extends SpringBootServletInitializer {
+
+    // 重载SpringBootServletInitializer.configure是为了打包成war包后能在tomcat中顺利运行
+    @Override
+    protected SpringApplicationBuilder configure(SpringApplicationBuilder app) {
+        return app.sources(LinghangApplication.class);
+    }
+
+    public static void main(String[] args) throws UnknownHostException {
+        SpringApplication app = new SpringApplication(LinghangApplication.class);
+        Environment env = app.run(args).getEnvironment();
+        String protocol = "http";
+        if (env.getProperty("server.ssl.key-store") != null) {
+            protocol = "https";
+        }
+        log.info("\n----------------------------------------------------------\n\t" +
+                        "Application '{}' is running! Access URLs:\n\t" +
+                        "Local: \t\t{}://localhost:{}{}\n\t" +
+                        "External: \t{}://{}:{}{}\n\t" +
+                        "Profile(s): \t{}\n----------------------------------------------------------",
+                env.getProperty("spring.application.name"),
+                protocol, env.getProperty("server.port"), env.getProperty("server.servlet.context-path"),
+                protocol, InetAddress.getLocalHost().getHostAddress(), env.getProperty("server.port"), env.getProperty("server.servlet.context-path"),
+                env.getActiveProfiles()
+        );
+
+        String configServerStatus = env.getProperty("configserver.status");
+        log.info("\n-------------------------- spring cloud --------------------------------\n\t" +
+                        "Config Server: \t{}\n----------------------------------------------------------",
+                configServerStatus == null ? "Not found or not setup for this application" : configServerStatus);
+    }
+
+    /**
+     * 文件上传配置
+     * @return
+     */
+    @Bean
+    public MultipartConfigElement multipartConfigElement() {
+        MultipartConfigFactory factory = new MultipartConfigFactory();
+        //单个文件最大
+        factory.setMaxFileSize("1024MB"); //KB,MB
+        /// 设置总上传数据总大小
+        factory.setMaxRequestSize("1024MB");
+        return factory.createMultipartConfig();
+    }
+}

+ 13 - 0
src/main/java/cn/com/sailfish/linghang/common/Constants.java

@@ -0,0 +1,13 @@
+package cn.com.sailfish.linghang.common;
+
+/**
+ * Author: wengxinyou
+ * Create at: 2018/12/27
+ * Description:  存储全局变量类
+ */
+public class Constants {
+
+    public final static String GMT_CREATED = "gmtCreated"; // 排序字段 创建时间
+    public final static String GMT_MODIFIED = "gmtModified"; // 排序字段 修改时间
+
+}

+ 554 - 0
src/main/java/cn/com/sailfish/linghang/common/ErrorConstants.java

@@ -0,0 +1,554 @@
+package cn.com.sailfish.linghang.common;
+
+/**
+ * Author: huangpeilin
+ * Create at: 2018-12-12 11:43:08
+ * Description:
+ *
+ * @author huangpeilin
+ */
+public class ErrorConstants {
+
+    //========================= 全局错误码 =============================
+
+    public static final int SUCCESS = 0x00000000;
+
+    public static final int ERR_BAD_PARAM = 0x00000001;
+
+    public static final int ERR_FORM_PARAMETER_ERROR = 0x00000002;
+
+    public static final int FILE_UPLOAD_FAILED = 0x00000003;
+
+    public static final int BIND_FAILED = 0x00000004;
+
+    /**
+     * 验证码到期失效
+     */
+    public static final int CAPTCHA_CODE_EXPIRES = 0x00000005;
+
+    /**
+     * 验证码错误
+     */
+    public static final int CAPTCHA_CODE_ERROR = 0x00000006;
+
+    /**
+     * 转化byte数据错误
+     */
+    public static final int GET_BYTE_ERROR = 0x00000007;
+
+    /**
+     * 不是数字的字符串
+     */
+    public static final int NUMBER_IS_ERROR = 0x00000008;
+
+    /**
+     * 验证码为空
+     */
+    public static final int CAPTCHA_CODE_IS_NULL = 0x00000009;
+
+    /**
+     * referer错误
+     */
+    public static final int REFERER_IS_ERROR = 0x0000000A;
+
+    public static final int ERR_INTERNAL_EXCEPTION = 0xfffffffE;
+
+    public static final int ERR_UNKNOWN = 0xffffffff;
+
+
+    //========================= 模块错误码 ===============================
+    //                   模块定义从0x00010000开始
+    /**
+     * 用户模块
+     */
+    private static final int USER_BASE_ERROR_SEGMENT = 0x00010000;
+    /**
+     * 文件模块
+     */
+    private static final int FILE_BASE_ERROR_SEGMENT = 0x00020000;
+
+    /**
+     * 区域模块
+     */
+    private static final int AREA_BASE_ERROR_SEGMENT = 0x00030000;
+
+    /**
+     * 机型模块
+     */
+    private static final int MODEL_BASE_ERROR_SEGMENT = 0x00040000;
+
+    /**
+     * 发动机模块
+     */
+    private static final int ENGINE_BASE_ERROR_SEGMENT = 0x00050000;
+
+    /**
+     * 课程模块
+     */
+    private static final int COURSE_BASE_ERROR_SEGMENT = 0x00060000;
+
+    /**
+     * 试卷模块
+     */
+    private static final int QUESTION_PAPER_BASE_ERROR_SEGMENT = 0x00070000;
+
+    /**
+     * 考试模块
+     */
+    private static final int EXAM_BASE_ERROR_SEGMENT = 0x00080000;
+
+    /**
+     * 成绩单模块
+     */
+    private static final int REPORT_CARD_BASE_ERROR_SEGMENT = 0x00090000;
+    /**
+     * 部门模块
+     */
+    private static final int ORG_UNIT_BASE_ERROR_SEGMENT = 0x00001000;
+    //==========================用户模块==============================
+
+    /**
+     * 用户卡号为空
+     */
+    public static final int USER_CARD_IS_NULL = USER_BASE_ERROR_SEGMENT | 1;
+
+    /**
+     * 用户类型为空
+     */
+    public static final int USER_TYPE_IS_NULL = USER_BASE_ERROR_SEGMENT | 2;
+
+    /**
+     * 用户手机号码为空
+     */
+    public static final int MOBILE_PHONE_IS_NULL = USER_BASE_ERROR_SEGMENT | 3;
+
+    /**
+     * 手机号码的格式错误
+     */
+    public static final int MOBILE_PHONE_IS_ERROR_FORMAT = USER_BASE_ERROR_SEGMENT | 4;
+
+    /**
+     * 用户电子邮箱为空
+     */
+    public static final int EMAIL_IS_NULL = USER_BASE_ERROR_SEGMENT | 5;
+
+    /**
+     * 电子邮箱的格式错误
+     */
+    public static final int EMAIL_IS_ERROR_FORMAT = USER_BASE_ERROR_SEGMENT | 6;
+
+    /**
+     * 用户名为空
+     */
+    public static final int USERNAME_IS_NULL = USER_BASE_ERROR_SEGMENT | 7;
+
+    /**
+     * 已经存在使用该手机号码的用户
+     */
+    public static final int USER_MOBILE_PHONE_IS_ALREADY_EXISTS = USER_BASE_ERROR_SEGMENT | 8;
+
+    /**
+     * 已经存在使用该电子邮箱的用户
+     */
+    public static final int USER_EMAIL_IS_ALREADY_EXISTS = USER_BASE_ERROR_SEGMENT | 9;
+
+    /**
+     * 用户类型出错
+     */
+    public static final int USER_TYPE_IS_ERROR = USER_BASE_ERROR_SEGMENT | 10;
+
+    /**
+     * 用户登录账号为空
+     */
+    public static final int USER_LOGIN_ACCOUNT_IS_NULL = USER_BASE_ERROR_SEGMENT | 11;
+
+    /**
+     * 用户密码为空
+     */
+    public static final int USER_PASSWORD_IS_NULL = USER_BASE_ERROR_SEGMENT | 12;
+
+    /**
+     * 用户不存在
+     */
+    public static final int USER_IS_NOT_EXISTS = USER_BASE_ERROR_SEGMENT | 13;
+
+    /**
+     * 用户账号没有被激活,在回收站中
+     */
+    public static final int USER_IS_NOT_ACTIVATED = USER_BASE_ERROR_SEGMENT | 14;
+
+    /**
+     * 登录用户不是管理员或者教员
+     */
+    public static final int CAN_NOT_OPERATE_WITHOUT_ADMIN_OR_TEACHER = USER_BASE_ERROR_SEGMENT | 15;
+
+    /**
+     * 密码错误authority is error.
+     */
+    public static final int USER_PASSWORD_IS_ERROR = USER_BASE_ERROR_SEGMENT | 16;
+
+    /**
+     * 用户权限值出错
+     */
+    public static final int USER_AUTHORITY_IS_ERROR = USER_BASE_ERROR_SEGMENT | 17;
+
+    /**
+     * 用户已经存在
+     */
+    public static final int USER_IS_ALREADY_EXISTS = USER_BASE_ERROR_SEGMENT | 18;
+
+    /**
+     * 用户在考试队列中
+     */
+    public static final int USER_IS_IN_TEST_QUEUE = USER_BASE_ERROR_SEGMENT | 19;
+
+    /**
+     * 用户权限不足
+     */
+    public static final int INSUFFICIENT_USER_PERMISSION = USER_BASE_ERROR_SEGMENT | 20;
+
+    /**
+     * 用户跨区操作
+     */
+    public static final int USER_OPERATE_ACROSS_REGION = USER_BASE_ERROR_SEGMENT | 21;
+
+    /**
+     * 用户ID为空
+     */
+    public static final int USER_ID_IS_NULL = USER_BASE_ERROR_SEGMENT | 22;
+
+    /**
+     * 用户登录的队列名为空
+     */
+    public static final int USER_QUEUE_NAME_IS_NULL = USER_BASE_ERROR_SEGMENT | 23;
+
+    /**
+     * 用户在异地登录
+     */
+    public static final int USER_REMOTE_LOGIN = USER_BASE_ERROR_SEGMENT | 24;
+
+    /**
+     * 请求sso系统失败
+     */
+    public static final int REQUEST_SSO_SERVER_FAILED = USER_BASE_ERROR_SEGMENT | 25;
+
+    /**
+     * 这个权限的用户只能存在一个
+     */
+    public static final int THIS_USER_PRIVILEGE_ONLY_ONE_CAN_EXIST = USER_BASE_ERROR_SEGMENT | 26;
+
+    /**
+     * 用户权限不足
+     */
+    public static final int USER_PERMISSION_DENIED = USER_BASE_ERROR_SEGMENT | 27;
+
+    /**
+     * sso系统不存在该数据失败
+     */
+    public static final int SSO_SERVER_CAN_NOT_FIND_THIS_DATA = USER_BASE_ERROR_SEGMENT | 28;
+
+    /**
+     * 用户账号被锁定
+     */
+    public static final int USER_ACCOUNT_IS_LOCK = USER_BASE_ERROR_SEGMENT | 29;
+
+    /**
+     * 用户名或密码错误,认证失败
+     */
+    public static final int USER_ACCOUNT_IS_ERROR = USER_BASE_ERROR_SEGMENT | 30;
+    /**
+     * 触发双因子验证
+     */
+    public static final int USER_ACCOUNT_IS_VERIFY = USER_BASE_ERROR_SEGMENT | 31;
+    /**
+     * IP列入黑名单
+     */
+    public static final int USER_SEND_MESSAGE_IPERROR = USER_BASE_ERROR_SEGMENT | 32;
+    /**
+     * token信息不存在或者已过期不合法等
+     */
+    public static final int USER_SEND_MESSAGE_TOKENERROR = USER_BASE_ERROR_SEGMENT | 33;
+    /**
+     * 双因子认证已关闭,请求失败
+     */
+    public static final int USER_SEND_MESSAGE_DFACTORERROR = USER_BASE_ERROR_SEGMENT | 34;
+    /**
+     * 发送验证码失败
+     */
+    public static final int USER_SEND_MESSAGE_FAILURE = USER_BASE_ERROR_SEGMENT | 35;
+
+    //==========================文件模块==============================
+
+    /**
+     * 导入的信息文件不是excel文件
+     */
+    public static final int FILE_IS_NOT_THE_EXCEL = FILE_BASE_ERROR_SEGMENT | 1;
+
+    /**
+     * 导入文件信息失败
+     */
+    public static final int IMPORT_INFO_IS_FAIL = FILE_BASE_ERROR_SEGMENT | 2;
+
+    /**
+     *  文件操作异常
+     */
+    public static final int FILE_OPERATING_EXCEPTION = FILE_BASE_ERROR_SEGMENT | 3;
+
+    /**
+     *  文件不存在
+     */
+    public static final int  FILE_IS_NOT_EXISTS = USER_BASE_ERROR_SEGMENT | 4;
+
+    /**
+     *  文件不是图片
+     */
+    public static final int  FILE_IS_NOT_IMAGE = USER_BASE_ERROR_SEGMENT | 5;
+
+    /**
+     *  图片太大
+     */
+    public static final int  FILE_IS_TOO_BIG = USER_BASE_ERROR_SEGMENT | 6;
+
+    //==========================区域模块==============================
+
+    /**
+     * 区域名字为空
+     */
+    public static final int AREA_NAME_IS_NULL = AREA_BASE_ERROR_SEGMENT | 1;
+    /**
+     * 区域已经存在
+     */
+    public static final int AREA_IS_ALREADY_EXISTS = AREA_BASE_ERROR_SEGMENT | 2;
+    /**
+     * 区域不存在
+     */
+    public static final int AREA_IS_NOT_EXISTS = AREA_BASE_ERROR_SEGMENT | 3;
+    /**
+     * 区域ID为空
+     */
+    public static final int AREA_ID_IS_NULL = AREA_BASE_ERROR_SEGMENT | 4;
+
+    //==========================机型模块==============================
+
+    /**
+     * 机型名字为空
+     */
+    public static final int MODEL_NAME_IS_NULL = MODEL_BASE_ERROR_SEGMENT | 1;
+    /**
+     * 机型已经存在
+     */
+    public static final int MODEL_IS_ALREADY_EXISTS = MODEL_BASE_ERROR_SEGMENT | 2;
+    /**
+     * 机型不存在
+     */
+    public static final int MODEL_IS_NOT_EXISTS = MODEL_BASE_ERROR_SEGMENT | 3;
+    /**
+     * 机型ID为空
+     */
+    public static final int MODEL_ID_IS_NULL = MODEL_BASE_ERROR_SEGMENT | 4;
+
+    //==========================发动机模块==============================
+
+    /**
+     * 发动机名字为空
+     */
+    public static final int ENGINE_NAME_IS_NULL = ENGINE_BASE_ERROR_SEGMENT | 1;
+    /**
+     * 发动机已经存在
+     */
+    public static final int ENGINE_IS_ALREADY_EXISTS = ENGINE_BASE_ERROR_SEGMENT | 2;
+    /**
+     * 发动机不存在
+     */
+    public static final int ENGINE_IS_NOT_EXISTS = ENGINE_BASE_ERROR_SEGMENT | 3;
+    /**
+     * 发动机ID为空
+     */
+    public static final int ENGINE_ID_IS_NULL = ENGINE_BASE_ERROR_SEGMENT | 4;
+
+    //==========================课程模块==============================
+    /**
+     * 课程已经存在
+     */
+    public static final int COURSE_IS_ALREADY_EXISTS = COURSE_BASE_ERROR_SEGMENT | 1;
+    /**
+     * 课程不存在
+     */
+    public static final int COURSE_IS_NOT_EXISTS = COURSE_BASE_ERROR_SEGMENT | 2;
+    /**
+     * 课程ID为空
+     */
+    public static final int COURSE_ID_IS_NULL = COURSE_BASE_ERROR_SEGMENT | 3;
+    /**
+     * 检查项ID为空
+     */
+    public static final int ITEM_ID_IS_NULL = COURSE_BASE_ERROR_SEGMENT | 4;
+    /**
+     * 检查项名字为空
+     */
+    public static final int ITEM_NAME_IS_NULL = COURSE_BASE_ERROR_SEGMENT | 5;
+    /**
+     * 检查项中文名字为空
+     */
+    public static final int ITEM_CHINESE_NAME_IS_NULL = COURSE_BASE_ERROR_SEGMENT | 6;
+    /**
+     * 检查项区域名字为空
+     */
+    public static final int ITEM_REGION_IS_NULL = COURSE_BASE_ERROR_SEGMENT | 7;
+    /**
+     * 检查项机型为空
+     */
+    public static final int ITEM_MODEL_IS_NULL = COURSE_BASE_ERROR_SEGMENT | 8;
+    /**
+     * 检查项发动机为空
+     */
+    public static final int ITEM_ENGINE_IS_NULL = COURSE_BASE_ERROR_SEGMENT | 9;
+    /**
+     * 课程名字为空
+     */
+    public static final int COURSE_NAME_IS_NULL = COURSE_BASE_ERROR_SEGMENT | 10;
+    /**
+     * 课程中文名字为空
+     */
+    public static final int COURSE_CHINESE_NAME_IS_NULL = COURSE_BASE_ERROR_SEGMENT | 11;
+    /**
+     * 课程机型为空
+     */
+    public static final int COURSE_MODEL_IS_NULL = COURSE_BASE_ERROR_SEGMENT | 12;
+    /**
+     * 课程发动机为空
+     */
+    public static final int COURSE_ENGINE_IS_NULL = COURSE_BASE_ERROR_SEGMENT | 13;
+    /**
+     * 课程职位为空
+     */
+    public static final int COURSE_DIVISION_IS_NULL = COURSE_BASE_ERROR_SEGMENT | 14;
+    /**
+     * 导入表中飞机制造商项为空
+     */
+    public static final int AIR_FRAMER_IS_NULL = COURSE_BASE_ERROR_SEGMENT | 15;
+    /**
+     * 导入表中职位数值错误
+     */
+    public static final int DIVISION_IS_ERROR = COURSE_BASE_ERROR_SEGMENT | 16;
+
+
+    //==========================试卷模块==============================
+    /**
+     * 试卷已经存在
+     */
+    public static final int QUESTION_PAPER_IS_ALREADY_EXISTS = QUESTION_PAPER_BASE_ERROR_SEGMENT | 1;
+    /**
+     * 试卷不存在
+     */
+    public static final int QUESTION_PAPER_IS_NOT_EXISTS = QUESTION_PAPER_BASE_ERROR_SEGMENT | 2;
+    /**
+     * 试卷标题为空
+     */
+    public static final int QUESTION_PAPER_TITLE_IS_NULL = QUESTION_PAPER_BASE_ERROR_SEGMENT | 3;
+    /**
+     * 试卷考试时长为空
+     */
+    public static final int QUESTION_PAPER_TEST_LENGTH_IS_NULL = QUESTION_PAPER_BASE_ERROR_SEGMENT | 4;
+    /**
+     * 试卷人为设定异常数量为空
+     */
+    public static final int QUESTION_PAPER_RANDOM_ABNORMAL_IS_NULL = QUESTION_PAPER_BASE_ERROR_SEGMENT | 5;
+    /**
+     * 试卷异常生成类型错误
+     */
+    public static final int QUESTION_PAPER_EXCEPTION_GENERATE_TYPE_IS_ERROR = QUESTION_PAPER_BASE_ERROR_SEGMENT | 6;
+    /**
+     * 试卷删除失败
+     */
+    public static final int QUESTION_PAPER_DELETE_FAILED = QUESTION_PAPER_BASE_ERROR_SEGMENT | 7;
+    /**
+     * 试卷ID为空
+     */
+    public static final int QUESTION_PAPER_ID_IS_NULL = QUESTION_PAPER_BASE_ERROR_SEGMENT | 8;
+    /**
+     * 试卷名字为空
+     */
+    public static final int QUESTION_PAPER_NAME_IS_NULL = QUESTION_PAPER_BASE_ERROR_SEGMENT | 9;
+    /**
+     * 试卷描述为空
+     */
+    public static final int QUESTION_PAPER_DESC_IS_NULL = QUESTION_PAPER_BASE_ERROR_SEGMENT | 10;
+    /**
+     * 试卷类型为空
+     */
+    public static final int QUESTION_PAPER_TYPE_IS_NULL = QUESTION_PAPER_BASE_ERROR_SEGMENT | 11;
+    /**
+     * 试卷类型错误
+     */
+    public static final int QUESTION_PAPER_TYPE_IS_ERROR = QUESTION_PAPER_BASE_ERROR_SEGMENT | 12;
+    /**
+     * 试卷的课程ID为空
+     */
+    public static final int QUESTION_PAPER_COURSE_ID_IS_NULL = QUESTION_PAPER_BASE_ERROR_SEGMENT | 13;
+    /**
+     * 试卷异常生成类型为空
+     */
+    public static final int QUESTION_PAPER_ABNORMAL_GENERATE_TYPE_IS_NULL = QUESTION_PAPER_BASE_ERROR_SEGMENT | 14;
+    /**
+     * 试卷总分数为空
+     */
+    public static final int QUESTION_PAPER_SCORE_IS_NULL = QUESTION_PAPER_BASE_ERROR_SEGMENT | 15;
+    /**
+     * 试卷通过分数为空
+     */
+    public static final int QUESTION_PAPER_PASSING_SCORE_IS_NULL = QUESTION_PAPER_BASE_ERROR_SEGMENT | 15;
+
+
+    //==========================考试模块==============================
+
+
+    /**
+     * 考试监考老师的数量错误
+     */
+    public static final int INVIGILATOR_NUMBER_IS_ERROR = EXAM_BASE_ERROR_SEGMENT | 1;
+    /**
+     * 用户正在考试,指定考试失败
+     */
+    public static final int USER_SPECIFIED_TEST_FAILED = EXAM_BASE_ERROR_SEGMENT | 2;
+    /**
+     * 考试ID为空
+     */
+    public static final int EXAM_ID_IS_NULL = EXAM_BASE_ERROR_SEGMENT | 3;
+    /**
+     * 考试不存在
+     */
+    public static final int EXAM_IS_NOT_EXISTS = EXAM_BASE_ERROR_SEGMENT | 4;
+
+
+    //==========================成绩单模块==============================
+
+    /**
+     * 成绩单不存在
+     */
+    public static final int REPORT_CARD_IS_NOT_EXISTS = REPORT_CARD_BASE_ERROR_SEGMENT | 1;
+    /**
+     * 成绩单ID为空
+     */
+    public static final int REPORT_CARD_ID_IS_NULL = REPORT_CARD_BASE_ERROR_SEGMENT | 2;
+    /**
+     * 成绩单的评定为空
+     */
+    public static final int REPORT_CARD_ASSESSMENT_IS_NULL = REPORT_CARD_BASE_ERROR_SEGMENT | 3;
+    /**
+     * 成绩单的分数为空
+     */
+    public static final int REPORT_CARD_SCORE_IS_NULL = REPORT_CARD_BASE_ERROR_SEGMENT | 4;
+    /**
+     * 成绩单的评定错误
+     */
+    public static final int REPORT_CARD_ASSESSMENT_IS_ERROR = REPORT_CARD_BASE_ERROR_SEGMENT | 5;
+
+    //==========================部门模块==============================
+
+    /**
+     * 部门编码为空
+     */
+    public static final int ORG_UNIT_LEVEL_IS_NULL = ORG_UNIT_BASE_ERROR_SEGMENT | 1;
+
+}

+ 165 - 0
src/main/java/cn/com/sailfish/linghang/common/ErrorMessages.java

@@ -0,0 +1,165 @@
+package cn.com.sailfish.linghang.common;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import static cn.com.sailfish.linghang.common.ErrorConstants.*;
+
+/**
+ * Author: huangpeilin
+ * Create at: 2018-12-12 11:44:18
+ * Description:
+ *
+ * @author huangpeilin
+ */
+public class ErrorMessages {
+
+    // 默认错误描述
+    public static final String DEFAULT_ERROR_MESSAGE = "unknown error";
+
+    private static final Map<Integer, String> MESSAGES;
+
+    static {
+        Map<Integer, String> map = new HashMap<Integer, String>();
+        map.put(ERR_BAD_PARAM, "bad request param");
+        map.put(ERR_FORM_PARAMETER_ERROR, "form parameter error");
+        map.put(FILE_UPLOAD_FAILED, "file upload failed.");
+        map.put(BIND_FAILED, "bind failed.");
+        map.put(CAPTCHA_CODE_EXPIRES, "The Captcha verification code expires.");
+        map.put(CAPTCHA_CODE_ERROR, "The Captcha verification code error.");
+        map.put(GET_BYTE_ERROR, "Error converting byte data.");
+        map.put(NUMBER_IS_ERROR, "Strings that are not Numbers");
+        map.put(CAPTCHA_CODE_IS_NULL, "The Captcha verification code is null.");
+        map.put(REFERER_IS_ERROR, "referer is error!");
+        map.put(ERR_INTERNAL_EXCEPTION, "internal server exception");
+
+        // 添加更多的错误的描述
+        MESSAGES = Collections.unmodifiableMap(map);
+
+        // 用户模块
+        map.put(USER_CARD_IS_NULL, "user card is null.");
+        map.put(USER_TYPE_IS_NULL, "user type is null.");
+        map.put(MOBILE_PHONE_IS_NULL, "mobile phone is null.");
+        map.put(MOBILE_PHONE_IS_ERROR_FORMAT, "mobile phone is error format.");
+        map.put(EMAIL_IS_NULL, "email is null.");
+        map.put(EMAIL_IS_ERROR_FORMAT, "email is error format.");
+        map.put(USERNAME_IS_NULL, "username is null.");
+        map.put(USER_MOBILE_PHONE_IS_ALREADY_EXISTS, "mobile phone is already exists.");
+        map.put(USER_EMAIL_IS_ALREADY_EXISTS, "email is already exists.");
+        map.put(USER_TYPE_IS_ERROR, "user type is error");
+        map.put(USER_LOGIN_ACCOUNT_IS_NULL, "user login account is null.");
+        map.put(USER_PASSWORD_IS_NULL, "user password is null.");
+        map.put(USER_IS_NOT_EXISTS, "user is not exists.");
+        map.put(USER_IS_NOT_ACTIVATED, "user is not activated.");
+        map.put(CAN_NOT_OPERATE_WITHOUT_ADMIN_OR_TEACHER, "user is not a admin or teacher.");
+        map.put(USER_PASSWORD_IS_ERROR, "user password is error.");
+        map.put(USER_AUTHORITY_IS_ERROR, "authority is error.");
+        map.put(USER_IS_ALREADY_EXISTS, "user is already exists.");
+        map.put(USER_IS_IN_TEST_QUEUE, "user is in test queue.");
+        map.put(INSUFFICIENT_USER_PERMISSION, "insufficient user permissions.");
+        map.put(USER_OPERATE_ACROSS_REGION, "users operate across regions.");
+        map.put(USER_ID_IS_NULL, "user id is null.");
+        map.put(USER_QUEUE_NAME_IS_NULL, "user queue name is null.");
+        map.put(USER_REMOTE_LOGIN, "user remote login.");
+        map.put(REQUEST_SSO_SERVER_FAILED, "request sso server failed");
+        map.put(THIS_USER_PRIVILEGE_ONLY_ONE_CAN_EXIST, "Only one user with this privilege can exist.");
+        map.put(USER_PERMISSION_DENIED, "permission denied.");
+        map.put(SSO_SERVER_CAN_NOT_FIND_THIS_DATA, "can not find this data.");
+        map.put(USER_ACCOUNT_IS_LOCK, "user account is lock.");
+        map.put(USER_ACCOUNT_IS_ERROR, "user account is error.");
+        map.put(USER_ACCOUNT_IS_VERIFY, "user account is verify.");
+        map.put(USER_SEND_MESSAGE_IPERROR, "user send message iperror.");
+        map.put(USER_SEND_MESSAGE_TOKENERROR, "user send message tokenerror.");
+        map.put(USER_SEND_MESSAGE_DFACTORERROR, "user send message dfactorerror.");
+        map.put(USER_SEND_MESSAGE_FAILURE, "user send message failure.");
+
+        // 文件模块
+        map.put(FILE_IS_NOT_THE_EXCEL, "File is not the excel.");
+        map.put(IMPORT_INFO_IS_FAIL, "Import info is fail.");
+        map.put(FILE_OPERATING_EXCEPTION, "File operating exception.");
+        map.put(FILE_IS_NOT_EXISTS, "File is not exists.");
+        map.put(FILE_IS_NOT_IMAGE, "File is not image.");
+        map.put(FILE_IS_TOO_BIG, "file is too big.");
+
+        // 区域模块
+        map.put(AREA_NAME_IS_NULL, "area name is null.");
+        map.put(AREA_IS_ALREADY_EXISTS, "area is already exists.");
+        map.put(AREA_IS_NOT_EXISTS, "area is not exists.");
+        map.put(AREA_ID_IS_NULL, "area id is null.");
+
+        // 机型模块
+        map.put(MODEL_NAME_IS_NULL, "model name is null.");
+        map.put(MODEL_IS_ALREADY_EXISTS, "model is already exists.");
+        map.put(MODEL_IS_NOT_EXISTS, "model is not exists.");
+        map.put(MODEL_ID_IS_NULL, "model id is null.");
+
+        // 发动机模块
+        map.put(ENGINE_NAME_IS_NULL, "engine name is null.");
+        map.put(ENGINE_IS_ALREADY_EXISTS, "engine is already exists.");
+        map.put(ENGINE_IS_NOT_EXISTS, "engine is not exists.");
+        map.put(ENGINE_ID_IS_NULL, "engine id is null.");
+
+        // 课程模块
+        map.put(COURSE_IS_ALREADY_EXISTS, "course is already exists.");
+        map.put(COURSE_IS_NOT_EXISTS, "course is not exists.");
+        map.put(COURSE_ID_IS_NULL, "course id is null.");
+        map.put(ITEM_ID_IS_NULL, "item id is null.");
+        map.put(ITEM_NAME_IS_NULL, "item name is null.");
+        map.put(ITEM_CHINESE_NAME_IS_NULL, "item chinese name is null.");
+        map.put(ITEM_REGION_IS_NULL, "item region is null.");
+        map.put(ITEM_MODEL_IS_NULL, "item model is null.");
+        map.put(ITEM_ENGINE_IS_NULL, "item engine is null.");
+        map.put(COURSE_NAME_IS_NULL, "course name is null.");
+        map.put(COURSE_CHINESE_NAME_IS_NULL, "course chinese name is null.");
+        map.put(COURSE_MODEL_IS_NULL, "course model is null.");
+        map.put(COURSE_ENGINE_IS_NULL, "course engine is null.");
+        map.put(COURSE_DIVISION_IS_NULL, "course division is null.");
+        map.put(AIR_FRAMER_IS_NULL, "air framer is null.");
+        map.put(DIVISION_IS_ERROR, "division is error.");
+
+        // 试卷模块
+        map.put(QUESTION_PAPER_IS_ALREADY_EXISTS, "question paper is already exists.");
+        map.put(QUESTION_PAPER_IS_NOT_EXISTS, "question paper is not exists.");
+        map.put(QUESTION_PAPER_TITLE_IS_NULL, "question paper id is null.");
+        map.put(QUESTION_PAPER_TEST_LENGTH_IS_NULL, "question paper test length is null.");
+        map.put(QUESTION_PAPER_RANDOM_ABNORMAL_IS_NULL, "question paper random abnormal is null");
+        map.put(QUESTION_PAPER_EXCEPTION_GENERATE_TYPE_IS_ERROR, "question paper exception generate type is error.");
+        map.put(QUESTION_PAPER_DELETE_FAILED, "The paper is used and cannot be deleted.");
+        map.put(QUESTION_PAPER_ID_IS_NULL, "question paper id is null.");
+        map.put(QUESTION_PAPER_NAME_IS_NULL, "question paper name is null");
+        map.put(QUESTION_PAPER_DESC_IS_NULL, "question paper desc is null.");
+        map.put(QUESTION_PAPER_TYPE_IS_NULL, "question paper type is null.");
+        map.put(QUESTION_PAPER_TYPE_IS_ERROR, "question paper type is error.");
+        map.put(QUESTION_PAPER_COURSE_ID_IS_NULL, "question paper course id is null.");
+        map.put(QUESTION_PAPER_ABNORMAL_GENERATE_TYPE_IS_NULL, "question paper abnormal generate type is null.");
+        map.put(QUESTION_PAPER_SCORE_IS_NULL, "question paper score is null.");
+        map.put(QUESTION_PAPER_PASSING_SCORE_IS_NULL, "question paper passing score is null.");
+
+        // 考试模块
+        map.put(INVIGILATOR_NUMBER_IS_ERROR, "invigilator number is error.");
+        map.put(USER_SPECIFIED_TEST_FAILED, "The specified user failed the test");
+        map.put(EXAM_ID_IS_NULL, "exam id is null");
+        map.put(EXAM_IS_NOT_EXISTS, "exam is not exists.");
+
+        // 成绩单模块
+        map.put(REPORT_CARD_IS_NOT_EXISTS, "report card is not exists.");
+        map.put(REPORT_CARD_ID_IS_NULL, "report card id is null.");
+        map.put(REPORT_CARD_ASSESSMENT_IS_NULL, "report card assessment is null.");
+        map.put(REPORT_CARD_SCORE_IS_NULL, "report card score is null.");
+        map.put(REPORT_CARD_ASSESSMENT_IS_ERROR, "report card assessment is error.");
+
+        //部门模块
+        map.put(ORG_UNIT_LEVEL_IS_NULL, "orgUnitLevel is null.");
+    }
+
+    /**
+     * 通过error code取得错误描述
+     *
+     * @param errCode : int 类型的错误代码
+     * @return String : 返回错误描述
+     */
+    public static String getErrorMessage(int errCode) {
+        return MESSAGES.get(errCode);
+    }
+}

+ 35 - 0
src/main/java/cn/com/sailfish/linghang/common/Http401UnauthorizedEntryPoint.java

@@ -0,0 +1,35 @@
+package cn.com.sailfish.linghang.common;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.AuthenticationEntryPoint;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * Author: huangpeilin
+ * Create at: 2018-12-12 11:47:13
+ * Description:
+ *
+ * @author huangpeilin
+ */
+public class Http401UnauthorizedEntryPoint implements AuthenticationEntryPoint {
+
+    private final Logger log = LoggerFactory.getLogger(Http401UnauthorizedEntryPoint.class);
+
+    /**
+     * Always returns a 401 error code to the client.
+     */
+    @Override
+    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException arg2)
+            throws IOException,
+            ServletException {
+
+        log.debug("Pre-authenticated entry point called. Rejecting access");
+        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Access Denied");
+    }
+}

+ 43 - 0
src/main/java/cn/com/sailfish/linghang/common/RestRespDTO.java

@@ -0,0 +1,43 @@
+package cn.com.sailfish.linghang.common;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+
+/**
+ * Author: huangpeilin
+ * Create at: 2018-12-12 11:42:32
+ * Description:
+ *
+ * @author huangpeilin
+ */
+@ApiModel("统一返回Dto")
+@NoArgsConstructor
+@AllArgsConstructor
+@Data
+@Accessors(chain = true)
+public class RestRespDTO<T> implements Serializable {
+
+    @ApiModelProperty("响应码")
+    private Integer errorCode = ErrorConstants.SUCCESS;
+
+    @ApiModelProperty("描述")
+    private String errorMessage;
+
+    @ApiModelProperty("数据")
+    private T data;
+
+    public RestRespDTO success() {
+        return new RestRespDTO(ErrorConstants.SUCCESS, "Success", null);
+    }
+
+    public RestRespDTO success(T data) {
+        return new RestRespDTO(ErrorConstants.SUCCESS, "Success", data);
+    }
+
+}

+ 45 - 0
src/main/java/cn/com/sailfish/linghang/common/auditor/Auditable.java

@@ -0,0 +1,45 @@
+package cn.com.sailfish.linghang.common.auditor;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.annotation.LastModifiedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import javax.persistence.Column;
+import javax.persistence.EntityListeners;
+import javax.persistence.MappedSuperclass;
+import java.time.Instant;
+import java.util.Date;
+
+/**
+ * Author: newma<newma@live.cn>
+ * Create at: 2018-06-05 18:56:10
+ * Description:
+ * @author newma
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Accessors(chain = true)
+@EntityListeners(AuditingEntityListener.class)
+@MappedSuperclass
+public abstract class Auditable {
+
+    @CreatedDate
+    @Column(name = "gmt_created", nullable = false, updatable = false)
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    protected Date gmtCreated;
+
+    @LastModifiedDate
+    @Column(name = "gmt_modified", nullable = false)
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    protected  Date gmtModified;
+
+}

+ 20 - 0
src/main/java/cn/com/sailfish/linghang/common/auditor/AuditorAwareImpl.java

@@ -0,0 +1,20 @@
+package cn.com.sailfish.linghang.common.auditor;
+
+import org.springframework.data.domain.AuditorAware;
+
+import java.util.Optional;
+
+/**
+ * Author: newma<newma@live.cn>
+ * Create at: 2018-06-05 19:02:48
+ * Description:
+ * @author newma
+ */
+public class AuditorAwareImpl implements AuditorAware<String> {
+
+    @Override
+    public Optional<String> getCurrentAuditor() {
+        return Optional.of("admin");
+    }
+
+}

+ 40 - 0
src/main/java/cn/com/sailfish/linghang/config/CrossConfig.java

@@ -0,0 +1,40 @@
+package cn.com.sailfish.linghang.config;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+import org.springframework.web.filter.CorsFilter;
+
+/**
+ * Author: huangpeilin
+ * Create at: 2019-04-22 11:03:35
+ * Description:
+ *
+ * @author huangpeilin
+ */
+@Configuration
+public class CrossConfig {
+
+    @Value("${sailfish.cros}")
+    private String cros;
+
+    @Bean
+    public FilterRegistrationBean crossFilter() {
+        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+        CorsConfiguration config = new CorsConfiguration();
+        config.setAllowCredentials(true);
+        // 设置你要允许的网站域名,如果全允许则设为 *
+        config.addAllowedOrigin(cros);
+        // 如果要限制 HEADER 或 METHOD 请自行更改
+        config.addAllowedHeader("*");
+        config.addAllowedMethod("*");
+        source.registerCorsConfiguration("/**", config);
+        FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
+        // 这个顺序很重要哦,为避免麻烦请设置在最前
+        bean.setOrder(0);
+        return bean;
+    }
+}

+ 46 - 0
src/main/java/cn/com/sailfish/linghang/config/FileUploadConfig.java

@@ -0,0 +1,46 @@
+package cn.com.sailfish.linghang.config;
+
+import cn.com.sailfish.linghang.config.property.FileUploadProperty;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.web.servlet.MultipartConfigFactory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.util.unit.DataSize;
+
+import javax.servlet.MultipartConfigElement;
+import java.io.File;
+
+/**
+ * Author: huangpeilin
+ * Create at: 2019-04-22 11:03:35
+ * Description:
+ *
+ * @author huangpeilin
+ */
+@Configuration
+public class FileUploadConfig {
+
+    @Autowired
+    private FileUploadProperty fileUploadProperty;
+
+    /**
+     *  文件上传配置
+     * @return
+     */
+    @Bean
+    public MultipartConfigElement multipartConfigElement() {
+        MultipartConfigFactory factory = new MultipartConfigFactory();
+        // 单个文件最大
+        factory.setMaxFileSize(DataSize.parse(fileUploadProperty.getMaxFileSize()));
+        // 设置总上传数据总大小
+        factory.setMaxRequestSize(DataSize.parse(fileUploadProperty.getMaxRequestSize()));
+        // 文件上传的临时路径
+        String location = System.getProperty("user.dir") + "/tmp";
+        File tmpFile = new File(location);
+        if (!tmpFile.exists()) {
+            tmpFile.mkdirs();
+        }
+        factory.setLocation(location);
+        return factory.createMultipartConfig();
+    }
+}

+ 51 - 0
src/main/java/cn/com/sailfish/linghang/config/LiquibaseConfig.java

@@ -0,0 +1,51 @@
+package cn.com.sailfish.linghang.config;
+
+import liquibase.configuration.GlobalConfiguration;
+import liquibase.configuration.LiquibaseConfiguration;
+import liquibase.integration.spring.SpringLiquibase;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+import javax.sql.DataSource;
+
+/**
+ * Author: newma<newma@live.cn>
+ * Create at: 2018-07-04 12:25:14
+ * Description:
+ */
+@Configuration
+@EnableJpaRepositories(basePackages = {"cn.com.sailfish.linghang.repository"})
+@EnableTransactionManagement
+public class LiquibaseConfig {
+
+    @Value("${sailfish.liquibase.change-log}")
+    private String changeLogPath;
+
+    @Value("${sailfish.liquibase.drop-first}")
+    private boolean dropFirst;
+
+    @Value("${sailfish.liquibase.enabled}")
+    private boolean enabled;
+
+    @Autowired
+    private DataSource dataSource;
+
+    @Bean
+    public SpringLiquibase liquibase() {
+        GlobalConfiguration configuration = LiquibaseConfiguration.getInstance().getConfiguration(GlobalConfiguration.class);
+        configuration.setDatabaseChangeLogTableName("LINGHANGLIQUIBASECHANGELOG");
+        configuration.setDatabaseChangeLogLockTableName("LINGHANGLIQUIBASECHANGELOGLOCK");
+
+        SpringLiquibase liquibase = new SpringLiquibase();
+        liquibase.setDataSource(dataSource);
+        liquibase.setShouldRun(enabled);
+        liquibase.setDropFirst(dropFirst);
+        liquibase.setChangeLog(changeLogPath);
+
+        return liquibase;
+    }
+}

+ 35 - 0
src/main/java/cn/com/sailfish/linghang/config/RequestLoggingFilterConfig.java

@@ -0,0 +1,35 @@
+package cn.com.sailfish.linghang.config;
+
+import cn.com.sailfish.linghang.filter.RequestLoggingFilter;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.filter.CommonsRequestLoggingFilter;
+
+/**
+ * Author: huangpeilin
+ * Create at: 2018-12-12 11:24:44
+ * Description:
+ *
+ * @author huangpeilin
+ */
+@Configuration
+public class RequestLoggingFilterConfig {
+
+    @Bean
+    public CommonsRequestLoggingFilter logFilter() {
+        CommonsRequestLoggingFilter filter = new RequestLoggingFilter();
+        ((RequestLoggingFilter) filter)
+                .get()
+                    // .exclude("要过滤的接口")
+                    .exclude("/actuator/**")
+                    .exclude("/monitor/**")
+                    .exclude("/swagger-ui.html")
+                    .exclude("/apiv1/web/exam/getUserInTheExamQueue")
+                .post()
+                    .exclude("/actuator/**")
+                    .exclude("/monitor/**")
+                    .exclude("/apiv1/web/user/login")
+                    .exclude("/apiv1/vr/user/login");
+        return filter;
+    }
+}

+ 39 - 0
src/main/java/cn/com/sailfish/linghang/config/SailfishPropertiesConfig.java

@@ -0,0 +1,39 @@
+package cn.com.sailfish.linghang.config;
+
+import cn.com.sailfish.linghang.config.property.*;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.converter.StringHttpMessageConverter;
+import org.springframework.web.client.RestTemplate;
+
+import java.nio.charset.Charset;
+
+/**
+ * Author: huangpeilin
+ * Create at: 2019-04-22 11:03:35
+ * Description:
+ *
+ * @author huangpeilin
+ */
+@Configuration
+@EnableConfigurationProperties({
+        FileUploadProperty.class,
+        SecurityProperty.class,
+        ExportTemplateProperty.class,
+        SsoProperty.class,
+        CaptchaProperty.class
+})
+public class SailfishPropertiesConfig {
+
+    // 设置RestTemplate的超时时间
+    @Bean
+    public RestTemplate restTemplate() {
+        RestTemplate restTemplate = new RestTemplate();
+        restTemplate.getMessageConverters()
+                .add(0, new StringHttpMessageConverter(Charset.forName("UTF-8")));
+
+        return restTemplate;
+    }
+
+}

+ 94 - 0
src/main/java/cn/com/sailfish/linghang/config/SwaggerConfig.java

@@ -0,0 +1,94 @@
+package cn.com.sailfish.linghang.config;
+
+import io.swagger.annotations.Api;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
+import org.springframework.http.HttpHeaders;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+import springfox.documentation.builders.ApiInfoBuilder;
+import springfox.documentation.builders.ParameterBuilder;
+import springfox.documentation.builders.PathSelectors;
+import springfox.documentation.builders.RequestHandlerSelectors;
+import springfox.documentation.schema.ModelRef;
+import springfox.documentation.service.ApiInfo;
+import springfox.documentation.service.Parameter;
+import springfox.documentation.spi.DocumentationType;
+import springfox.documentation.spring.web.plugins.Docket;
+import springfox.documentation.swagger2.annotations.EnableSwagger2;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Author: huangpeilin
+ * Create at: 2018-12-12 11:29:33
+ * Description:
+ *
+ * @author huangpeilin
+ */
+@Slf4j
+@Profile({"dev", "test"})
+@Configuration
+@EnableSwagger2
+public class SwaggerConfig implements WebMvcConfigurer {
+
+    @Override
+    public void addResourceHandlers(ResourceHandlerRegistry registry) {
+        registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/");
+        registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
+        registry.addResourceHandler("/web_frontend/**").addResourceLocations("classpath:/web_frontend/");
+    }
+
+    @Bean
+    public Docket createRestApi() {
+        ParameterBuilder tokenPar = new ParameterBuilder();
+        List<Parameter> pars = new ArrayList<Parameter>();
+        tokenPar.name("Authorization")
+                .description("令牌, Bearer 开头")
+                .modelRef(new ModelRef("string"))
+                .parameterType("header")
+                .defaultValue("Bearer")
+                .required(false)
+                .build();
+        pars.add(tokenPar.build());
+
+        return new Docket(DocumentationType.SWAGGER_2)
+                .apiInfo(apiInfo())
+                .globalOperationParameters(pars)
+                .select()
+                .apis(RequestHandlerSelectors.withClassAnnotation(Api.class)) // 拦截的包路径
+                .paths(PathSelectors.regex("(/web/.*)|(/admin/.*)")) // 拦截的接口路径
+                .build();
+    }
+
+    private ApiInfo apiInfo() {
+        return new ApiInfoBuilder()
+                .title("Swagger REST API")
+                .description("领航系统后台接口")
+                .termsOfServiceUrl("")
+                .version("1.0")
+                .build();
+    }
+
+    /**
+     * 功能描述:
+     * 结合Security之后,还需要在SecurityConfig中设置`and().cors().and().csrf().disable()`
+     *
+     * @param registry
+     */
+    @Override
+    public void addCorsMappings(CorsRegistry registry) {
+        registry.addMapping("/**") // 设置允许跨域的路径
+                .allowCredentials(true) // 是否允许证书,不再默认开启
+                .allowedOrigins("*") // 设置允许跨域请求的域名
+                .allowedHeaders("*") //允许任何请求头
+                .allowedMethods("*") //允许任何方法(post、get等)
+                .maxAge(60 * 60) // 跨域允许时间
+                .exposedHeaders(HttpHeaders.SET_COOKIE).maxAge(60 * 60L); //maxAge(3600)表明在3600秒内,不需要再发送预检验请求,可以缓存该结果
+    }
+
+}

+ 147 - 0
src/main/java/cn/com/sailfish/linghang/config/WebSecurityConfig.java

@@ -0,0 +1,147 @@
+package cn.com.sailfish.linghang.config;
+
+import cn.com.sailfish.linghang.common.Http401UnauthorizedEntryPoint;
+import cn.com.sailfish.linghang.repository.UserRepository;
+import cn.com.sailfish.linghang.security.jwt.JWTConfigurer;
+import cn.com.sailfish.linghang.security.jwt.TokenProvider;
+import org.springframework.beans.factory.BeanInitializationException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpMethod;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.builders.WebSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+import javax.annotation.PostConstruct;
+
+import static cn.com.sailfish.linghang.security.SystemPrivilege.*;
+
+/**
+ * Author: huangpeilin
+ * Create at: 2018-12-12 11:48:26
+ * Description:
+ *
+ * @author huangpeilin
+ */
+@Configuration
+@EnableWebSecurity
+public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
+
+    @Autowired
+    AuthenticationManagerBuilder authenticationManagerBuilder;
+
+    @Autowired
+    UserDetailsService userDetailsService;
+
+    @Autowired
+    TokenProvider tokenProvider;
+
+    @Autowired
+    UserRepository userRepository;
+
+    @Bean
+    public PasswordEncoder passwordEncoder() {
+        return new BCryptPasswordEncoder();
+    }
+
+    @Override
+    protected void configure(HttpSecurity http) throws Exception {
+        http
+                .cors().and().csrf().disable()
+                .headers().frameOptions().disable()
+                .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
+                .and().authorizeRequests()
+                .antMatchers("/vr_resource/**").permitAll()
+                .antMatchers("/web/**").permitAll() // Portal前台不需要权限
+                .antMatchers("/admin/**").permitAll() // 管理后台界面权限
+//                .antMatchers("/admin/**").hasAnyAuthority(DEVELOPER, SUPER_ADMIN, ADMIN, INSTRUCTOR) // todo 管理后台界面权限
+                .antMatchers("/apiv1/web/course/import").hasAnyAuthority(DEVELOPER)
+                .antMatchers("/apiv1/web/check_item/import").hasAnyAuthority(DEVELOPER)
+                .antMatchers("/apiv1/web/exception_item/import").hasAnyAuthority(DEVELOPER)
+                .antMatchers("/apiv1/web/question_paper/import").hasAnyAuthority(DEVELOPER)
+                .antMatchers("/apiv1/web/user/checkAddSuper").hasAnyAuthority(DEVELOPER, SUPER_ADMIN)
+                .antMatchers("/apiv1/web/user/export").hasAnyAuthority(DEVELOPER, SUPER_ADMIN, ADMIN, INSTRUCTOR)
+                .antMatchers("/apiv1/web/user/findOne").hasAnyAuthority(DEVELOPER, SUPER_ADMIN, ADMIN, INSTRUCTOR)
+                .antMatchers("/apiv1/web/user/findAllByArea").hasAnyAuthority(DEVELOPER, SUPER_ADMIN, ADMIN, INSTRUCTOR)
+                .antMatchers("/apiv1/web/user/findAllByCondition").hasAnyAuthority(DEVELOPER, SUPER_ADMIN, ADMIN, INSTRUCTOR)
+                .antMatchers("/apiv1/web/user/recycle").hasAnyAuthority(DEVELOPER, SUPER_ADMIN, ADMIN, INSTRUCTOR)
+                .antMatchers("/apiv1/web/user/getRecycle").hasAnyAuthority(DEVELOPER, SUPER_ADMIN, ADMIN, INSTRUCTOR)
+                .antMatchers("/apiv1/web/user/recycle/findAll").hasAnyAuthority(DEVELOPER, SUPER_ADMIN, ADMIN, INSTRUCTOR)
+                .antMatchers("/apiv1/web/user/enableAuthority").hasAnyAuthority(DEVELOPER, SUPER_ADMIN, ADMIN, INSTRUCTOR)
+                .antMatchers("/apiv1/web/user/restore").hasAnyAuthority(DEVELOPER, SUPER_ADMIN, ADMIN, INSTRUCTOR)
+                .antMatchers("/apiv1/web/user/importByLocomotive").hasAnyAuthority(DEVELOPER, SUPER_ADMIN, ADMIN, INSTRUCTOR)
+                .antMatchers("/apiv1/web/user/findAllByAreaId").hasAnyAuthority(DEVELOPER, SUPER_ADMIN, ADMIN, INSTRUCTOR)
+                .antMatchers("/apiv1/web/question_paper/**").hasAnyAuthority(DEVELOPER, SUPER_ADMIN, ADMIN, INSTRUCTOR)
+                .antMatchers("/apiv1/web/exam/**").hasAnyAuthority(DEVELOPER, SUPER_ADMIN, ADMIN, INSTRUCTOR)
+                .antMatchers("/apiv1/web/course/**").hasAnyAuthority(DEVELOPER, SUPER_ADMIN, ADMIN, INSTRUCTOR)
+                .antMatchers("/apiv1/web/model/**").hasAnyAuthority(DEVELOPER, SUPER_ADMIN, ADMIN, INSTRUCTOR)
+                .antMatchers("/apiv1/web/engine/**").hasAnyAuthority(DEVELOPER, SUPER_ADMIN, ADMIN, INSTRUCTOR)
+                .antMatchers("/apiv1/web/area/**").hasAnyAuthority(DEVELOPER, SUPER_ADMIN, ADMIN, INSTRUCTOR)
+                .antMatchers("/apiv1/web/operation_record/**").hasAnyAuthority(DEVELOPER, SUPER_ADMIN, ADMIN, INSTRUCTOR)
+                .antMatchers("/apiv1/web/report_card/**").hasAnyAuthority(DEVELOPER, SUPER_ADMIN, ADMIN, INSTRUCTOR)
+                .antMatchers("/apiv1/web/sum/**").hasAnyAuthority(DEVELOPER, SUPER_ADMIN, ADMIN, INSTRUCTOR)
+                .antMatchers("/apiv1/univ/file/upload").hasAnyAuthority(DEVELOPER, SUPER_ADMIN, ADMIN, INSTRUCTOR)
+                .antMatchers("/apiv1/web/user/login").permitAll()
+                .antMatchers("/apiv1/vr/user/login").permitAll()
+                .antMatchers("/apiv1/vr/user/sendMobileCode").permitAll()
+                .antMatchers("/apiv1/web/user/sendMobileCode").permitAll()
+                .antMatchers("/apiv1/univ/account/**").permitAll()
+                .antMatchers("/apiv1/web/user/logout").permitAll()
+                .antMatchers("/apiv1/vr/user/logout").permitAll()
+                .antMatchers("/websocket").permitAll()
+                .anyRequest().authenticated()
+                .and()
+                .apply(securityConfigurerAdapter());
+    }
+
+    @Override
+    public void configure(WebSecurity web) throws Exception {
+        web.ignoring()
+                .antMatchers(HttpMethod.OPTIONS, "/**")
+                .antMatchers("/app/**/*.{js,html}")
+                .antMatchers("/i18n/**")
+                .antMatchers("/content/**")
+                .antMatchers("/swagger-ui.html")
+                .antMatchers("/webjars/**")
+                .antMatchers("/v2/**")
+                .antMatchers("/swagger-resources/**")
+                .antMatchers("/test/**")
+                .antMatchers("/resources/vr_resource/**")
+                .antMatchers("/actuator/**")
+                .antMatchers("/monitor/**")
+                .antMatchers("/h2/**");
+    }
+
+    @PostConstruct
+    public void init() {
+        try {
+            authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
+        } catch (Exception e) {
+            throw new BeanInitializationException("Security configuration failed", e);
+        }
+    }
+
+    private JWTConfigurer securityConfigurerAdapter() {
+        return new JWTConfigurer(tokenProvider, userRepository);
+    }
+
+    @Bean
+    public Http401UnauthorizedEntryPoint http401UnauthorizedEntryPoint() {
+        return new Http401UnauthorizedEntryPoint();
+    }
+
+    @Bean
+    @Override
+    public AuthenticationManager authenticationManagerBean() throws Exception {
+        return super.authenticationManagerBean();
+    }
+
+}

+ 20 - 0
src/main/java/cn/com/sailfish/linghang/config/WebSocketConfig.java

@@ -0,0 +1,20 @@
+package cn.com.sailfish.linghang.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.socket.server.standard.ServerEndpointExporter;
+
+/**
+ * @author huangpeilin<plhuang @ sailfish.com.cn>
+ * Create at: 2019-05-30 11:14:14
+ * Description:
+ */
+@Configuration
+public class WebSocketConfig {
+
+    @Bean
+    public ServerEndpointExporter serverEndpointExporter() {
+        return new ServerEndpointExporter();
+    }
+
+}

+ 23 - 0
src/main/java/cn/com/sailfish/linghang/config/property/CaptchaProperty.java

@@ -0,0 +1,23 @@
+package cn.com.sailfish.linghang.config.property;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * @author huangpeilin<plhuang @ sailfish.com.cn>
+ * Create at: 2019-05-28 18:35:33
+ * Description:
+ */
+@Data
+@ConfigurationProperties(prefix = "sailfish.captcha")
+public class CaptchaProperty {
+
+    private Integer validTime;
+
+    @Override
+    public String toString() {
+        return "CaptchaProperty{" +
+                "validTime=" + validTime +
+                '}';
+    }
+}

+ 45 - 0
src/main/java/cn/com/sailfish/linghang/config/property/ExportTemplateProperty.java

@@ -0,0 +1,45 @@
+package cn.com.sailfish.linghang.config.property;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * Author: huangpeilin
+ * Create at: 2019-05-09 15:13:54
+ * Description:
+ *
+ * @author huangpeilin
+ */
+@Data
+@ConfigurationProperties(prefix = "sailfish.export-template")
+public class ExportTemplateProperty {
+
+    private String exportTemplateLocation;
+
+    private String exportFileName;
+
+    private String exportFilePath;
+
+    private String userExportTemplate;
+
+    private String personReportCardExportTemplate;
+
+    private String classReportCardExportTemplate;
+
+    private String examOperationRecordExportTemplate;
+
+    private String trainOperationRecordExportTemplate;
+
+    private String personalExamOperationRecordExportTemplate;
+
+    private String personalTrainingOperationRecordExportTemplate;
+
+    @Override
+    public String toString() {
+        return "ExportTemplateProperty{" +
+                "exportTemplateLocation='" + exportTemplateLocation + '\'' +
+                ", exportFileName='" + exportFileName + '\'' +
+                '}';
+    }
+
+}

+ 28 - 0
src/main/java/cn/com/sailfish/linghang/config/property/FileUploadProperty.java

@@ -0,0 +1,28 @@
+package cn.com.sailfish.linghang.config.property;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * Author: huangpeilin
+ * Create at: 2018-12-12 11:05:42
+ * Description:
+ *
+ * @author huangpeilin
+ */
+@Data
+@ConfigurationProperties(prefix = "sailfish.file-upload")
+public class FileUploadProperty {
+
+    private String maxFileSize;
+
+    private String maxRequestSize;
+
+    @Override
+    public String toString() {
+        return "FileUploadProperty{" +
+                "maxFileSize='" + maxFileSize + '\'' +
+                ", maxRequestSize='" + maxRequestSize + '\'' +
+                '}';
+    }
+}

+ 72 - 0
src/main/java/cn/com/sailfish/linghang/config/property/SecurityProperty.java

@@ -0,0 +1,72 @@
+package cn.com.sailfish.linghang.config.property;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * Author: huangpeilin
+ * Create at: 2018-12-12 11:10:10
+ * Description:
+ *
+ * @author huangpeilin
+ */
+@ConfigurationProperties(prefix = "sailfish.security")
+public class SecurityProperty {
+
+    private JWT jwt;
+
+    public JWT getJwt() {
+        return jwt;
+    }
+
+    public void setJwt(JWT jwt) {
+        this.jwt = jwt;
+    }
+
+    @Override
+    public String toString() {
+        return "SecurityConfig{" +
+                "jwt=" + jwt +
+                '}';
+    }
+
+    public static class JWT {
+        private String secret;
+
+        private Long tokenValidateInSecond;
+
+        private Long tokenValidateInSecondForRememberMe;
+
+        public String getSecret() {
+            return secret;
+        }
+
+        public void setSecret(String secret) {
+            this.secret = secret;
+        }
+
+        public Long getTokenValidateInSecond() {
+            return tokenValidateInSecond;
+        }
+
+        public void setTokenValidateInSecond(Long tokenValidateInSecond) {
+            this.tokenValidateInSecond = tokenValidateInSecond;
+        }
+
+        public Long getTokenValidateInSecondForRememberMe() {
+            return tokenValidateInSecondForRememberMe;
+        }
+
+        public void setTokenValidateInSecondForRememberMe(Long tokenValidateInSecondForRememberMe) {
+            this.tokenValidateInSecondForRememberMe = tokenValidateInSecondForRememberMe;
+        }
+
+        @Override
+        public String toString() {
+            return "JWT{" +
+                    "secret='" + secret + '\'' +
+                    ", tokenValidateInSecond=" + tokenValidateInSecond +
+                    ", tokenValidateInSecondForWeek=" + tokenValidateInSecondForRememberMe +
+                    '}';
+        }
+    }
+}

+ 30 - 0
src/main/java/cn/com/sailfish/linghang/config/property/SsoProperty.java

@@ -0,0 +1,30 @@
+package cn.com.sailfish.linghang.config.property;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * @author huangpeilin<plhuang @ sailfish.com.cn>
+ * Create at: 2019-05-25 11:22:16
+ * Description:
+ */
+@Data
+@ConfigurationProperties(prefix = "sailfish.sso")
+public class SsoProperty {
+
+    private String ip;
+    private String keyDate;
+    private String appName;
+    private String userDataIp;
+
+    @Override
+    public String toString() {
+        return "SsoProperty{" +
+                "ip='" + ip + '\'' +
+                ", keyDate='" + keyDate + '\'' +
+                ", appName='" + appName + '\'' +
+                ", userDataIp='" + userDataIp + '\'' +
+                '}';
+    }
+
+}

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