diff --git a/.browserslistrc b/.browserslistrc
new file mode 100644
index 0000000000000000000000000000000000000000..214388fe43cdfd7ce1c29cd3e401541ded620dba
--- /dev/null
+++ b/.browserslistrc
@@ -0,0 +1,3 @@
+> 1%
+last 2 versions
+not dead
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000000000000000000000000000000000000..4039ff11154a8acc4991d32fc3da2795ff3ac467
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,12 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_size = 2
+indent_style = space
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000000000000000000000000000000000000..1ef592bef79cd8a0af1aeeae9279e200ce8bbc13
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,5 @@
+src/assets
+src/icons
+public
+dist
+node_modules
diff --git a/.eslintrc.js b/.eslintrc.js
new file mode 100644
index 0000000000000000000000000000000000000000..984c9935f3f58afbc14679106e27e67b5ad519d7
--- /dev/null
+++ b/.eslintrc.js
@@ -0,0 +1,106 @@
+module.exports = {
+ root: true,
+ env: {
+ node: true,
+ },
+ extends: ["plugin:vue/recommended", "eslint:recommended", "@vue/prettier"],
+ parser: "vue-eslint-parser",
+ parserOptions: {
+ ecmaVersion: 2020,
+ sourceType: "module",
+ ecmaFeatures: {
+ jsx: true,
+ },
+ },
+ rules: {
+ //'no-undef': 2,
+ "no-console": "off",
+ "no-debugger": "off",
+ "vue/no-v-html": "off",
+ "vue/html-self-closing": [
+ "error",
+ {
+ html: {
+ void: "any",
+ normal: "any",
+ component: "always",
+ },
+ svg: "always",
+ math: "always",
+ },
+ ],
+ // Vue.js风格指南(https://cn.vuejs.org/v2/style-guide/)
+ // Vue组件排序
+ "vue/order-in-components": [
+ "warn",
+ {
+ order: [
+ "el",
+ "name",
+ "key",
+ "parent",
+ "functional",
+ ["delimiters", "comments"],
+ ["components", "directives", "filters"],
+ "extends",
+ "mixins",
+ ["provide", "inject"],
+ "ROUTER_GUARDS",
+ "layout",
+ "middleware",
+ "validate",
+ "scrollToTop",
+ "transition",
+ "loading",
+ "inheritAttrs",
+ "model",
+ ["props", "propsData"],
+ "emits",
+ "setup",
+ "fetch",
+ "asyncData",
+ "data",
+ "head",
+ "computed",
+ "watch",
+ "watchQuery",
+ "LIFECYCLE_HOOKS",
+ "methods",
+ ["template", "render"],
+ "renderError",
+ ],
+ },
+ ],
+ // Vue属性排序
+ "vue/attributes-order": [
+ "warn",
+ {
+ order: [
+ "DEFINITION",
+ "LIST_RENDERING",
+ "CONDITIONALS",
+ "RENDER_MODIFIERS",
+ "GLOBAL",
+ "UNIQUE",
+ "TWO_WAY_BINDING",
+ "OTHER_DIRECTIVES",
+ "OTHER_ATTR",
+ "EVENTS",
+ "CONTENT",
+ ],
+ alphabetical: true, //字母顺序
+ },
+ ],
+ },
+ overrides: [
+ {
+ files: [
+ "**/__tests__/*.{j,t}s?(x)",
+ "**/tests/unit/**/*.spec.{j,t}s?(x)",
+ ],
+ env: {
+ jest: true,
+ },
+ },
+ ],
+};
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..a892a799e6c31d4eecf8771e2dd836043eea1293
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,42 @@
+.DS_Store
+node_modules
+/dist
+
+# local env files
+.env.local
+.env.*.local
+
+# Log files
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+# Editor directories and files
+.idea
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+# Lock files
+yarn.lock
+pnpm-lock.yaml
+package-lock.json
+
+# Yarn v2
+.pnp.*
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+
+# Vab
+public/video
+*.zip
+*.7z
+*.rar
+src/vab/styles/themes/red.scss
diff --git a/.stylelintrc.js b/.stylelintrc.js
new file mode 100644
index 0000000000000000000000000000000000000000..591e39124d52756720ea651b3341d26ddb1fd4c2
--- /dev/null
+++ b/.stylelintrc.js
@@ -0,0 +1,3 @@
+module.exports = {
+ extends: ['stylelint-config-recess-order', 'stylelint-config-prettier'],
+}
diff --git a/babel.config.js b/babel.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..03a1186ce52ab9d78b9942570841ebc31b8f1b64
--- /dev/null
+++ b/babel.config.js
@@ -0,0 +1,8 @@
+module.exports = {
+ presets: ['@vue/cli-plugin-babel/preset'],
+ env: {
+ development: {
+ plugins: ['dynamic-import-node'],
+ },
+ },
+}
diff --git a/jsconfig.json b/jsconfig.json
new file mode 100644
index 0000000000000000000000000000000000000000..2552f428c1497c95ba02fa62d6978eba489127b6
--- /dev/null
+++ b/jsconfig.json
@@ -0,0 +1,12 @@
+{
+ "compilerOptions": {
+ "target": "ES6",
+ "module": "commonjs",
+ "allowSyntheticDefaultImports": true,
+ "baseUrl": "./",
+ "paths": {
+ "@/*": ["src/*"]
+ }
+ },
+ "exclude": ["node_modules"]
+}
diff --git a/mock/index.js b/mock/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..08575337ade4baed899eeebfc3334a5e646e117c
--- /dev/null
+++ b/mock/index.js
@@ -0,0 +1,99 @@
+const chokidarNext = require('chokidar')
+const bodyParser = require('body-parser')
+const chalkNext = require('chalk')
+const path = require('path')
+const { mock } = require('mockjs')
+const { baseURL } = require('../src/config')
+const mockDir = path.join(process.cwd(), 'mock')
+const { handleMockArray } = require('./utils')
+
+/**
+ *
+ * @param url
+ * @param type
+ * @param respond
+ * @returns {{response(*=, *=): void, type: (*|string), url: RegExp}}
+ */
+const responseFake = (url, type, respond) => {
+ return {
+ url: new RegExp(`${baseURL}${url}`),
+ type: type || 'get',
+ response(req, res) {
+ res.status(200)
+ console.log(chalkNext.green(`\n> 请求地址:${req.path}`))
+ if (JSON.stringify(req.body) !== '{}')
+ console.log(
+ chalkNext.green(`> 请求参数(body):${JSON.stringify(req.body)}`)
+ )
+ if (JSON.stringify(req.query) !== '{}')
+ console.log(
+ chalkNext.green(`> 请求参数(query):${JSON.stringify(req.query)}`)
+ )
+ res.json(mock(respond instanceof Function ? respond(req, res) : respond))
+ },
+ }
+}
+
+/**
+ *
+ * @param app
+ * @returns {{mockStartIndex: number, mockRoutesLength: number}}
+ */
+const registerRoutes = (app) => {
+ let mockLastIndex
+ const mocks = []
+ const mockArray = handleMockArray()
+ mockArray.forEach((item) => {
+ const obj = require(item)
+ mocks.push(...obj)
+ })
+ const mocksForServer = mocks.map((route) =>
+ responseFake(route.url, route.type, route.response)
+ )
+ const mockRoutesLength = Object.keys(mocksForServer).length
+ for (const item of mocksForServer) {
+ app[item.type](item.url, item.response)
+ mockLastIndex = app._router.stack.length
+ }
+ return {
+ mockRoutesLength,
+ mockStartIndex: mockLastIndex - mockRoutesLength,
+ }
+}
+
+/**
+ *
+ * @param app
+ */
+module.exports = (app) => {
+ app.use(bodyParser.json())
+ app.use(
+ bodyParser.urlencoded({
+ extended: true,
+ })
+ )
+ const mockRoutes = registerRoutes(app)
+ let mockRoutesLength = mockRoutes.mockRoutesLength
+ let mockStartIndex = mockRoutes.mockStartIndex
+ chokidarNext
+ .watch(mockDir, {
+ ignored: /xmo-mock-server/,
+ ignoreInitial: true,
+ })
+ .on('all', (event) => {
+ if (event === 'change' || event === 'add') {
+ try {
+ app._router.stack.splice(mockStartIndex, mockRoutesLength)
+ Object.keys(require.cache).forEach((item) => {
+ if (item.includes(mockDir))
+ delete require.cache[require.resolve(item)]
+ })
+ const mockRoutes = registerRoutes(app)
+ mockRoutesLength = mockRoutes.mockRoutesLength
+ mockStartIndex = mockRoutes.mockStartIndex
+ } catch (error) {
+ console.log(chalkNext.red(error))
+ }
+ }
+ })
+}
diff --git a/mock/utils/index.js b/mock/utils/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..7cdd34ebdf8c638ab8df5a2cd3240222f908016b
--- /dev/null
+++ b/mock/utils/index.js
@@ -0,0 +1,33 @@
+const fs = require('fs')
+const { Random } = require('mockjs')
+
+/**
+ * @description 随机生成图片url。
+ * @returns {string}
+ */
+function handleRandomImage(width = 50, height = 50) {
+ return `https://picsum.photos/${width}/${height}?random=${Random.guid()}`
+}
+
+/**
+ * @description 处理所有 controller 模块,npm run serve时在node环境中自动输出controller文件夹下Mock接口,请勿修改。
+ * @returns {[]}
+ */
+function handleMockArray() {
+ const getFiles = (path, baseUrl = './controller') => {
+ const files = fs.readdirSync(path)
+ return files.flatMap((file) => {
+ const fPath = `${path}/${file}`
+ const stat = fs.statSync(fPath)
+ return stat.isDirectory()
+ ? getFiles(fPath, `${baseUrl}/${file}`)
+ : `${baseUrl}/${file}`
+ })
+ }
+ return getFiles('mock/controller')
+}
+
+module.exports = {
+ handleRandomImage,
+ handleMockArray,
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..fdfdd5e837d66590c51f4cbd84028b32e86e6b23
--- /dev/null
+++ b/package.json
@@ -0,0 +1,92 @@
+{
+ "name": "exam-ui",
+ "version": "0.0.1",
+ "author": "xmo",
+ "license": "Mozilla Public License Version 2.0",
+ "private": true,
+ "scripts": {
+ "serve": "vue-cli-service serve",
+ "build": "vue-cli-service build",
+ "build:report": "vue-cli-service build --report",
+ "lint": "eslint --ext .js mock --fix&&vue-cli-service lint",
+ "lint:style": "stylelint **/*.{vue,scss} --fix",
+ "inspect": "vue-cli-service inspect",
+ "clear": "npm cache clean -f&&rimraf node_modules&&npm install --registry=https://registry.npm.taobao.org&&cnpm i image-webpack-loader -D",
+ "use:npm": "nrm use npm",
+ "use:taobao": "nrm use taobao",
+ "update": "ncu -u --reject compression-webpack-plugin,filemanager-webpack-plugin,sass,sass-loader --registry https://registry.npm.taobao.org&&npm install --registry=https://registry.npm.taobao.org&&cnpm i image-webpack-loader -D",
+ "update:globle": "ncu -g",
+ "image-webpack-loader": "cnpm i image-webpack-loader -D"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://codechina.csdn.net/xmo/exam-ui.git"
+ },
+ "dependencies": {
+ "axios": "^0.21.1",
+ "clipboard": "^2.0.8",
+ "core-js": "^3.15.1",
+ "dayjs": "^1.10.5",
+ "echarts": "^5.1.2",
+ "element-ui": "^2.15.3",
+ "js-cookie": "^2.2.1",
+ "file-saver": "^2.0.5",
+ "jsencrypt": "^3.2.0",
+ "lodash": "^4.17.21",
+ "mockjs": "^1.1.0",
+ "nprogress": "^0.2.0",
+ "qs": "^6.10.1",
+ "resize-detector": "^0.3.0",
+ "screenfull": "^5.1.0",
+ "vue": "^2.6.14",
+ "vue-i18n": "^8.24.5",
+ "vue-router": "^3.5.2",
+ "vuedraggable": "^2.24.3",
+ "vuex": "^3.6.2"
+ },
+ "devDependencies": {
+ "@vue/cli-plugin-babel": "^4.5.13",
+ "@vue/cli-plugin-eslint": "^4.5.13",
+ "@vue/cli-plugin-router": "^4.5.13",
+ "@vue/cli-plugin-vuex": "^4.5.13",
+ "@vue/cli-service": "^4.5.13",
+ "@vue/eslint-config-prettier": "^6.0.0",
+ "body-parser": "^1.19.0",
+ "chalk": "^4.1.1",
+ "chokidar": "^3.5.2",
+ "compression-webpack-plugin": "^6.1.1",
+ "eslint": "^7.29.0",
+ "eslint-plugin-prettier": "^3.4.0",
+ "eslint-plugin-vue": "^7.11.1",
+ "filemanager-webpack-plugin": "^3.1.1",
+ "image-webpack-loader": "^7.0.1",
+ "lint-staged": "^11.0.0",
+ "plop": "^2.7.4",
+ "prettier": "^2.3.1",
+ "raw-loader": "^4.0.2",
+ "sass": "~1.32.13",
+ "sass-loader": "^10.2.0",
+ "stylelint": "^13.13.1",
+ "stylelint-config-prettier": "^8.0.2",
+ "stylelint-config-recess-order": "^2.4.0",
+ "svg-sprite-loader": "^6.0.8",
+ "vab-templates": "^0.0.5",
+ "vue-eslint-parser": "^7.6.0",
+ "vue-template-compiler": "^2.6.14",
+ "webpackbar": "^5.0.0-3"
+ },
+ "gitHooks": {
+ "pre-commit": "lint-staged"
+ },
+ "lint-staged": {
+ "*.(js,jsx,vue)": [
+ "vue-cli-service lint",
+ "git add"
+ ]
+ },
+ "keywords": [],
+ "engines": {
+ "node": ">= 12.0.0",
+ "npm": ">=3.0.0"
+ }
+}
diff --git a/prettier.config.js b/prettier.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..67b4714e57a544a4970a1a3e23e15fff3286bda3
--- /dev/null
+++ b/prettier.config.js
@@ -0,0 +1,16 @@
+module.exports = {
+ printWidth: 80,
+ tabWidth: 2,
+ useTabs: false,
+ semi: false,
+ singleQuote: true,
+ quoteProps: "as-needed",
+ jsxSingleQuote: false,
+ trailingComma: "es5",
+ bracketSpacing: true,
+ jsxBracketSameLine: false,
+ arrowParens: "always",
+ htmlWhitespaceSensitivity: "ignore",
+ vueIndentScriptAndStyle: true,
+ endOfLine: "lf",
+};
diff --git a/public/css/loading.css b/public/css/loading.css
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..0ad21b4f25b0b2e4aa4aa836c0a2811679f84f5c
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+ <%= VUE_APP_TITLE %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
<%= VUE_APP_TITLE %>
+
+
+
+
diff --git a/src/App.vue b/src/App.vue
new file mode 100644
index 0000000000000000000000000000000000000000..81488ff8e7183398262e7a62e3b095fa82c7d48d
--- /dev/null
+++ b/src/App.vue
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
diff --git a/src/components/XmoTheme/index.vue b/src/components/XmoTheme/index.vue
new file mode 100644
index 0000000000000000000000000000000000000000..8de4765b2519abd353388d65bd3f7ed6ad9d1af9
--- /dev/null
+++ b/src/components/XmoTheme/index.vue
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/src/config/cli.config.js b/src/config/cli.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..fceb5bd1a8966f62b477d3d754f98b0b1cb8acb2
--- /dev/null
+++ b/src/config/cli.config.js
@@ -0,0 +1,27 @@
+/**
+ * @description 导出vue/cli配置,以下所有配置修改需要重启项目
+ */
+module.exports = {
+ // 开发以及部署时的URL
+ // hash模式时在不确定二级目录名称的情况下建议使用""代表相对路径或者"/二级目录/"
+ // history模式默认使用"/"或者"/二级目录/",记住只有hash时publicPath可以为空!!!
+ publicPath: '',
+ // 生产环境构建文件的目录名
+ outputDir: 'dist',
+ // 放置生成的静态资源 (js、css、img、fonts) 的 (相对于 outputDir 的) 目录。
+ assetsDir: 'static',
+ // 开发环境每次保存时是否输出为eslint编译警告
+ lintOnSave: true,
+ // 进行编译的依赖
+ transpileDependencies: ['resize-detector'],
+ // 开发环境端口号
+ devPort: 10000,
+ // 需要自动注入并加载的模块
+ providePlugin: {},
+ // npm run build时是否自动生成7z压缩包
+ build7z: false,
+ // npm run build时是否生成gzip
+ buildGzip: false,
+ // npm run build时是否开启图片压缩,由于国内网路原因image-webpack-loader必须使用cnpm安装,如无法使用cnpm,请配置false
+ imageCompression: true,
+}
diff --git a/src/config/index.js b/src/config/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..ba363a12aa6fce7195dbb1dc16d36bdeb85773a4
--- /dev/null
+++ b/src/config/index.js
@@ -0,0 +1,14 @@
+/**
+ * @description 4个子配置,vue/cli配置|通用配置|主题配置|网络配置导出
+ * config中的部分配置由vue.config.js读取,本质是node,故不可使用window等浏览器对象
+ */
+const cli = require('./cli.config')
+const setting = require('./setting.config')
+const theme = require('./theme.config')
+const network = require('./net.config')
+module.exports = {
+ ...cli,
+ ...setting,
+ ...theme,
+ ...network,
+}
diff --git a/src/config/net.config.js b/src/config/net.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..de20fdf14221f987ecf9b6809a1cad83d798c9bd
--- /dev/null
+++ b/src/config/net.config.js
@@ -0,0 +1,22 @@
+/**
+ * @description 导出网络配置
+ **/
+module.exports = {
+ // 默认的接口地址,开发环境和生产环境都会走/vab-mock-server
+ // 正式项目可以选择自己配置成需要的接口地址,如"https://api.xxx.com"
+ // 问号后边代表开发环境,冒号后边代表生产环境
+ baseURL:
+ process.env.NODE_ENV === 'development'
+ ? '/xmo-mock-server'
+ : '/xmo-mock-server',
+ // 配后端数据的接收方式application/json;charset=UTF-8 或 application/x-www-form-urlencoded;charset=UTF-8
+ contentType: 'application/json;charset=UTF-8',
+ // 最长请求时间
+ requestTimeout: 10000,
+ // 操作正常code,支持String、Array、int多种类型
+ successCode: [200, 0, '200', '0'],
+ // 数据状态的字段名称
+ statusName: 'code',
+ // 状态信息的字段名称
+ messageName: 'msg',
+}
diff --git a/src/config/setting.config.js b/src/config/setting.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..ec03d1e4e2f6fbb3e83f38caa34e765aa8d43e1c
--- /dev/null
+++ b/src/config/setting.config.js
@@ -0,0 +1,60 @@
+/**
+ * @description 导出通用配置
+ */
+module.exports = {
+ // 标题,此项修改后需要重启项目!!! (包括初次加载雪花屏的标题 页面的标题 浏览器的标题)
+ title: 'Exam UI',
+ // 标题分隔符
+ titleSeparator: ' - ',
+ // 标题是否反转
+ // 如果为false: "page - title"
+ // 如果为ture : "title - page"
+ titleReverse: false,
+ // 简写
+ abbreviation: 'exam-ui',
+ // pro版本copyright可随意修改
+ copyright: 'xmo',
+ // 缓存路由的最大数量
+ keepAliveMaxNum: 20,
+ // 路由模式,可选值为 history 或 hash
+ routerMode: 'hash',
+ // 不经过token校验的路由,白名单路由建议配置到与login页面同级,如果需要放行带传参的页面,请使用query传参,配置时只配置path即可
+ routesWhiteList: ['/login', '/register', '/callback', '/404', '/403'],
+ // 加载时显示文字
+ loadingText: '正在加载中...',
+ // token名称
+ tokenName: 'token',
+ // token在localStorage、sessionStorage、cookie存储的key的名称
+ tokenTableName: 'exam-token',
+ // token存储位置localStorage sessionStorage cookie
+ storage: 'localStorage',
+ // token失效回退到登录页时是否记录本次的路由(是否记录当前tab页)
+ recordRoute: true,
+ // 是否开启logo,不显示时设置false,请填写src/icon路径下的图标名称
+ // 如需使用内置RemixIcon图标,请自行去logo组件切换注释代码(内置svg雪碧图较大,对性能有一定影响)
+ logo: 'vuejs-fill',
+ // 语言类型zh、en
+ i18n: 'zh',
+ // 消息框消失时间
+ messageDuration: 3000,
+ // 在哪些环境下显示高亮错误 ['development', 'production']
+ errorLog: 'development',
+ // 是否开启登录拦截
+ loginInterception: true,
+ // 是否开启登录RSA加密
+ loginRSA: false,
+ // intelligence(前端导出路由)和all(后端导出路由)两种方式
+ authentication: 'intelligence',
+ // 是否支持游客模式,支持情况下,访问白名单,可查看所有asyncRoutes
+ supportVisit: false,
+ // 是否开启roles字段进行角色权限控制(如果是all模式后端完全处理角色并进行json组装,可设置false不处理路由中的roles字段)
+ rolesControl: true,
+ // vertical column comprehensive common布局时是否只保持一个子菜单的展开
+ uniqueOpened: false,
+ // vertical column comprehensive common布局时默认展开的菜单path,使用逗号隔开建议只展开一个
+ defaultOpeneds: [],
+ // 需要加loading层的请求,防止重复提交
+ debounce: ['doEdit'],
+ // 分栏布局和综合布局时,是否点击一级菜单默认开启二级菜单(默认第一个,可通过redirect自定义)
+ openFirstMenu: true,
+}
diff --git a/src/config/theme.config.js b/src/config/theme.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..6cb2331b7858cc1f2e22755f640e225cd279998c
--- /dev/null
+++ b/src/config/theme.config.js
@@ -0,0 +1,37 @@
+/**
+ * @description 导出主题配置,注意事项:此配置下的项修改后需清理浏览器缓存!!!
+ */
+module.exports = {
+ // 布局种类:横向布局horizontal、纵向布局vertical、分栏布局column、综合布局comprehensive、常规布局common
+ layout: 'column',
+ // 主题名称:默认default、海洋之心ocean、绿荫草场green、碰触纯白white
+ themeName: 'default',
+ // 分栏风格(仅针对分栏布局column时生效):横向风格horizontal、纵向风格vertical、卡片风格card、箭头风格arrow
+ columnStyle: 'vertical',
+ // 是否固定头部固定
+ fixedHeader: true,
+ // 是否开启顶部进度条
+ showProgressBar: true,
+ // 是否开启标签页
+ showTabs: true,
+ // 显示标签页时标签页样式:卡片风格card、灵动风格smart、圆滑风格smooth
+ tabsBarStyle: 'card',
+ // 是否标签页图标
+ showTabsBarIcon: true,
+ // 是否开启语言选择组件
+ showLanguage: true,
+ // 是否开启刷新组件
+ showRefresh: true,
+ // 是否开启搜索组件
+ showSearch: true,
+ // 是否开启主题组件
+ showTheme: true,
+ // 是否开启通知组件
+ showNotice: true,
+ // 是否开启全屏组件
+ showFullScreen: true,
+ // 是否开启右侧悬浮窗
+ showThemeSetting: true,
+ //纵向布局、常规布局、综合布局时是否默认收起左侧菜单(不支持分栏布局、横向布局)
+ collapse: false,
+}
diff --git a/src/icon/index.js b/src/icon/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/src/main.js b/src/main.js
new file mode 100644
index 0000000000000000000000000000000000000000..f3a53806071db378fe4ad1a9cce5f6f0c27e69ff
--- /dev/null
+++ b/src/main.js
@@ -0,0 +1,41 @@
+import Vue from 'vue'
+import App from './App'
+import store from './store'
+import router from './router'
+
+import '@/icon'
+// 全局样式
+import './styles/main.scss'
+// 加载主题
+const Themes = require.context('./styles/themes', false, /\.scss$/)
+Themes.keys().map(Themes)
+// 加载插件
+const Plugins = require.context('./plugins', true, /\.js$/)
+Plugins.keys().map(Plugins)
+// 加载组件
+const Components = require.context('./components', true, /\.vue$/)
+Components.keys()
+ .map(Components)
+ .forEach((item) => {
+ if (item.default.name) {
+ Vue.component(item.default.name, item.default)
+ }
+ })
+
+// 生产环境注释掉下方代码
+import { baseURL } from './config'
+import { isExternal } from '@/utils/validate'
+if (process.env.NODE_ENV === 'production' && !isExternal(baseURL)) {
+ const { mockXHR } = require('@/utils/mock')
+ mockXHR()
+}
+// 生产环境注释掉上方代码
+
+Vue.config.productionTip = false
+
+new Vue({
+ el: '#app',
+ store,
+ router,
+ render: (h) => h(App),
+})
diff --git a/src/router/index.js b/src/router/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/src/store/index.js b/src/store/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..e9dfe9da8728b4d810ece3135ce04869b15baa8f
--- /dev/null
+++ b/src/store/index.js
@@ -0,0 +1,21 @@
+/**
+ * @description 导入所有 vuex 模块,自动加入namespaced:true,用于解决vuex命名冲突,请勿修改。
+ */
+import Vue from 'vue'
+import Vuex from 'vuex'
+
+Vue.use(Vuex)
+
+const modules = {}
+const files = require.context('./modules', false, /\.js$/)
+files.keys().forEach((key) => {
+ modules[key.replace(/(modules|\/|\.|js)/g, '')] = {
+ ...files(key).default,
+ namespaced: true,
+ }
+})
+
+const store = new Vuex.Store({
+ modules,
+})
+export default store
diff --git a/src/store/modules/errorLog.js b/src/store/modules/errorLog.js
new file mode 100644
index 0000000000000000000000000000000000000000..f6bd3677b5578876e60c602bd58c329ec93f3607
--- /dev/null
+++ b/src/store/modules/errorLog.js
@@ -0,0 +1,26 @@
+/**
+ * @description 异常捕获的状态拦截,请勿修改
+ */
+const state = () => ({
+ errorLogs: [],
+})
+const getters = {
+ errorLogs: (state) => state.errorLogs,
+}
+const mutations = {
+ addErrorLog(state, errorLog) {
+ state.errorLogs.push(errorLog)
+ },
+ clearErrorLog: (state) => {
+ state.errorLogs.splice(0)
+ },
+}
+const actions = {
+ addErrorLog({ commit }, errorLog) {
+ commit('addErrorLog', errorLog)
+ },
+ clearErrorLog({ commit }) {
+ commit('clearErrorLog')
+ },
+}
+export default { state, getters, mutations, actions }
diff --git a/src/store/modules/settings.js b/src/store/modules/settings.js
new file mode 100644
index 0000000000000000000000000000000000000000..4fd6b0f2ef44112b8cd9db1ed3f5f18a803474fa
--- /dev/null
+++ b/src/store/modules/settings.js
@@ -0,0 +1,124 @@
+/**
+ * @description 所有全局配置的状态管理,如无必要请勿修改
+ */
+import { isJson } from '@/utils/validate'
+import {
+ collapse as _collapse,
+ columnStyle,
+ fixedHeader,
+ i18n,
+ layout,
+ logo,
+ showFullScreen,
+ showLanguage,
+ showNotice,
+ showProgressBar,
+ showRefresh,
+ showSearch,
+ showTabs,
+ showTabsBarIcon,
+ showTheme,
+ showThemeSetting,
+ tabsBarStyle,
+ themeName,
+ title,
+} from '@/config'
+
+const defaultTheme = {
+ layout,
+ themeName,
+ columnStyle,
+ fixedHeader,
+ showProgressBar,
+ showTabs,
+ tabsBarStyle,
+ showTabsBarIcon,
+ showLanguage,
+ showRefresh,
+ showSearch,
+ showTheme,
+ showNotice,
+ showFullScreen,
+ showThemeSetting,
+}
+const getLocalStorage = (key) => {
+ const value = localStorage.getItem(key)
+ if (isJson(value)) {
+ return JSON.parse(value)
+ } else {
+ return false
+ }
+}
+const { collapse } = getLocalStorage('collapse')
+const { language } = getLocalStorage('language')
+const state = () => ({
+ logo,
+ title,
+ device: 'desktop',
+ collapse: collapse || _collapse,
+ language: language || i18n,
+ theme: getLocalStorage('theme') || { ...defaultTheme },
+ extra: { first: '' },
+})
+const getters = {
+ logo: (state) => state.logo,
+ title: (state) => state.title,
+ device: (state) => state.device,
+ collapse: (state) => state.collapse,
+ language: (state) => state.language,
+ theme: (state) => state.theme,
+ extra: (state) => state.extra,
+}
+const mutations = {
+ openSideBar(state) {
+ state.collapse = false
+ },
+ foldSideBar(state) {
+ state.collapse = true
+ },
+ toggleDevice(state, device) {
+ state.device = device
+ },
+ toggleCollapse(state) {
+ state.collapse = !state.collapse
+ localStorage.setItem('collapse', `{"collapse":${state.collapse}}`)
+ },
+ changeLanguage(state, language) {
+ state.language = language
+ localStorage.setItem('language', `{"language":"${language}"}`)
+ },
+ saveTheme(state) {
+ localStorage.setItem('theme', JSON.stringify(state.theme))
+ },
+ resetTheme(state) {
+ state.theme = { ...defaultTheme }
+ localStorage.removeItem('theme')
+ document.getElementsByTagName(
+ 'body'
+ )[0].className = `xmo-theme-${state.theme.themeName}`
+ },
+}
+const actions = {
+ openSideBar({ commit }) {
+ commit('openSideBar')
+ },
+ foldSideBar({ commit }) {
+ commit('foldSideBar')
+ },
+ toggleDevice({ commit }, device) {
+ commit('toggleDevice', device)
+ },
+ toggleCollapse({ commit }) {
+ commit('toggleCollapse')
+ },
+ changeLanguage: ({ commit }, language) => {
+ commit('changeLanguage', language)
+ },
+ saveTheme({ commit }) {
+ commit('saveTheme')
+ },
+ resetTheme({ commit }) {
+ commit('resetTheme')
+ },
+}
+export default { state, getters, mutations, actions }
diff --git a/src/styles/main.scss b/src/styles/main.scss
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/src/styles/variables/variables.scss b/src/styles/variables/variables.scss
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/src/utils/mock.js b/src/utils/mock.js
new file mode 100644
index 0000000000000000000000000000000000000000..40247a763f3bd8bcbbe35858fd5f5009cd800826
--- /dev/null
+++ b/src/utils/mock.js
@@ -0,0 +1,49 @@
+/**
+ * @description 导入所有 controller 模块,浏览器环境中自动输出controller文件夹下Mock接口,请勿修改。
+ */
+import Mock from 'mockjs'
+import { paramObj } from '@/utils/index'
+
+const files = require.context('../../mock/controller', true, /\.js$/)
+const mocks = files.keys().flatMap(files)
+
+export function mockXHR() {
+ Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send
+ Mock.XHR.prototype.send = function () {
+ if (this.custom.xhr) {
+ this.custom.xhr.withCredentials = this.withCredentials || false
+ if (this.responseType) {
+ this.custom.xhr.responseType = this.responseType
+ }
+ }
+ if (this.custom.requestHeaders)
+ this.custom.options.headers = this.custom.requestHeaders
+ this.proxy_send(...arguments)
+ }
+
+ function XHRHttpRequest(respond) {
+ return function (options) {
+ let result
+ if (respond instanceof Function) {
+ const { body, type, url, headers } = options
+ result = respond({
+ method: type,
+ body: JSON.parse(body),
+ query: paramObj(url),
+ headers: headers,
+ })
+ } else {
+ result = respond
+ }
+ return Mock.mock(result)
+ }
+ }
+
+ mocks.forEach((item) => {
+ Mock.mock(
+ new RegExp(item.url),
+ item.type || 'get',
+ XHRHttpRequest(item.response)
+ )
+ })
+}
diff --git a/src/utils/validate.js b/src/utils/validate.js
new file mode 100644
index 0000000000000000000000000000000000000000..8d93eb935ed250270196692cb87bfce5f2c78280
--- /dev/null
+++ b/src/utils/validate.js
@@ -0,0 +1,210 @@
+/**
+ * @description 判读是否为外链
+ * @param path
+ * @returns {boolean}
+ */
+export function isExternal(path) {
+ return /^(https?:|mailto:|tel:|\/\/)/.test(path)
+}
+
+/**
+ * @description 校验密码是否小于6位
+ * @param value
+ * @returns {boolean}
+ */
+export function isPassword(value) {
+ return value.length >= 6
+}
+
+/**
+ * @description 判断是否为数字
+ * @param value
+ * @returns {boolean}
+ */
+export function isNumber(value) {
+ const reg = /^[0-9]*$/
+ return reg.test(value)
+}
+
+/**
+ * @description 判断是否是名称
+ * @param value
+ * @returns {boolean}
+ */
+export function isName(value) {
+ const reg = /^[\u4e00-\u9fa5a-zA-Z0-9]+$/
+ return reg.test(value)
+}
+
+/**
+ * @description 判断是否为IP
+ * @param ip
+ * @returns {boolean}
+ */
+export function isIP(ip) {
+ const reg =
+ /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/
+ return reg.test(ip)
+}
+
+/**
+ * @description 判断是否是传统网站
+ * @param url
+ * @returns {boolean}
+ */
+export function isUrl(url) {
+ const reg =
+ /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/
+ return reg.test(url)
+}
+
+/**
+ * @description 判断是否是小写字母
+ * @param value
+ * @returns {boolean}
+ */
+export function isLowerCase(value) {
+ const reg = /^[a-z]+$/
+ return reg.test(value)
+}
+
+/**
+ * @description 判断是否是大写字母
+ * @param value
+ * @returns {boolean}
+ */
+export function isUpperCase(value) {
+ const reg = /^[A-Z]+$/
+ return reg.test(value)
+}
+
+/**
+ * @description 判断是否是大写字母开头
+ * @param value
+ * @returns {boolean}
+ */
+export function isAlphabets(value) {
+ const reg = /^[A-Za-z]+$/
+ return reg.test(value)
+}
+
+/**
+ * @description 判断是否是字符串
+ * @param value
+ * @returns {boolean}
+ */
+export function isString(value) {
+ return typeof value === 'string' || value instanceof String
+}
+
+/**
+ * @description 判断是否是数组
+ * @param arg
+ */
+export function isArray(arg) {
+ if (typeof Array.isArray === 'undefined') {
+ return Object.prototype.toString.call(arg) === '[object Array]'
+ }
+ return Array.isArray(arg)
+}
+
+/**
+ * @description 判断是否是端口号
+ * @param value
+ * @returns {boolean}
+ */
+export function isPort(value) {
+ const reg =
+ /^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$/
+ return reg.test(value)
+}
+
+/**
+ * @description 判断是否是手机号
+ * @param value
+ * @returns {boolean}
+ */
+export function isPhone(value) {
+ const reg = /^1\d{10}$/
+ return reg.test(value)
+}
+
+/**
+ * @description 判断是否是身份证号(第二代)
+ * @param value
+ * @returns {boolean}
+ */
+export function isIdCard(value) {
+ const reg =
+ /^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/
+ return reg.test(value)
+}
+
+/**
+ * @description 判断是否是邮箱
+ * @param value
+ * @returns {boolean}
+ */
+export function isEmail(value) {
+ const reg = /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/
+ return reg.test(value)
+}
+
+/**
+ * @description 判断是否中文
+ * @param value
+ * @returns {boolean}
+ */
+export function isChina(value) {
+ const reg = /^[\u4E00-\u9FA5]{2,4}$/
+ return reg.test(value)
+}
+
+/**
+ * @description 判断是否为空
+ * @param value
+ * @returns {boolean}
+ */
+export function isBlank(value) {
+ return (
+ value === null ||
+ false ||
+ value === '' ||
+ value.trim() === '' ||
+ value.toLocaleLowerCase().trim() === 'null'
+ )
+}
+
+/**
+ * @description 判断是否为固话
+ * @param value
+ * @returns {boolean}
+ */
+export function isTel(value) {
+ const reg =
+ /^(400|800)([0-9\\-]{7,10})|(([0-9]{4}|[0-9]{3})([- ])?)?([0-9]{7,8})(([- 转])*([0-9]{1,4}))?$/
+ return reg.test(value)
+}
+
+/**
+ * @description 判断是否为数字且最多两位小数
+ * @param value
+ * @returns {boolean}
+ */
+export function isNum(value) {
+ const reg = /^\d+(\.\d{1,2})?$/
+ return reg.test(value)
+}
+
+/**
+ * @description 判断是否为json
+ * @param value
+ * @returns {boolean}
+ */
+export function isJson(value) {
+ if (typeof value === 'string') {
+ const obj = JSON.parse(value)
+ return !!(typeof obj === 'object' && obj)
+ }
+ return false
+}
diff --git a/vue.config.js b/vue.config.js
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..49f2ccb6a30f7654a3b331503057d738f7693200 100644
--- a/vue.config.js
+++ b/vue.config.js
@@ -0,0 +1,201 @@
+/**
+ * config配置
+ */
+const path = require('path')
+const {
+ publicPath,
+ assetsDir,
+ outputDir,
+ lintOnSave,
+ transpileDependencies,
+ title,
+ abbreviation,
+ devPort,
+ providePlugin,
+ build7z,
+ buildGzip,
+ imageCompression,
+} = require('./src/config')
+const webpackBanner = ' build : EXAM-UI'
+const webpackBarName = 'Exam UI'
+const { version, author } = require('./package.json')
+const Webpack = require('webpack')
+const WebpackBar = require('webpackbar')
+const FileManagerPlugin = require('filemanager-webpack-plugin')
+const dayjs = require('dayjs')
+const dateTime = dayjs().format('YYYY-M-D HH:mm:ss')
+const CompressionWebpackPlugin = require('compression-webpack-plugin')
+const productionGzipExtensions = ['html', 'js', 'css', 'svg']
+process.env.VUE_APP_TITLE = title
+process.env.VUE_APP_AUTHOR = author
+process.env.VUE_APP_UPDATE_TIME = dateTime
+process.env.VUE_APP_VERSION = version
+const resolve = (dir) => {
+ return path.join(__dirname, dir)
+}
+
+module.exports = {
+ publicPath,
+ assetsDir,
+ outputDir,
+ lintOnSave,
+ transpileDependencies,
+ devServer: {
+ hot: true,
+ port: devPort,
+ open: true,
+ noInfo: false,
+ overlay: {
+ warnings: true,
+ errors: true,
+ },
+ // 注释掉的地方是前端配置代理访问后端的示例
+ // baseURL必须为/xxx,而不是后端服务器,请先了解代理逻辑,再设置前端代理
+ // !!!一定要注意!!!
+ // 1.这里配置了跨域及代理只针对开发环境生效
+ // 2.不建议你在前端配置跨域,建议你后端配置Allow-Origin,Method,Headers,放行token字段,一步到位
+ // 3.后端配置了跨域,就不需要前端再配置,会发生Origin冲突
+ // proxy: {
+ // [baseURL]: {
+ // target: `http://你的后端接口地址`,
+ // ws: true,
+ // changeOrigin: true,
+ // pathRewrite: {
+ // ['^' + baseURL]: '',
+ // },
+ // },
+ // },
+ after: require('./mock'),
+ },
+ configureWebpack() {
+ return {
+ resolve: {
+ alias: {
+ '~': resolve('.'),
+ '@': resolve('src'),
+ },
+ },
+ plugins: [
+ new Webpack.ProvidePlugin(providePlugin),
+ new WebpackBar({
+ name: webpackBarName,
+ }),
+ ],
+ }
+ },
+ chainWebpack(config) {
+ config.resolve.symlinks(true)
+ config.module.rule('svg').exclude.add(resolve('src/icon'))
+ config.module
+ .rule('vabIcon')
+ .test(/\.svg$/)
+ .include.add(resolve('src/icon'))
+ .end()
+ .use('svg-sprite-loader')
+ .loader('svg-sprite-loader')
+ .options({ symbolId: 'xmo-icon-[name]' })
+ config.when(process.env.NODE_ENV === 'development', (config) => {
+ config.devtool('source-map')
+ })
+ config.when(process.env.NODE_ENV === 'production', (config) => {
+ config.performance.set('hints', false)
+ config.devtool('none')
+ config.optimization.splitChunks({
+ automaticNameDelimiter: '-',
+ chunks: 'all',
+ cacheGroups: {
+ chunk: {
+ name: 'xmo-chunk',
+ test: /[\\/]node_modules[\\/]/,
+ minSize: 131072,
+ maxSize: 524288,
+ chunks: 'async',
+ minChunks: 2,
+ priority: 10,
+ },
+ vue: {
+ name: 'vue',
+ test: /[\\/]node_modules[\\/](vue(.*)|core-js)[\\/]/,
+ chunks: 'initial',
+ priority: 20,
+ },
+ elementUI: {
+ name: 'element-ui',
+ test: /[\\/]node_modules[\\/]element-ui(.*)[\\/]/,
+ priority: 30,
+ },
+ extra: {
+ name: 'vab-extra',
+ test: resolve('src/extra'),
+ priority: 40,
+ },
+ // 根据使用模块抽取公共代码
+ // echarts: {
+ // name: 'echarts',
+ // test: /[\\/]node_modules[\\/](echarts|zrender|tslib)[\\/]/,
+ // priority: 50,
+ // },
+ },
+ })
+ config
+ .plugin('banner')
+ .use(Webpack.BannerPlugin, [`${webpackBanner}${dateTime}`])
+ if (imageCompression)
+ config.module
+ .rule('images')
+ .use('image-webpack-loader')
+ .loader('image-webpack-loader')
+ .options({
+ bypassOnDebug: true,
+ })
+ .end()
+ if (buildGzip)
+ config.plugin('compression').use(CompressionWebpackPlugin, [
+ {
+ filename: '[path][base].gz[query]',
+ algorithm: 'gzip',
+ test: new RegExp(
+ '\\.(' + productionGzipExtensions.join('|') + ')$'
+ ),
+ threshold: 8192,
+ minRatio: 0.8,
+ },
+ ])
+ if (build7z)
+ config.plugin('fileManager').use(FileManagerPlugin, [
+ {
+ events: {
+ onEnd: {
+ archive: [
+ {
+ source: `./${outputDir}`,
+ destination: `./${outputDir}/${abbreviation}_${dayjs().unix()}.zip`,
+ },
+ ],
+ },
+ },
+ },
+ ])
+ })
+ },
+ runtimeCompiler: true,
+ productionSourceMap: false,
+ css: {
+ requireModuleExtension: true,
+ sourceMap: true,
+ loaderOptions: {
+ scss: {
+ additionalData(content, loaderContext) {
+ const { resourcePath, rootContext } = loaderContext
+ const relativePath = path.relative(rootContext, resourcePath)
+ if (
+ relativePath.replace(/\\/g, '/') !==
+ 'src/styles/variables/variables.scss'
+ )
+ return '@import "~@/styles/variables/variables.scss";' + content
+ return content
+ },
+ },
+ },
+ },
+}
diff --git a/webstorm.config.js b/webstorm.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..fba240b8634a00021055854c5e859011baae94eb
--- /dev/null
+++ b/webstorm.config.js
@@ -0,0 +1,2 @@
+const webpackConfig = require("@vue/cli-service/webpack.config.js");
+module.exports = webpackConfig;