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;