├── .env ├── .env.development ├── .env.production ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierrc.js ├── LICENSE ├── README.md ├── index.html ├── package.json ├── plugins.d.ts ├── public └── favicon.ico ├── shim.d.ts ├── source.d.ts ├── src ├── App.vue ├── api │ ├── useAutoApi │ │ ├── debugTalk.ts │ │ ├── env.ts │ │ ├── module.ts │ │ ├── project.ts │ │ ├── report.ts │ │ ├── suite.ts │ │ ├── testcase.ts │ │ └── timedTasks.ts │ └── useSystemApi │ │ ├── file.ts │ │ ├── lookup.ts │ │ ├── menu.ts │ │ ├── roles.ts │ │ ├── statistic.ts │ │ └── user.ts ├── assets │ ├── jsoneditor │ │ ├── img │ │ │ └── jsoneditor-icons.svg │ │ ├── jsoneditor.css │ │ └── jsoneditor.min.css │ ├── login-icon-two.svg │ ├── logo.png │ └── three │ │ └── three.js ├── components │ ├── DefDialog │ │ ├── index.js │ │ └── index.vue │ ├── Pagination │ │ └── index.vue │ ├── QueryTable │ │ └── index.vue │ ├── VaceEditor │ │ └── index.js │ ├── iconSelector │ │ └── index.vue │ └── svgIcon │ │ └── index.vue ├── config │ └── config.ts ├── icons │ └── indexSvg │ │ ├── add_case.svg │ │ ├── bronze_medal.svg │ │ ├── case_run_count.svg │ │ ├── case_svg.svg │ │ ├── exc_count.svg │ │ ├── gold_medal.svg │ │ ├── module_svg.svg │ │ ├── project_svg.svg │ │ ├── silver_medal.svg │ │ ├── suite_svg.svg │ │ └── user_login.svg ├── layout │ ├── component │ │ ├── aside.vue │ │ ├── columnsAside.vue │ │ ├── header.vue │ │ └── main.vue │ ├── footer │ │ └── index.vue │ ├── index.vue │ ├── lockScreen │ │ └── index.vue │ ├── logo │ │ └── index.vue │ ├── main │ │ ├── classic.vue │ │ ├── columns.vue │ │ ├── defaults.vue │ │ └── transverse.vue │ ├── navBars │ │ ├── breadcrumb │ │ │ ├── breadcrumb.vue │ │ │ ├── closeFull.vue │ │ │ ├── index.vue │ │ │ ├── search.vue │ │ │ ├── setings.vue │ │ │ ├── user.vue │ │ │ └── userNews.vue │ │ ├── index.vue │ │ └── tagsView │ │ │ ├── contextmenu.vue │ │ │ └── tagsView.vue │ ├── navMenu │ │ ├── horizontal.vue │ │ ├── subItem.vue │ │ └── vertical.vue │ └── routerView │ │ ├── iframes.vue │ │ ├── link.vue │ │ └── parent.vue ├── main.ts ├── router │ ├── backEnd.ts │ ├── frontEnd.ts │ ├── index.ts │ └── route.ts ├── store │ ├── index.ts │ ├── interface │ │ └── index.ts │ └── modules │ │ ├── keepAliveNames.ts │ │ ├── lookup.ts │ │ ├── requestOldRoutes.ts │ │ ├── routesList.ts │ │ ├── tagsViewRoutes.ts │ │ ├── themeConfig.ts │ │ └── userInfos.ts ├── styles │ └── extent.css ├── theme │ ├── app.scss │ ├── common │ │ └── transition.scss │ ├── dark.scss │ ├── element.scss │ ├── iconSelector.scss │ ├── index.scss │ ├── loading.scss │ ├── media │ │ ├── chart.scss │ │ ├── cityLinkage.scss │ │ ├── date.scss │ │ ├── dialog.scss │ │ ├── error.scss │ │ ├── form.scss │ │ ├── home.scss │ │ ├── index.scss │ │ ├── layout.scss │ │ ├── login.scss │ │ ├── media.scss │ │ ├── pagination.scss │ │ ├── personal.scss │ │ ├── scrollbar.scss │ │ └── tagsView.scss │ ├── mixins │ │ └── index.scss │ ├── other.scss │ └── waves.scss ├── utils │ ├── arrayOperation.ts │ ├── authDirective.ts │ ├── authFunction.ts │ ├── commonFunction.ts │ ├── customDirective.ts │ ├── directive.ts │ ├── formatTime.ts │ ├── getStyleSheets.ts │ ├── loading.ts │ ├── lookup.ts │ ├── other.ts │ ├── request.ts │ ├── setIconfont.ts │ ├── storage.ts │ ├── textToImg.js │ ├── theme.ts │ ├── toolsValidate.ts │ ├── vue-json-viewer.js │ └── wartermark.ts └── views │ ├── api │ ├── Report │ │ ├── components │ │ │ └── report.vue │ │ └── index.vue │ ├── case │ │ ├── catCase │ │ │ ├── catCase.vue │ │ │ └── catMessages.vue │ │ ├── components │ │ │ ├── AutomaticAssertion.vue │ │ │ ├── extractValidate.vue │ │ │ ├── messages.vue │ │ │ ├── outputList.vue │ │ │ ├── requestBody.vue │ │ │ ├── requestHeaders.vue │ │ │ ├── saveOrUpdate.vue │ │ │ ├── skip.vue │ │ │ ├── url.vue │ │ │ └── variablesParameters.vue │ │ └── index.vue │ ├── caseSuite │ │ ├── components │ │ │ └── saveOrUpdate.vue │ │ └── index.vue │ ├── configure │ │ ├── components │ │ │ ├── AutomaticAssertion.vue │ │ │ ├── extractValidate.vue │ │ │ ├── messages.vue │ │ │ ├── requestHeaders.vue │ │ │ ├── saveOrUpdate.vue │ │ │ └── variablesParameters.vue │ │ └── index.vue │ ├── debugTalk │ │ ├── components │ │ │ ├── debugFunc.vue │ │ │ ├── editDebugtalk.vue │ │ │ └── saveOrUpdate.vue │ │ └── index.vue │ ├── env │ │ ├── components │ │ │ └── saveOrUpdate.vue │ │ └── index.vue │ ├── module │ │ ├── components │ │ │ └── saveOrUpdate.vue │ │ └── index.vue │ ├── project │ │ ├── components │ │ │ └── saveOrUpdate.vue │ │ └── index.vue │ └── timedTask │ │ ├── components │ │ └── saveOrUpdate.vue │ │ └── index.vue │ ├── error │ ├── 401.vue │ └── 404.vue │ ├── home │ ├── components │ │ ├── caseTopStatistics.vue │ │ ├── countStatistics.vue │ │ ├── extTopStatistics.vue │ │ ├── extTrendStatistics.vue │ │ ├── projectStatistics.vue │ │ ├── runTrendStatistics.vue │ │ └── suiteTopStatistics.vue │ └── index.vue │ ├── login │ ├── component │ │ ├── account.vue │ │ ├── mobile.vue │ │ └── scan.vue │ └── index.vue │ └── system │ ├── dept │ ├── component │ │ ├── addDept.vue │ │ └── editDept.vue │ └── index.vue │ ├── lookup │ └── index.vue │ ├── menu │ ├── components │ │ └── saveOrUpdate.vue │ └── index.vue │ ├── role │ ├── components │ │ └── saveOrUpdate.vue │ └── index.vue │ └── user │ ├── components │ └── saveOrUpdate.vue │ └── index.vue ├── tsconfig.json ├── vite.config.ts └── yarn.lock /.env: -------------------------------------------------------------------------------- 1 | # port 端口号 2 | VITE_PORT = 8888 3 | 4 | # open 运行 npm run dev 时自动打开浏览器 5 | VITE_OPEN = false 6 | 7 | # public path 配置线上环境路径(打包)、本地通过 http-server 访问时,请置空即可 8 | VITE_PUBLIC_PATH = -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # 本地环境 2 | ENV = 'development' 3 | 4 | # 本地环境接口地址 5 | VITE_API_URL = 'http://localhost:8888/' -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # 线上环境 2 | ENV = 'production' 3 | 4 | # 线上环境接口地址 5 | VITE_API_URL = '' -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | 2 | *.sh 3 | node_modules 4 | lib 5 | *.md 6 | *.scss 7 | *.woff 8 | *.ttf 9 | .vscode 10 | .idea 11 | dist 12 | mock 13 | public 14 | bin 15 | build 16 | config 17 | index.html 18 | src/assets -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | es2021: true, 6 | node: true, 7 | }, 8 | parser: 'vue-eslint-parser', 9 | parserOptions: { 10 | ecmaVersion: 12, 11 | parser: '@typescript-eslint/parser', 12 | sourceType: 'module', 13 | }, 14 | extends: ['plugin:vue/vue3-essential', 'plugin:vue/essential', 'eslint:recommended'], 15 | plugins: ['vue', '@typescript-eslint'], 16 | rules: { 17 | // http://eslint.cn/docs/rules/ 18 | // https://eslint.vuejs.org/rules/ 19 | '@type-eslint/ban-ts-ignore': 'off', 20 | '@type-eslint/explicit-function-return-type': 'off', 21 | '@type-eslint/no-explicit-any': 'off', 22 | '@type-eslint/no-var-requires': 'off', 23 | '@type-eslint/no-empty-function': 'off', 24 | '@type-eslint/no-use-before-define': 'off', 25 | '@type-eslint/ban-ts-comment': 'off', 26 | '@type-eslint/ban-types': 'off', 27 | '@type-eslint/no-non-null-assertion': 'off', 28 | '@type-eslint/explicit-module-boundary-types': 'off', 29 | 'vue/custom-event-name-casing': 'off', 30 | 'vue/attributes-order': 'off', 31 | 'vue/one-component-per-file': 'off', 32 | 'vue/html-closing-bracket-newline': 'off', 33 | 'vue/max-attributes-per-line': 'off', 34 | 'vue/multiline-html-element-content-newline': 'off', 35 | 'vue/singleline-html-element-content-newline': 'off', 36 | 'vue/attribute-hyphenation': 'off', 37 | 'vue/html-self-closing': 'off', 38 | 'vue/no-multiple-template-root': 'off', 39 | 'vue/require-default-prop': 'off', 40 | 'vue/no-v-model-argument': 'off', 41 | 'vue/no-arrow-functions-in-watch': 'off', 42 | 'vue/no-template-key': 'off', 43 | 'vue/no-v-html': 'off', 44 | 'vue/comment-directive': 'off', 45 | 'vue/no-parsing-error': 'off', 46 | 'vue/no-deprecated-v-on-native-modifier': 'off', 47 | 'vue/multi-word-component-names': 'off', 48 | 'no-useless-escape': 'off', 49 | 'no-sparse-arrays': 'off', 50 | 'no-prototype-builtins': 'off', 51 | 'no-constant-condition': 'off', 52 | 'no-use-before-define': 'off', 53 | 'no-restricted-globals': 'off', 54 | 'no-restricted-syntax': 'off', 55 | 'generator-star-spacing': 'off', 56 | 'no-unreachable': 'off', 57 | 'no-multiple-template-root': 'off', 58 | 'no-unused-vars': 'error', 59 | 'no-v-model-argument': 'off', 60 | 'no-case-declarations': 'off', 61 | 'no-console': 'error', 62 | }, 63 | }; 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // 一行最多多少个字符 3 | printWidth: 150, 4 | // 指定每个缩进级别的空格数 5 | tabWidth: 2, 6 | // 使用制表符而不是空格缩进行 7 | useTabs: true, 8 | // 在语句末尾打印分号 9 | semi: true, 10 | // 使用单引号而不是双引号 11 | singleQuote: true, 12 | // 更改引用对象属性的时间 可选值"" 13 | quoteProps: 'as-needed', 14 | // 在JSX中使用单引号而不是双引号 15 | jsxSingleQuote: false, 16 | // 多行时尽可能打印尾随逗号。(例如,单行数组永远不会出现逗号结尾。) 可选值"",默认none 17 | trailingComma: 'es5', 18 | // 在对象文字中的括号之间打印空格 19 | bracketSpacing: true, 20 | // jsx 标签的反尖括号需要换行 21 | jsxBracketSameLine: false, 22 | // 在单独的箭头函数参数周围包括括号 always:(x) => x \ avoid:x => x 23 | arrowParens: 'always', 24 | // 这两个选项可用于格式化以给定字符偏移量(分别包括和不包括)开始和结束的代码 25 | rangeStart: 0, 26 | rangeEnd: Infinity, 27 | // 指定要使用的解析器,不需要写文件开头的 @prettier 28 | requirePragma: false, 29 | // 不需要自动在文件开头插入 @prettier 30 | insertPragma: false, 31 | // 使用默认的折行标准 always\never\preserve 32 | proseWrap: 'preserve', 33 | // 指定HTML文件的全局空格敏感度 css\strict\ignore 34 | htmlWhitespaceSensitivity: 'css', 35 | // Vue文件脚本和样式标签缩进 36 | vueIndentScriptAndStyle: false, 37 | // 换行符使用 lf 结尾是 可选值"" 38 | endOfLine: 'lf', 39 | }; 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 lyt-Top 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #### 🌈 介绍 2 | 3 | 基于 python + flask + httprunner3.1.6 + celery + sqlalchemy + marshmallow + redis 4 | 5 | #### 💒 前端地址 6 | - github 7 | https://github.com/baizunxian/zero_autotest_front 8 | - gitee 9 | https://gitee.com/xb_xiaobai/zero_autotest_front 10 | #### 💒 后端地址 11 | - github 12 | https://github.com/baizunxian/zero_autotest_backend 13 | - gitee 14 | https://gitee.com/xb_xiaobai/zero_autotest_backend 15 | #### ⛱️ 线上预览 16 | 17 | - ZERO AUTOTEST 自动化测试平台在线预览 https://xiaobaicodes.com:8888 18 | 19 | #### 🚧 安装 cnpm、yarn 20 | 21 | ```bash 22 | # node 版本 23 | node -v 24 | v14.17.5 25 | ``` 26 | 27 | - 复制代码(桌面 cmd 运行) `npm install -g cnpm --registry=https://registry.npm.taobao.org` 28 | - 复制代码(桌面 cmd 运行) `npm install -g yarn` 29 | 30 | ```bash 31 | # 克隆项目 32 | git clone https://github.com/baizunxian/zero_autotest_front 33 | 34 | # 进入项目 35 | cd zero_autotest_front 36 | 37 | # 安装依赖 38 | cnpm install 39 | # 或者 40 | yarn 41 | 42 | # 运行项目 43 | cnpm run dev 44 | # 或者 45 | yarn dev 46 | 47 | # 打包发布 48 | cnpm run build 49 | # 或者 50 | yarn build 51 | ``` 52 | 53 | #### 💯 学习交流加 微信 群 54 | 55 | - 微信群 56 | zero autotest 交流群 57 | 58 | #### 💌 支持作者 59 | 60 | 如果觉得框架不错,或者已经在使用了,希望你可以去 Github 帮我点个 ⭐ Star,这将是对我极大的鼓励与支持, 平台会持续迭代更新。 61 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 15 | 16 | zero-autotest 17 | 18 | 19 |
20 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zero_autotest", 3 | "version": "1.0.0", 4 | "description": "一个自动化测试平台", 5 | "author": "baizunxian", 6 | "license": "MIT", 7 | "scripts": { 8 | "dev": "vite --force", 9 | "build": "vite build", 10 | "lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/" 11 | }, 12 | "dependencies": { 13 | "-": "^0.0.1", 14 | "@element-plus/icons-vue": "^1.0.0", 15 | "D": "^1.0.0", 16 | "axios": "^0.26.0", 17 | "echarts": "^5.3.2", 18 | "element-plus": "^2.1.8", 19 | "json-editor-vue": "^0.3.6", 20 | "mitt": "^3.0.0", 21 | "nprogress": "^0.2.0", 22 | "qrcodejs2-fixes": "^0.0.2", 23 | "screenfull": "^6.0.1", 24 | "sortablejs": "^1.14.0", 25 | "svelte-jsoneditor": "^0.3.25", 26 | "vue": "^3.2.31", 27 | "vue-clipboard3": "^1.0.1", 28 | "vue-router": "^4.0.13", 29 | "vuex": "^4.0.2" 30 | }, 31 | "devDependencies": { 32 | "@types/axios": "^0.14.0", 33 | "@types/clipboard": "^2.0.1", 34 | "@types/node": "^17.0.21", 35 | "@types/nprogress": "^0.2.0", 36 | "@typescript-eslint/eslint-plugin": "^5.13.0", 37 | "@typescript-eslint/parser": "^5.13.0", 38 | "@vitejs/plugin-vue": "^2.2.4", 39 | "@vue/compiler-sfc": "^3.2.31", 40 | "ace-builds": "^1.5.1", 41 | "clipboard": "^2.0.10", 42 | "countup": "^1.8.2", 43 | "dotenv": "^16.0.0", 44 | "eslint": "7.8.0", 45 | "eslint-plugin-vue": "^8.5.0", 46 | "json-editor-vue3": "^1.0.6", 47 | "prettier": "^2.5.1", 48 | "resize-observer-polyfill": "^1.5.1", 49 | "sass": "^1.49.9", 50 | "sass-loader": "^12.6.0", 51 | "typescript": "^4.6.2", 52 | "vanta": "^0.5.22", 53 | "vite": "^2.8.6", 54 | "vue-eslint-parser": "^8.3.0" 55 | }, 56 | "browserslist": [ 57 | "> 1%", 58 | "last 2 versions", 59 | "not dead" 60 | ], 61 | "engines": { 62 | "node": ">=12.0.0", 63 | "npm": ">= 6.0.0" 64 | }, 65 | "keywords": [ 66 | "vue", 67 | "vue3", 68 | "vuejs/vue-next", 69 | "vuejs/vue-next-template", 70 | "element-ui", 71 | "element-plus" 72 | ] 73 | } 74 | -------------------------------------------------------------------------------- /plugins.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'vue-grid-layout'; 2 | declare module 'sortablejs'; 3 | declare module 'qrcodejs2-fixes'; 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baizunxian/zero_autotest_front/70e52bbb7d8a0d6e418d1417ed3271a7ea6bcb3d/public/favicon.ico -------------------------------------------------------------------------------- /shim.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | // 声明文件,*.vue 后缀的文件交给 vue 模块来处理 4 | declare module '*.vue' { 5 | import type { DefineComponent } from 'vue'; 6 | const component: DefineComponent<{}, {}, any>; 7 | export default component; 8 | } 9 | 10 | // 声明文件,定义全局变量。其它 app.configure.globalProperties.xxx,使用 getCurrentInstance() 来获取 11 | interface Window { 12 | nextLoading: boolean; 13 | } 14 | -------------------------------------------------------------------------------- /source.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.json'; 2 | declare module '*.png'; 3 | declare module '*.jpg'; 4 | declare module '*.scss'; 5 | declare module '*.ts'; 6 | declare module '*.js'; 7 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 83 | 84 | -------------------------------------------------------------------------------- /src/api/useAutoApi/debugTalk.ts: -------------------------------------------------------------------------------- 1 | import request from '/@/utils/request'; 2 | 3 | /** 4 | * 项目使用接口 5 | * @method getProjectList 获取项目列表 6 | * @method getMenuTest 获取后端动态路由菜单(test) 7 | */ 8 | export function useDebugTalkApi() { 9 | return { 10 | getList: (data?: object) => { 11 | return request({ 12 | url: '/debugTalk/list', 13 | method: 'POST', 14 | data, 15 | }); 16 | }, 17 | saveOrUpdate(data?: object) { 18 | return request({ 19 | url: '/debugTalk/saveOrUpdate', 20 | method: 'POST', 21 | data 22 | }) 23 | }, 24 | getDebugTalkInfo: (data?: object) => { 25 | return request({ 26 | url: '/debugTalk/getDebugTalkInfo', 27 | method: 'POST', 28 | data, 29 | }); 30 | }, 31 | deleted: (data?: object) => { 32 | return request({ 33 | url: '/debugTalk/deleted', 34 | method: 'post', 35 | data, 36 | }); 37 | }, 38 | getFuncList: (data?: object) => { 39 | return request({ 40 | url: '/debugTalk/getFuncList', 41 | method: 'post', 42 | data, 43 | }); 44 | }, 45 | debugFunc: (data?: object) => { 46 | return request({ 47 | url: '/debugTalk/debugFunc', 48 | method: 'post', 49 | data, 50 | }); 51 | }, 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /src/api/useAutoApi/env.ts: -------------------------------------------------------------------------------- 1 | import request from '/@/utils/request'; 2 | 3 | /** 4 | * 项目使用接口 5 | * @method getProjectList 获取项目列表 6 | * @method getMenuTest 获取后端动态路由菜单(test) 7 | */ 8 | export function useEnvApi() { 9 | return { 10 | getList: (data?: object) => { 11 | return request({ 12 | url: '/env/list', 13 | method: 'POST', 14 | data, 15 | }); 16 | }, 17 | saveOrUpdate(data?: object) { 18 | return request({ 19 | url: '/env/saveOrUpdate', 20 | method: 'POST', 21 | data 22 | }) 23 | }, 24 | deleted: (data?: object) => { 25 | return request({ 26 | url: '/env/deleted', 27 | method: 'POST', 28 | data, 29 | }); 30 | }, 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /src/api/useAutoApi/module.ts: -------------------------------------------------------------------------------- 1 | import request from '/@/utils/request'; 2 | 3 | /** 4 | * 项目使用接口 5 | * @method getProjectList 获取项目列表 6 | * @method getMenuTest 获取后端动态路由菜单(test) 7 | */ 8 | export function useModuleApi() { 9 | return { 10 | getList: (data?: object) => { 11 | return request({ 12 | url: '/module/list', 13 | method: 'POST', 14 | data, 15 | }); 16 | }, 17 | saveOrUpdate(data?: object) { 18 | return request({ 19 | url: '/module/saveOrUpdate', 20 | method: 'POST', 21 | data 22 | }) 23 | }, 24 | deleted: (data?: object) => { 25 | return request({ 26 | url: '/module/deleted', 27 | method: 'POST', 28 | data, 29 | }); 30 | }, 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /src/api/useAutoApi/project.ts: -------------------------------------------------------------------------------- 1 | import request from '/@/utils/request'; 2 | 3 | /** 4 | * 项目使用接口 5 | * @method getProjectList 获取项目列表 6 | * @method getMenuTest 获取后端动态路由菜单(test) 7 | */ 8 | export function useProjectApi() { 9 | return { 10 | getList: (data?: object) => { 11 | return request({ 12 | url: '/project/list', 13 | method: 'POST', 14 | data, 15 | }); 16 | }, 17 | saveOrUpdate(data?: object) { 18 | return request({ 19 | url: '/project/saveOrUpdate', 20 | method: 'POST', 21 | data 22 | }) 23 | }, 24 | deleted: (data?: object) => { 25 | return request({ 26 | url: '/project/deleted', 27 | method: 'POST', 28 | data, 29 | }); 30 | }, 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /src/api/useAutoApi/report.ts: -------------------------------------------------------------------------------- 1 | import request from '/@/utils/request'; 2 | 3 | /** 4 | * 项目使用接口 5 | * @method getProjectList 获取项目列表 6 | * @method getMenuTest 获取后端动态路由菜单(test) 7 | */ 8 | export function useReportApi() { 9 | return { 10 | getList: (data?: object) => { 11 | return request({ 12 | url: '/report/list', 13 | method: 'POST', 14 | data, 15 | }); 16 | }, 17 | saveOrUpdate(data?: object) { 18 | return request({ 19 | url: '/report/saveOrUpdate', 20 | method: 'POST', 21 | data 22 | }) 23 | }, 24 | deleted: (data?: object) => { 25 | return request({ 26 | url: '/report/deleted', 27 | method: 'POST', 28 | data, 29 | }); 30 | }, 31 | getReportById: (data?: object) => { 32 | return request({ 33 | url: '/report/getReportById', 34 | method: 'POST', 35 | data, 36 | }); 37 | }, 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /src/api/useAutoApi/suite.ts: -------------------------------------------------------------------------------- 1 | import request from '/@/utils/request'; 2 | 3 | /** 4 | * 项目使用接口 5 | * @method getProjectList 获取项目列表 6 | * @method getMenuTest 获取后端动态路由菜单(test) 7 | */ 8 | export function useTestSuiteApi() { 9 | return { 10 | getList: (data?: object) => { 11 | return request({ 12 | url: '/testSuites/list', 13 | method: 'POST', 14 | data, 15 | }); 16 | }, 17 | saveOrUpdate(data?: object) { 18 | return request({ 19 | url: '/testSuites/saveOrUpdate', 20 | method: 'POST', 21 | data 22 | }) 23 | }, 24 | deleted: (data?: object) => { 25 | return request({ 26 | url: '/testSuites/deleted', 27 | method: 'POST', 28 | data, 29 | }); 30 | }, 31 | getSuitesInfo: (data?: object) => { 32 | return request({ 33 | url: '/testSuites/getSuiteInfo', 34 | method: 'POST', 35 | data, 36 | }); 37 | }, 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /src/api/useAutoApi/testcase.ts: -------------------------------------------------------------------------------- 1 | import request from '/@/utils/request'; 2 | 3 | /** 4 | * 项目使用接口 5 | * @method getProjectList 获取项目列表 6 | * @method getMenuTest 获取后端动态路由菜单(test) 7 | */ 8 | export function useTestCaseApi() { 9 | return { 10 | getList: (data?: object) => { 11 | return request({ 12 | url: '/testcase/list', 13 | method: 'POST', 14 | data, 15 | }); 16 | }, 17 | saveOrUpdate(data?: object) { 18 | return request({ 19 | url: '/testcase/saveOrUpdate', 20 | method: 'POST', 21 | data 22 | }) 23 | }, 24 | deleted: (data?: object) => { 25 | return request({ 26 | url: '/testcase/deleted', 27 | method: 'POST', 28 | data, 29 | }); 30 | }, 31 | runTestCase: (data?: object) => { 32 | return request({ 33 | url: '/testcase/runTestCase', 34 | method: 'POST', 35 | data, 36 | }); 37 | }, 38 | runTestCaseNew: (data?: object) => { 39 | return request({ 40 | url: '/testcase/runTestCaseNew', 41 | method: 'POST', 42 | data, 43 | }); 44 | }, 45 | debugTestCase: (data?: object) => { 46 | return request({ 47 | url: '/testcase/debugTestCase', 48 | method: 'POST', 49 | data, 50 | }); 51 | }, 52 | debugTestCaseNew: (data?: object) => { 53 | return request({ 54 | url: '/testcase/debugTestCaseNew', 55 | method: 'POST', 56 | data, 57 | }); 58 | }, 59 | getTestCaseInfo: (data?: object) => { 60 | return request({ 61 | url: '/testcase/getTestCaseInfo', 62 | method: 'POST', 63 | data, 64 | }); 65 | }, 66 | postman2case: (data?: object) => { 67 | return request({ 68 | url: '/testcase/postman2case', 69 | method: 'POST', 70 | headers: {"Content-Type": "multipart/form-data"}, 71 | data, 72 | }); 73 | }, 74 | }; 75 | } 76 | -------------------------------------------------------------------------------- /src/api/useAutoApi/timedTasks.ts: -------------------------------------------------------------------------------- 1 | import request from '/@/utils/request'; 2 | 3 | /** 4 | * 项目使用接口 5 | * @method getProjectList 获取项目列表 6 | * @method getMenuTest 获取后端动态路由菜单(test) 7 | */ 8 | export function useTimedTasksApi() { 9 | return { 10 | getList: (data?: object) => { 11 | return request({ 12 | url: '/timedTasks/list', 13 | method: 'POST', 14 | data, 15 | }); 16 | }, 17 | saveOrUpdate(data?: object) { 18 | return request({ 19 | url: '/timedTasks/saveOrUpdate', 20 | method: 'POST', 21 | data 22 | }) 23 | }, 24 | deleted: (data?: object) => { 25 | return request({ 26 | url: '/timedTasks/deleted', 27 | method: 'POST', 28 | data, 29 | }); 30 | }, 31 | taskSwitch: (data?: object) => { 32 | return request({ 33 | url: '/timedTasks/taskSwitch', 34 | method: 'POST', 35 | data, 36 | }); 37 | }, 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /src/api/useSystemApi/file.ts: -------------------------------------------------------------------------------- 1 | import request from '/@/utils/request'; 2 | 3 | /** 4 | * 用户接口 5 | * @method getUserList 获取用户列表 6 | * @method allMenu 获取菜单接口,平铺 7 | * @method saveOrUpdateMenu 更新保存菜单 8 | */ 9 | export function useFileApi() { 10 | return { 11 | upload: (data: object) => { 12 | return request({ 13 | url: '/file/upload', 14 | method: 'POST', 15 | headers: {"Content-Type": "multipart/form-data"}, 16 | data, 17 | }); 18 | }, 19 | download: (path: string) => { 20 | return request({ 21 | url: '/file/download/' + path, 22 | method: 'GET', 23 | }); 24 | }, 25 | deleted: (data: object) => { 26 | return request({ 27 | url: '/file/deleted', 28 | method: 'POST', 29 | data, 30 | }); 31 | }, 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/api/useSystemApi/lookup.ts: -------------------------------------------------------------------------------- 1 | import request from '/@/utils/request'; 2 | 3 | /** 4 | * 数据字典 5 | * @method getLookupList 获取数据字典列表 6 | * @method saveOrUpdateLookup 更新保存数据字典 7 | * @method delLookup 删除数据字典 8 | * @method getLookupValue 获取数据字典列值 9 | * @method saveOrUpdateLookupValue 更新保存数据字典值 10 | * @method delLookupValue 删除数据字典值 11 | */ 12 | export function useLookupApi() { 13 | return { 14 | getAllLookup: () => { 15 | return request({ 16 | url: '/lookup/getAllLookup', 17 | method: 'POST', 18 | data: {} 19 | }); 20 | }, 21 | getLookupList: (data: object) => { 22 | return request({ 23 | url: '/lookup/getLookupList', 24 | method: 'POST', 25 | data, 26 | }); 27 | }, 28 | saveOrUpdateLookup: (data: object) => { 29 | return request({ 30 | url: '/lookup/saveOrUpdateLookup', 31 | method: 'POST', 32 | data 33 | }); 34 | }, 35 | delLookup: (data?: object) => { 36 | return request({ 37 | url: '/lookup/delLookup', 38 | method: 'POST', 39 | data, 40 | }); 41 | }, 42 | getLookupValue(data?: object) { 43 | return request({ 44 | url: '/lookup/getLookupValue', 45 | method: 'POST', 46 | data 47 | }) 48 | }, 49 | saveOrUpdateLookupValue(data?: object) { 50 | return request({ 51 | url: '/lookup/saveOrUpdateLookupValue', 52 | method: 'POST', 53 | data 54 | }) 55 | }, 56 | delLookupValue(data?: object) { 57 | return request({ 58 | url: '/lookup/delLookupValue', 59 | method: 'POST', 60 | data 61 | }) 62 | } 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /src/api/useSystemApi/menu.ts: -------------------------------------------------------------------------------- 1 | import request from '/@/utils/request'; 2 | 3 | /** 4 | * 菜单接口 5 | * @method getAllMenus 获取菜单接口,路由格式 6 | * @method allMenu 获取菜单接口,平铺 7 | * @method saveOrUpdateMenu 更新保存菜单 8 | */ 9 | export function useMenuApi() { 10 | return { 11 | // 获取所有菜单,嵌套 12 | getAllMenus: (data?: object) => { 13 | return request({ 14 | url: '/menu/getAllMenus', 15 | method: 'POST', 16 | data, 17 | }); 18 | }, 19 | //后去所有菜单,平铺 20 | allMenu: (params?: object) => { 21 | return request({ 22 | url: '/menu/allMenu', 23 | method: 'POST', 24 | params, 25 | }); 26 | }, 27 | // 新增修改 28 | saveOrUpdate(data?: object) { 29 | return request({ 30 | url: '/menu/saveOrUpdate', 31 | method: 'POST', 32 | data 33 | }) 34 | }, 35 | // 删除 36 | deleted(data?: object) { 37 | return request({ 38 | url: '/menu/deleted', 39 | method: 'POST', 40 | data 41 | }) 42 | } 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /src/api/useSystemApi/roles.ts: -------------------------------------------------------------------------------- 1 | import request from '/@/utils/request'; 2 | 3 | /** 4 | * 用户接口 5 | * @method getUserList 获取用户列表 6 | * @method allMenu 获取菜单接口,平铺 7 | * @method saveOrUpdateMenu 更新保存菜单 8 | */ 9 | export function useRolesApi() { 10 | return { 11 | getList: (data?: object) => { 12 | return request({ 13 | url: '/roles/list', 14 | method: 'POST', 15 | data, 16 | }); 17 | }, 18 | saveOrUpdate(data?: object) { 19 | return request({ 20 | url: '/roles/saveOrUpdate', 21 | method: 'POST', 22 | data 23 | }) 24 | }, 25 | deleted(data?: object) { 26 | return request({ 27 | url: '/roles/deleted', 28 | method: 'POST', 29 | data 30 | }) 31 | } 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/api/useSystemApi/statistic.ts: -------------------------------------------------------------------------------- 1 | import request from '/@/utils/request'; 2 | 3 | /** 4 | * 统计接口 5 | */ 6 | export function useStatisticsApi() { 7 | return { 8 | countStatistic: () => { 9 | return request({ 10 | url: '/statistic/countStatistic', 11 | method: 'POST', 12 | data: {} 13 | }); 14 | }, 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /src/api/useSystemApi/user.ts: -------------------------------------------------------------------------------- 1 | import request from '/@/utils/request'; 2 | 3 | /** 4 | * 用户接口 5 | * @method getUserList 获取用户列表 6 | * @method allMenu 获取菜单接口,平铺 7 | * @method saveOrUpdateMenu 更新保存菜单 8 | */ 9 | export function useUserApi() { 10 | return { 11 | signIn: (data: object) => { 12 | return request({ 13 | url: '/user/login', 14 | method: 'POST', 15 | data, 16 | }); 17 | }, 18 | logout: () => { 19 | return request({ 20 | url: '/user/logout', 21 | method: 'POST', 22 | }); 23 | }, 24 | getList: (data?: object) => { 25 | return request({ 26 | url: '/user/list', 27 | method: 'POST', 28 | data, 29 | }); 30 | }, 31 | saveOrUpdate(data?: object) { 32 | return request({ 33 | url: '/user/saveOrUpdate', 34 | method: 'POST', 35 | data 36 | }) 37 | }, 38 | deleted(data?: object) { 39 | return request({ 40 | url: '/user/deleted', 41 | method: 'POST', 42 | data 43 | }) 44 | } 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baizunxian/zero_autotest_front/70e52bbb7d8a0d6e418d1417ed3271a7ea6bcb3d/src/assets/logo.png -------------------------------------------------------------------------------- /src/components/DefDialog/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | // width 单位px 4 | // 最小宽度360,最大宽度1200,计算公式:16*2 + 160*N + (N-1)*8 5 | // 2: 360, 3: 528, 4: 696, 5: 864, 6: 1032, 7: 1200 6 | 7 | 8 | const DefDialog = Vue.component('def-dialog', { 9 | functional: true, 10 | render (h, self) { 11 | self.props.width = self.props.width || '360px' 12 | if (typeof self.props.width === 'number') { 13 | if (self.props.width > 1) { 14 | let width = self.props.width * 212 + (self.props.width - 1) * 8 + 48 15 | self.props.width = `${width > 1200 ? 1200 : width}px` 16 | } else { 17 | self.props.width = '360px' 18 | } 19 | } 20 | if (self.props.hasOwnProperty('view')) { 21 | self.props.customClass = 'def-dialog-dynamic' 22 | } 23 | if (!self.props.hasOwnProperty('closeOnClickModal')) { 24 | self.props.closeOnClickModal = false 25 | } 26 | if (!self.props.hasOwnProperty('closeOnPressEscape')) { 27 | self.props.closeOnPressEscape = false 28 | } 29 | if (!self.props.hasOwnProperty('appendToBody') || self.props.appendToBody === false) { 30 | if (!self.props.hasOwnProperty('modalAppendToBody')) { 31 | self.props.modalAppendToBody = false 32 | } 33 | const wrapperClassName = 'el-dialog__wrapper def-dialog__modal-in-tab' 34 | self.data.staticClass = self.data.staticClass ? `${wrapperClassName} ${self.data.staticClass}` : wrapperClassName 35 | } 36 | // if (!self.props.hasOwnProperty('appendToBody')) { 37 | // self.props.appendToBody = true 38 | // } 39 | let directives = self.data.directives || [] 40 | self.data.directives = [...directives, { name: 'drag' }, { name: 'next' }] 41 | let onClose = self.listeners.close 42 | let $on = { 43 | close: () => { 44 | // 关闭时发送事件:解绑动态组件,解决再次打开时数据缓存的问题 45 | if (self.listeners['update:view']) { 46 | self.listeners['update:view'](null) 47 | } 48 | if (onClose) { 49 | onClose() 50 | } 51 | } 52 | } 53 | self.data.on = { ...self.data.on, ...self.listeners, ...$on, _popupId: 'popup-127'} 54 | self.data.attrs = { ...self.data.attrs, ...self.props } 55 | return h( 56 | 'el-dialog', 57 | self.data, 58 | self.children && self.children.map(t => { 59 | if (t.data && t.data.attrs) { 60 | t.data.attrs = { ...t.data.attrs, ...(t.componentOptions && t.componentOptions.propsData)} 61 | } 62 | if (t.data && t.data.on === undefined) { 63 | t.data.on = t.data.on || (t.componentOptions && t.componentOptions.listeners) 64 | } 65 | return h( 66 | (t.componentOptions && t.componentOptions.tag) || t.tag, 67 | t.data, 68 | t.children || (t.componentOptions && t.componentOptions.children) 69 | ) 70 | }) 71 | ) 72 | } 73 | }) 74 | 75 | export default DefDialog 76 | -------------------------------------------------------------------------------- /src/components/DefDialog/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 23 | 24 | 25 | 36 | -------------------------------------------------------------------------------- /src/components/Pagination/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 87 | 88 | 97 | -------------------------------------------------------------------------------- /src/components/QueryTable/index.vue: -------------------------------------------------------------------------------- 1 | 46 | -------------------------------------------------------------------------------- /src/components/svgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 46 | -------------------------------------------------------------------------------- /src/config/config.ts: -------------------------------------------------------------------------------- 1 | //自动化平台后端域名 2 | 3 | let url = null 4 | let webSocketUri = null 5 | 6 | export const getEnv = () => { 7 | if (window.location.href.includes('xiaobaicodes.com') || window.location.href.includes('42.192.38.108')) return 'prd' //prd 8 | return 'dev' //本地环境 9 | } 10 | 11 | const env = getEnv() 12 | 13 | export const initConfig = () => { 14 | if (env === 'prd') { 15 | url = 'https://xiaobaicodes.com:8888' 16 | } else { 17 | url = 'http://127.0.0.1:8012' 18 | } 19 | } 20 | 21 | initConfig() 22 | 23 | //自动化平台后端接口 24 | export const BaseUrl = url + '/api' 25 | const WebSocketUrl = ((window.location.protocol === 'https:') ? 'wss' : 'wss') + '://' + webSocketUri + '/ws/message' 26 | export default { 27 | BaseUrl, 28 | env, 29 | WebSocketUrl, 30 | } -------------------------------------------------------------------------------- /src/icons/indexSvg/add_case.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/indexSvg/bronze_medal.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/indexSvg/case_run_count.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/indexSvg/case_svg.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/indexSvg/exc_count.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/indexSvg/gold_medal.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/indexSvg/project_svg.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/indexSvg/silver_medal.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/indexSvg/suite_svg.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/indexSvg/user_login.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/layout/component/header.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 33 | -------------------------------------------------------------------------------- /src/layout/component/main.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 92 | -------------------------------------------------------------------------------- /src/layout/footer/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 34 | 35 | 47 | -------------------------------------------------------------------------------- /src/layout/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 56 | -------------------------------------------------------------------------------- /src/layout/logo/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 43 | 44 | 99 | -------------------------------------------------------------------------------- /src/layout/main/classic.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 37 | -------------------------------------------------------------------------------- /src/layout/main/columns.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 40 | 41 | -------------------------------------------------------------------------------- /src/layout/main/defaults.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 45 | -------------------------------------------------------------------------------- /src/layout/main/transverse.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 17 | -------------------------------------------------------------------------------- /src/layout/navBars/breadcrumb/closeFull.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 31 | 32 | 64 | -------------------------------------------------------------------------------- /src/layout/navBars/breadcrumb/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 105 | 106 | 116 | -------------------------------------------------------------------------------- /src/layout/navBars/breadcrumb/search.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 112 | 113 | 129 | -------------------------------------------------------------------------------- /src/layout/navBars/breadcrumb/userNews.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 63 | 64 | 130 | -------------------------------------------------------------------------------- /src/layout/navBars/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 29 | 30 | 38 | -------------------------------------------------------------------------------- /src/layout/navBars/tagsView/contextmenu.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 118 | 119 | 133 | -------------------------------------------------------------------------------- /src/layout/navMenu/subItem.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 48 | -------------------------------------------------------------------------------- /src/layout/navMenu/vertical.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 99 | -------------------------------------------------------------------------------- /src/layout/routerView/iframes.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 61 | -------------------------------------------------------------------------------- /src/layout/routerView/link.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 60 | -------------------------------------------------------------------------------- /src/layout/routerView/parent.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 95 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import {createApp} from 'vue'; 2 | import App from './App.vue'; 3 | import router from './router'; 4 | import {key, store} from './store'; 5 | import {directive} from '/@/utils/directive'; 6 | import other from '/@/utils/other'; 7 | 8 | import ElementPlus from 'element-plus'; 9 | import 'element-plus/dist/index.css'; 10 | import '/@/theme/index.scss'; 11 | import mitt from 'mitt'; 12 | 13 | const app = createApp(App); 14 | 15 | directive(app); 16 | other.elSvg(app); 17 | 18 | app.use(router).use(store, key).use(ElementPlus,{size: 'small'}).mount('#app'); 19 | 20 | app.config.globalProperties.mittBus = mitt(); 21 | -------------------------------------------------------------------------------- /src/router/backEnd.ts: -------------------------------------------------------------------------------- 1 | import {store} from '/@/store/index.ts'; 2 | import {Session} from '/@/utils/storage'; 3 | import {NextLoading} from '/@/utils/loading'; 4 | import {setAddRoute, setFilterMenuAndCacheTagsViewRoutes} from '/@/router/index'; 5 | import {dynamicRoutes} from '/@/router/route'; 6 | // import {useMenuApi} from '/@/api/menu/index'; 7 | 8 | // const menuApi = useMenuApi(); 9 | 10 | const layouModules: any = import.meta.glob('../layout/routerView/*.{vue,tsx}'); 11 | const viewsModules: any = import.meta.glob('../views/**/*.{vue,tsx}'); 12 | /** 13 | * 获取目录下的 .vue、.tsx 全部文件 14 | * @method import.meta.glob 15 | * @link 参考:https://cn.vitejs.dev/guide/features.html#json 16 | */ 17 | const dynamicViewsModules: Record = Object.assign({}, {...layouModules}, {...viewsModules}); 18 | 19 | /** 20 | * 后端控制路由:初始化方法,防止刷新时路由丢失 21 | * @method NextLoading 界面 loading 动画开始执行 22 | * @method store.dispatch('userInfos/setUserInfos') 触发初始化用户信息 23 | * @method store.dispatch('requestOldRoutes/setBackEndControlRoutes') 存储接口原始路由(未处理component),根据需求选择使用 24 | * @method setAddRoute 添加动态路由 25 | * @method setFilterMenuAndCacheTagsViewRoutes 设置递归过滤有权限的路由到 vuex routesList 中(已处理成多级嵌套路由)及缓存多级嵌套数组处理后的一维数组 26 | */ 27 | export async function initBackEndControlRoutes() { 28 | // 界面 loading 动画开始执行 29 | if (window.nextLoading === undefined) NextLoading.start(); 30 | // 无 token 停止执行下一步 31 | if (!Session.get('token')) return false; 32 | // 触发初始化用户信息 33 | store.dispatch('userInfos/setUserInfos'); 34 | store.dispatch('lookup/setLookup'); 35 | // 获取路由菜单数据 36 | const res = await getBackEndControlRoutes(); 37 | // 存储接口原始路由(未处理component),根据需求选择使用 38 | store.dispatch('requestOldRoutes/setBackEndControlRoutes', JSON.parse(JSON.stringify(res))); 39 | // 处理路由(components),替换 dynamicRoutes(/@/router/route)第一个顶级 children 的路由 40 | dynamicRoutes[0].children = await backEndComponent(res); 41 | // 添加动态路由 42 | await setAddRoute(); 43 | // 设置递归过滤有权限的路由到 vuex routesList 中(已处理成多级嵌套路由)及缓存多级嵌套数组处理后的一维数组 44 | setFilterMenuAndCacheTagsViewRoutes(); 45 | } 46 | 47 | /** 48 | * 请求后端路由菜单接口 49 | * @description isRequestRoutes 为 true,则开启后端控制路由 50 | * @returns 返回后端路由菜单数据 51 | */ 52 | export function getBackEndControlRoutes() { 53 | console.log('store.state.userInfos.userInfos.menus', store.state.userInfos.userInfos.menus) 54 | return store.state.userInfos.userInfos.menus; 55 | // 模拟 admin 与 test 56 | // const auth = store.state.userInfos.userInfos.roles[0]; 57 | // 管理员 admin 58 | // if (auth === 'admin') return menuApi.getMenuAdmin(); 59 | // 其它用户 test 60 | // else return menuApi.getMenuTest(); 61 | } 62 | 63 | /** 64 | * 重新请求后端路由菜单接口 65 | * @description 用于菜单管理界面刷新菜单(未进行测试) 66 | * @description 路径:/src/views/system/menu/components/addMenu.vue 67 | */ 68 | export function setBackEndControlRefreshRoutes() { 69 | getBackEndControlRoutes(); 70 | } 71 | 72 | /** 73 | * 后端路由 components 转换 74 | * @param routes 后端返回的路由表数组 75 | * @returns 返回处理成函数后的 components 76 | */ 77 | export function backEndComponent(routes: any) { 78 | if (!routes) return; 79 | return routes.map((item: any) => { 80 | if (item.component) item.component = dynamicImport(dynamicViewsModules, item.component as string); 81 | item.children && backEndComponent(item.children); 82 | return item; 83 | }); 84 | } 85 | 86 | /** 87 | * 后端路由 components 转换函数 88 | * @param dynamicViewsModules 获取目录下的 .vue、.tsx 全部文件 89 | * @param component 当前要处理项 components 90 | * @returns 返回处理成函数后的 components 91 | */ 92 | export function dynamicImport(dynamicViewsModules: Record, component: string) { 93 | const keys = Object.keys(dynamicViewsModules); 94 | const matchKeys = keys.filter((key) => { 95 | const k = key.replace(/..\/views|../, ''); 96 | return k.startsWith(`${component}`) || k.startsWith(`/${component}`); 97 | }); 98 | if (matchKeys?.length === 1) { 99 | const matchKey = matchKeys[0]; 100 | return dynamicViewsModules[matchKey]; 101 | } 102 | if (matchKeys?.length > 1) { 103 | return false; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/router/frontEnd.ts: -------------------------------------------------------------------------------- 1 | import { store } from '/@/store/index'; 2 | import { Session } from '/@/utils/storage'; 3 | import { NextLoading } from '/@/utils/loading'; 4 | import { setAddRoute, setFilterMenuAndCacheTagsViewRoutes } from '/@/router/index'; 5 | 6 | /** 7 | * 前端控制路由:初始化方法,防止刷新时路由丢失 8 | * @method NextLoading 界面 loading 动画开始执行 9 | * @method store.dispatch('userInfos/setUserInfos') 触发初始化用户信息 10 | * @method setAddRoute 添加动态路由 11 | * @method setFilterMenuAndCacheTagsViewRoutes 设置递归过滤有权限的路由到 vuex routesList 中(已处理成多级嵌套路由)及缓存多级嵌套数组处理后的一维数组 12 | */ 13 | export async function initFrontEndControlRoutes() { 14 | // 界面 loading 动画开始执行 15 | if (window.nextLoading === undefined) NextLoading.start(); 16 | // 无 token 停止执行下一步 17 | if (!Session.get('token')) return false; 18 | // 触发初始化用户信息 19 | store.dispatch('userInfos/setUserInfos'); 20 | // 添加动态路由 21 | await setAddRoute(); 22 | // 设置递归过滤有权限的路由到 vuex routesList 中(已处理成多级嵌套路由)及缓存多级嵌套数组处理后的一维数组 23 | setFilterMenuAndCacheTagsViewRoutes(); 24 | } 25 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import {InjectionKey} from 'vue'; 2 | import {createStore, Store, useStore as baseUseStore} from 'vuex'; 3 | import {RootStateTypes} from '/@/store/interface'; 4 | 5 | // Vite supports importing multiple modules from the file system using the special import.meta.glob function 6 | // see https://cn.vitejs.dev/guide/features.html#glob-import 7 | const modulesFiles = import.meta.globEager('./modules/*.ts'); 8 | const pathList: string[] = []; 9 | 10 | for (const path in modulesFiles) { 11 | pathList.push(path); 12 | } 13 | 14 | const modules = pathList.reduce((modules: { [x: string]: any }, modulePath: string) => { 15 | const moduleName = modulePath.replace(/^\.\/modules\/(.*)\.\w+$/, '$1'); 16 | const value = modulesFiles[modulePath]; 17 | modules[moduleName] = value.default; 18 | return modules; 19 | }, {}); 20 | 21 | export const key: InjectionKey> = Symbol(); 22 | 23 | export const store = createStore({modules}); 24 | 25 | export function useStore() { 26 | return baseUseStore(key); 27 | } 28 | -------------------------------------------------------------------------------- /src/store/interface/index.ts: -------------------------------------------------------------------------------- 1 | // 接口类型声明 2 | 3 | // 布局配置 4 | 5 | export interface ThemeConfigState { 6 | themeConfig: { 7 | isDrawer: boolean; 8 | primary: string; 9 | topBar: string; 10 | topBarColor: string; 11 | isTopBarColorGradual: boolean; 12 | menuBar: string; 13 | menuBarColor: string; 14 | isMenuBarColorGradual: boolean; 15 | columnsMenuBar: string; 16 | columnsMenuBarColor: string; 17 | isColumnsMenuBarColorGradual: boolean; 18 | isCollapse: boolean; 19 | isUniqueOpened: boolean; 20 | isFixedHeader: boolean; 21 | isFixedHeaderChange: boolean; 22 | isClassicSplitMenu: boolean; 23 | isLockScreen: boolean; 24 | lockScreenTime: number; 25 | isShowLogo: boolean; 26 | isShowLogoChange: boolean; 27 | isBreadcrumb: boolean; 28 | isTagsview: boolean; 29 | isBreadcrumbIcon: boolean; 30 | isTagsviewIcon: boolean; 31 | isCacheTagsView: boolean; 32 | isSortableTagsView: boolean; 33 | isShareTagsView: boolean; 34 | isFooter: boolean; 35 | isGrayscale: boolean; 36 | isInvert: boolean; 37 | isIsDark: boolean; 38 | isWartermark: boolean; 39 | wartermarkText: string; 40 | tagsStyle: string; 41 | animation: string; 42 | columnsAsideStyle: string; 43 | columnsAsideLayout: string; 44 | layout: string; 45 | isRequestRoutes: boolean; 46 | globalTitle: string; 47 | globalViceTitle: string; 48 | globalComponentSize: string; 49 | }; 50 | } 51 | 52 | // 路由列表 53 | export interface RoutesListState { 54 | routesList: object[]; 55 | isColumnsMenuHover: Boolean; 56 | isColumnsNavHover: Boolean; 57 | } 58 | 59 | // 路由缓存列表 60 | export interface KeepAliveNamesState { 61 | keepAliveNames: string[]; 62 | } 63 | 64 | // TagsView 路由列表 65 | export interface TagsViewRoutesState { 66 | tagsViewRoutes: object[]; 67 | isTagsViewCurrenFull: Boolean; 68 | } 69 | 70 | // 用户信息 71 | export interface UserInfosState { 72 | userInfos: { 73 | id: any; 74 | authBtnList: string[]; 75 | photo: string; 76 | roles: string[]; 77 | menus: object[]; 78 | time: number; 79 | username: string; 80 | nickname: string; 81 | user_type: any; 82 | }; 83 | } 84 | 85 | // 后端返回原始路由(未处理时) 86 | export interface RequestOldRoutesState { 87 | requestOldRoutes: object[]; 88 | } 89 | 90 | // 主接口(顶级类型声明) 91 | export interface RootStateTypes { 92 | themeConfig: ThemeConfigState; 93 | routesList: RoutesListState; 94 | keepAliveNames: KeepAliveNamesState; 95 | tagsViewRoutes: TagsViewRoutesState; 96 | userInfos: UserInfosState; 97 | lookup: any; 98 | requestOldRoutes: RequestOldRoutesState; 99 | } 100 | -------------------------------------------------------------------------------- /src/store/modules/keepAliveNames.ts: -------------------------------------------------------------------------------- 1 | import { Module } from 'vuex'; 2 | import { KeepAliveNamesState, RootStateTypes } from '/@/store/interface/index'; 3 | 4 | const keepAliveNamesModule: Module = { 5 | namespaced: true, 6 | state: { 7 | keepAliveNames: [], 8 | }, 9 | mutations: { 10 | // 设置路由缓存(name字段) 11 | getCacheKeepAlive(state: any, data: Array) { 12 | state.keepAliveNames = data; 13 | }, 14 | }, 15 | actions: { 16 | // 设置路由缓存(name字段) 17 | async setCacheKeepAlive({ commit }, data: Array) { 18 | commit('getCacheKeepAlive', data); 19 | }, 20 | }, 21 | }; 22 | 23 | export default keepAliveNamesModule; 24 | -------------------------------------------------------------------------------- /src/store/modules/lookup.ts: -------------------------------------------------------------------------------- 1 | import {Module} from 'vuex'; 2 | import {useLookupApi} from "/@/api/useSystemApi/lookup"; 3 | 4 | const lookupInfoModule: Module = { 5 | namespaced: true, 6 | state: { 7 | lookup: {}, 8 | }, 9 | mutations: { 10 | // 设置数据字典 11 | getLookup(state, data: any) { 12 | state.lookup = data; 13 | }, 14 | }, 15 | actions: { 16 | // 设置数据字典 17 | async setLookup({commit}) { 18 | useLookupApi().getAllLookup() 19 | .then((res: any) => { 20 | commit('getLookup', res.data); 21 | }) 22 | }, 23 | }, 24 | }; 25 | 26 | export default lookupInfoModule; 27 | -------------------------------------------------------------------------------- /src/store/modules/requestOldRoutes.ts: -------------------------------------------------------------------------------- 1 | import { Module } from 'vuex'; 2 | import { RequestOldRoutesState, RootStateTypes } from '/@/store/interface/index'; 3 | 4 | const requestOldRoutesModule: Module = { 5 | namespaced: true, 6 | state: { 7 | requestOldRoutes: [], 8 | }, 9 | mutations: { 10 | // 后端控制路由 11 | getBackEndControlRoutes(state: any, data: object) { 12 | state.requestOldRoutes = data; 13 | }, 14 | }, 15 | actions: { 16 | // 后端控制路由 17 | setBackEndControlRoutes({ commit }, routes: Array) { 18 | commit('getBackEndControlRoutes', routes); 19 | }, 20 | }, 21 | }; 22 | 23 | export default requestOldRoutesModule; 24 | -------------------------------------------------------------------------------- /src/store/modules/routesList.ts: -------------------------------------------------------------------------------- 1 | import { Module } from 'vuex'; 2 | import { RoutesListState, RootStateTypes } from '/@/store/interface'; 3 | 4 | const routesListModule: Module = { 5 | namespaced: true, 6 | state: { 7 | routesList: [], 8 | isColumnsMenuHover: false, 9 | isColumnsNavHover: false, 10 | }, 11 | mutations: { 12 | // 设置路由,菜单中使用到 13 | getRoutesList(state: any, data: Array) { 14 | state.routesList = data; 15 | }, 16 | // 设置分栏布局,鼠标是否移入移出(菜单) 17 | getColumnsMenuHover(state: any, bool: Boolean) { 18 | state.isColumnsMenuHover = bool; 19 | }, 20 | // 设置分栏布局,鼠标是否移入移出(导航) 21 | getColumnsNavHover(state: any, bool: Boolean) { 22 | state.isColumnsNavHover = bool; 23 | }, 24 | }, 25 | actions: { 26 | // 设置路由,菜单中使用到 27 | async setRoutesList({ commit }, data: any) { 28 | commit('getRoutesList', data); 29 | }, 30 | // 设置分栏布局,鼠标是否移入移出(菜单) 31 | async setColumnsMenuHover({ commit }, bool: Boolean) { 32 | commit('getColumnsMenuHover', bool); 33 | }, 34 | // 设置分栏布局,鼠标是否移入移出(菜单) 35 | async setColumnsNavHover({ commit }, bool: Boolean) { 36 | commit('getColumnsNavHover', bool); 37 | }, 38 | }, 39 | }; 40 | 41 | export default routesListModule; 42 | -------------------------------------------------------------------------------- /src/store/modules/tagsViewRoutes.ts: -------------------------------------------------------------------------------- 1 | import { Module } from 'vuex'; 2 | import { TagsViewRoutesState, RootStateTypes } from '/@/store/interface/index'; 3 | import { Session } from '/@/utils/storage'; 4 | 5 | const tagsViewRoutesModule: Module = { 6 | namespaced: true, 7 | state: { 8 | tagsViewRoutes: [], 9 | isTagsViewCurrenFull: false, 10 | }, 11 | mutations: { 12 | // 设置 TagsView 路由 13 | getTagsViewRoutes(state: any, data: Array) { 14 | state.tagsViewRoutes = data; 15 | }, 16 | // 设置卡片全屏 17 | getCurrenFullscreen(state: any, bool: boolean) { 18 | Session.set('isTagsViewCurrenFull', bool); 19 | state.isTagsViewCurrenFull = bool; 20 | }, 21 | }, 22 | actions: { 23 | // 设置 TagsView 路由 24 | async setTagsViewRoutes({ commit }, data: Array) { 25 | commit('getTagsViewRoutes', data); 26 | }, 27 | // 设置卡片全屏 28 | setCurrenFullscreen({ commit }, bool: Boolean) { 29 | commit('getCurrenFullscreen', bool); 30 | }, 31 | }, 32 | }; 33 | 34 | export default tagsViewRoutesModule; 35 | -------------------------------------------------------------------------------- /src/store/modules/userInfos.ts: -------------------------------------------------------------------------------- 1 | import {Module} from 'vuex'; 2 | import {Session} from '/@/utils/storage'; 3 | import {RootStateTypes, UserInfosState} from '/@/store/interface/index'; 4 | 5 | const userInfosModule: Module = { 6 | namespaced: true, 7 | state: { 8 | userInfos: { 9 | id: null, 10 | authBtnList: [], 11 | photo: '', 12 | roles: [], 13 | menus: [], 14 | time: 0, 15 | username: '', 16 | nickname: '', 17 | user_type: null, 18 | }, 19 | }, 20 | mutations: { 21 | // 设置用户信息 22 | getUserInfos(state, data: any) { 23 | state.userInfos = data; 24 | }, 25 | }, 26 | actions: { 27 | // 设置用户信息 28 | async setUserInfos({commit}, data: UserInfosState) { 29 | if (data) { 30 | commit('getUserInfos', data); 31 | } else { 32 | if (Session.get('userInfo')) commit('getUserInfos', Session.get('userInfo')); 33 | } 34 | }, 35 | }, 36 | }; 37 | 38 | export default userInfosModule; 39 | -------------------------------------------------------------------------------- /src/theme/common/transition.scss: -------------------------------------------------------------------------------- 1 | /* 页面切换动画 2 | ------------------------------- */ 3 | .slide-right-enter-active, 4 | .slide-right-leave-active, 5 | .slide-left-enter-active, 6 | .slide-left-leave-active { 7 | will-change: transform; 8 | transition: all 0.3s ease; 9 | } 10 | // slide-right 11 | .slide-right-enter-from { 12 | opacity: 0; 13 | transform: translateX(-20px); 14 | } 15 | .slide-right-leave-to { 16 | opacity: 0; 17 | transform: translateX(20px); 18 | } 19 | // slide-left 20 | .slide-left-enter-from { 21 | @extend .slide-right-leave-to; 22 | } 23 | .slide-left-leave-to { 24 | @extend .slide-right-enter-from; 25 | } 26 | // opacitys 27 | .opacitys-enter-active, 28 | .opacitys-leave-active { 29 | will-change: transform; 30 | transition: all 0.3s ease; 31 | } 32 | .opacitys-enter-from, 33 | .opacitys-leave-to { 34 | opacity: 0; 35 | } 36 | 37 | /* Breadcrumb 面包屑过渡动画 38 | ------------------------------- */ 39 | .breadcrumb-enter-active, 40 | .breadcrumb-leave-active { 41 | transition: all 0.5s ease; 42 | } 43 | .breadcrumb-enter-from, 44 | .breadcrumb-leave-active { 45 | opacity: 0; 46 | transform: translateX(20px); 47 | } 48 | .breadcrumb-leave-active { 49 | position: absolute; 50 | z-index: -1; 51 | } 52 | 53 | /* logo 过渡动画 54 | ------------------------------- */ 55 | @keyframes logoAnimation { 56 | 0% { 57 | transform: scale(0); 58 | } 59 | 80% { 60 | transform: scale(1.2); 61 | } 62 | 100% { 63 | transform: scale(1); 64 | } 65 | } 66 | 67 | /* 404、401 过渡动画 68 | ------------------------------- */ 69 | @keyframes error-num { 70 | 0% { 71 | transform: translateY(60px); 72 | opacity: 0; 73 | } 74 | 100% { 75 | transform: translateY(0); 76 | opacity: 1; 77 | } 78 | } 79 | @keyframes error-img { 80 | 0% { 81 | opacity: 0; 82 | } 83 | 100% { 84 | opacity: 1; 85 | } 86 | } 87 | @keyframes error-img-two { 88 | 0% { 89 | opacity: 1; 90 | } 91 | 100% { 92 | opacity: 0; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/theme/iconSelector.scss: -------------------------------------------------------------------------------- 1 | /* Popover 弹出框(图标选择器) 2 | ------------------------------- */ 3 | .icon-selector-popper { 4 | padding: 0 !important; 5 | .icon-selector-warp { 6 | height: 260px; 7 | overflow: hidden; 8 | .icon-selector-warp-title { 9 | height: 40px; 10 | line-height: 40px; 11 | padding: 0 15px; 12 | .icon-selector-warp-title-tab { 13 | span { 14 | cursor: pointer; 15 | &:hover { 16 | color: var(--el-color-primary); 17 | text-decoration: underline; 18 | } 19 | } 20 | .span-active { 21 | color: var(--el-color-primary); 22 | text-decoration: underline; 23 | } 24 | } 25 | } 26 | .icon-selector-warp-row { 27 | height: 230px; 28 | overflow: hidden; 29 | border-top: var(--el-border-base); 30 | .el-row { 31 | padding: 15px; 32 | } 33 | .el-scrollbar__bar.is-horizontal { 34 | display: none; 35 | } 36 | .icon-selector-warp-item { 37 | display: flex; 38 | border: var(--el-border-base); 39 | padding: 5px; 40 | border-radius: 5px; 41 | margin-bottom: 10px; 42 | .icon-selector-warp-item-value { 43 | i { 44 | font-size: 20px; 45 | color: var(--el-text-color-regular); 46 | } 47 | } 48 | &:hover { 49 | cursor: pointer; 50 | background-color: var(--el-color-primary-light-9); 51 | border: 1px solid var(--el-color-primary-light-6); 52 | .icon-selector-warp-item-value { 53 | i { 54 | color: var(--el-color-primary); 55 | } 56 | } 57 | } 58 | } 59 | .icon-selector-active { 60 | background-color: var(--el-color-primary-light-9); 61 | border: 1px solid var(--el-color-primary-light-6); 62 | .icon-selector-warp-item-value { 63 | i { 64 | color: var(--el-color-primary); 65 | } 66 | } 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/theme/index.scss: -------------------------------------------------------------------------------- 1 | @import './app.scss'; 2 | @import 'common/transition.scss'; 3 | @import './other.scss'; 4 | @import './element.scss'; 5 | @import './iconSelector.scss'; 6 | @import './media/media.scss'; 7 | @import './waves.scss'; 8 | @import './dark.scss'; 9 | -------------------------------------------------------------------------------- /src/theme/loading.scss: -------------------------------------------------------------------------------- 1 | .loading-next { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | .loading-next .loading-next-box { 6 | position: absolute; 7 | top: 50%; 8 | left: 50%; 9 | transform: translate(-50%, -50%); 10 | } 11 | .loading-next .loading-next-box-warp { 12 | width: 80px; 13 | height: 80px; 14 | } 15 | .loading-next .loading-next-box-warp .loading-next-box-item { 16 | width: 33.333333%; 17 | height: 33.333333%; 18 | background: var(--el-color-primary); 19 | float: left; 20 | animation: loading-next-animation 1.2s infinite ease; 21 | border-radius: 1px; 22 | } 23 | .loading-next .loading-next-box-warp .loading-next-box-item:nth-child(7) { 24 | animation-delay: 0s; 25 | } 26 | .loading-next .loading-next-box-warp .loading-next-box-item:nth-child(4), 27 | .loading-next .loading-next-box-warp .loading-next-box-item:nth-child(8) { 28 | animation-delay: 0.1s; 29 | } 30 | .loading-next .loading-next-box-warp .loading-next-box-item:nth-child(1), 31 | .loading-next .loading-next-box-warp .loading-next-box-item:nth-child(5), 32 | .loading-next .loading-next-box-warp .loading-next-box-item:nth-child(9) { 33 | animation-delay: 0.2s; 34 | } 35 | .loading-next .loading-next-box-warp .loading-next-box-item:nth-child(2), 36 | .loading-next .loading-next-box-warp .loading-next-box-item:nth-child(6) { 37 | animation-delay: 0.3s; 38 | } 39 | .loading-next .loading-next-box-warp .loading-next-box-item:nth-child(3) { 40 | animation-delay: 0.4s; 41 | } 42 | @keyframes loading-next-animation { 43 | 0%, 44 | 70%, 45 | 100% { 46 | transform: scale3D(1, 1, 1); 47 | } 48 | 35% { 49 | transform: scale3D(0, 0, 1); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/theme/media/chart.scss: -------------------------------------------------------------------------------- 1 | @import './index.scss'; 2 | 3 | /* 页面宽度小于768px 4 | ------------------------------- */ 5 | @media screen and (max-width: $sm) { 6 | .big-data-down-left { 7 | width: 100% !important; 8 | flex-direction: unset !important; 9 | flex-wrap: wrap; 10 | .flex-warp-item { 11 | min-height: 196.24px; 12 | padding: 0 7.5px 15px 15px !important; 13 | .flex-warp-item-box { 14 | border: none !important; 15 | border-bottom: 1px solid #ebeef5 !important; 16 | } 17 | } 18 | } 19 | .big-data-down-center { 20 | width: 100% !important; 21 | .big-data-down-center-one, 22 | .big-data-down-center-two { 23 | min-height: 196.24px; 24 | padding-left: 15px !important; 25 | .big-data-down-center-one-content { 26 | border: none !important; 27 | border-bottom: 1px solid #ebeef5 !important; 28 | } 29 | .flex-warp-item-box { 30 | @extend .big-data-down-center-one-content; 31 | } 32 | } 33 | } 34 | .big-data-down-right { 35 | .flex-warp-item { 36 | .flex-warp-item-box { 37 | border: none !important; 38 | border-bottom: 1px solid #ebeef5 !important; 39 | } 40 | &:nth-of-type(2) { 41 | padding-left: 15px !important; 42 | } 43 | &:last-of-type { 44 | .flex-warp-item-box { 45 | border: none !important; 46 | } 47 | } 48 | } 49 | } 50 | } 51 | 52 | /* 页面宽度大于768px小于1200px 53 | ------------------------------- */ 54 | @media screen and (min-width: $sm) and (max-width: $lg) { 55 | .chart-warp-bottom { 56 | .big-data-down-left { 57 | width: 50% !important; 58 | } 59 | .big-data-down-center { 60 | width: 50% !important; 61 | } 62 | .big-data-down-right { 63 | .flex-warp-item { 64 | width: 50% !important; 65 | &:nth-of-type(2) { 66 | padding-left: 7.5px !important; 67 | } 68 | } 69 | } 70 | } 71 | } 72 | 73 | /* 页面宽度小于1200px 74 | ------------------------------- */ 75 | @media screen and (max-width: $lg) { 76 | .chart-warp-top { 77 | .up-left { 78 | display: none; 79 | } 80 | } 81 | .chart-warp-bottom { 82 | overflow-y: auto !important; 83 | flex-wrap: wrap; 84 | .big-data-down-right { 85 | width: 100% !important; 86 | flex-direction: unset !important; 87 | flex-wrap: wrap; 88 | .flex-warp-item { 89 | min-height: 196.24px; 90 | padding: 0 7.5px 15px 15px !important; 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/theme/media/cityLinkage.scss: -------------------------------------------------------------------------------- 1 | @import './index.scss'; 2 | 3 | /* 页面宽度小于576px 4 | ------------------------------- */ 5 | @media screen and (max-width: $xs) { 6 | .el-cascader__dropdown.el-popper { 7 | overflow: auto; 8 | max-width: 100%; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/theme/media/date.scss: -------------------------------------------------------------------------------- 1 | @import './index.scss'; 2 | 3 | /* 页面宽度小于768px 4 | ------------------------------- */ 5 | @media screen and (max-width: $sm) { 6 | // 时间选择器适配 7 | .el-date-range-picker { 8 | width: 100vw; 9 | .el-picker-panel__body { 10 | min-width: 100%; 11 | .el-date-range-picker__content { 12 | .el-date-range-picker__header div { 13 | margin-left: 22px; 14 | margin-right: 0px; 15 | } 16 | & + .el-date-range-picker__content { 17 | .el-date-range-picker__header div { 18 | margin-left: 0px; 19 | margin-right: 22px; 20 | } 21 | } 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/theme/media/dialog.scss: -------------------------------------------------------------------------------- 1 | @import './index.scss'; 2 | 3 | /* 页面宽度小于800px 4 | ------------------------------- */ 5 | @media screen and (max-width: 800px) { 6 | .el-dialog { 7 | width: 90% !important; 8 | } 9 | .el-dialog.is-fullscreen { 10 | width: 100% !important; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/theme/media/error.scss: -------------------------------------------------------------------------------- 1 | @import './index.scss'; 2 | 3 | /* 页面宽度小于768px 4 | ------------------------------- */ 5 | @media screen and (max-width: $sm) { 6 | .error { 7 | .error-flex { 8 | flex-direction: column-reverse !important; 9 | height: auto !important; 10 | width: 100% !important; 11 | } 12 | .right, 13 | .left { 14 | flex: unset !important; 15 | display: flex !important; 16 | } 17 | .left-item { 18 | margin: auto !important; 19 | } 20 | .right img { 21 | max-width: 450px !important; 22 | @extend .left-item; 23 | } 24 | } 25 | } 26 | 27 | /* 页面宽度大于768px小于992px 28 | ------------------------------- */ 29 | @media screen and (min-width: $sm) and (max-width: $md) { 30 | .error { 31 | .error-flex { 32 | padding-left: 30px !important; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/theme/media/form.scss: -------------------------------------------------------------------------------- 1 | @import './index.scss'; 2 | 3 | /* 页面宽度小于576px 4 | ------------------------------- */ 5 | @media screen and (max-width: $xs) { 6 | .el-form-item__label { 7 | width: 100% !important; 8 | text-align: left !important; 9 | } 10 | .el-form-item__content { 11 | margin-left: 0 !important; 12 | } 13 | .el-form-item { 14 | display: unset !important; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/theme/media/home.scss: -------------------------------------------------------------------------------- 1 | @import './index.scss'; 2 | 3 | /* 页面宽度小于768px 4 | ------------------------------- */ 5 | @media screen and (max-width: $sm) { 6 | .home-media, 7 | .home-media-sm { 8 | margin-top: 15px; 9 | } 10 | } 11 | 12 | /* 页面宽度小于1200px 13 | ------------------------------- */ 14 | @media screen and (max-width: $lg) { 15 | .home-media-lg { 16 | margin-top: 15px; 17 | } 18 | .home-monitor { 19 | .flex-warp-item { 20 | width: 33.33% !important; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/theme/media/index.scss: -------------------------------------------------------------------------------- 1 | /* 栅格布局(媒体查询变量) 2 | * https://developer.mozilla.org/zh-CN/docs/Learn/CSS/CSS_layout/Media_queries 3 | * $us ≥376px 响应式栅格 4 | * $xs ≥576px 响应式栅格 5 | * $sm ≥768px 响应式栅格 6 | * $md ≥992px 响应式栅格 7 | * $lg ≥1200px 响应式栅格 8 | * $xl ≥1920px 响应式栅格 9 | ------------------------------- */ 10 | $us: 376px; 11 | $xs: 576px; 12 | $sm: 768px; 13 | $md: 992px; 14 | $lg: 1200px; 15 | $xl: 1920px; 16 | -------------------------------------------------------------------------------- /src/theme/media/layout.scss: -------------------------------------------------------------------------------- 1 | @import './index.scss'; 2 | 3 | /* 页面宽度小于576px 4 | ------------------------------- */ 5 | @media screen and (max-width: $xs) { 6 | // MessageBox 弹框 7 | .el-message-box { 8 | width: 80% !important; 9 | } 10 | } 11 | 12 | /* 页面宽度小于768px 13 | ------------------------------- */ 14 | @media screen and (max-width: $sm) { 15 | // Breadcrumb 面包屑 16 | .layout-navbars-breadcrumb-hide { 17 | display: none; 18 | } 19 | // 外链视图 20 | .layout-view-link { 21 | a { 22 | max-width: 80%; 23 | text-align: center; 24 | } 25 | } 26 | // 菜单搜索 27 | .layout-search-dialog { 28 | .el-autocomplete { 29 | width: 80% !important; 30 | } 31 | } 32 | } 33 | 34 | /* 页面宽度小于1000px 35 | ------------------------------- */ 36 | @media screen and (max-width: 1000px) { 37 | // 布局配置 38 | .layout-drawer-content-flex { 39 | position: relative; 40 | &::after { 41 | content: '手机版不支持切换布局'; 42 | position: absolute; 43 | top: 0; 44 | right: 0; 45 | bottom: 0; 46 | left: 0; 47 | z-index: 1; 48 | text-align: center; 49 | height: 140px; 50 | line-height: 140px; 51 | background: rgba(255, 255, 255, 0.9); 52 | color: #666666; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/theme/media/login.scss: -------------------------------------------------------------------------------- 1 | @import './index.scss'; 2 | 3 | /* 页面宽度小于992px 4 | ------------------------------- */ 5 | @media screen and (max-width: $lg) { 6 | .login-container { 7 | .login-icon-group { 8 | &::before { 9 | content: ''; 10 | height: 70% !important; 11 | transition: all 0.3s ease; 12 | } 13 | &::after { 14 | content: ''; 15 | width: 100px !important; 16 | height: 200px !important; 17 | transition: all 0.3s ease; 18 | } 19 | } 20 | } 21 | } 22 | 23 | /* 页面宽度小于992px 24 | ------------------------------- */ 25 | @media screen and (max-width: $md) { 26 | .login-content { 27 | right: unset !important; 28 | left: 50% !important; 29 | transform: translate(-50%, -50%) translate3d(0, 0, 0) !important; 30 | } 31 | } 32 | 33 | /* 页面宽度小于576px 34 | ------------------------------- */ 35 | @media screen and (max-width: $xs) { 36 | .login-container { 37 | .login-icon-group { 38 | display: none !important; 39 | } 40 | .login-content { 41 | width: 100% !important; 42 | height: 100% !important; 43 | padding: 20px 0 !important; 44 | border-radius: 0 !important; 45 | box-shadow: unset !important; 46 | border: none !important; 47 | } 48 | .el-form-item { 49 | display: flex !important; 50 | } 51 | } 52 | } 53 | 54 | /* 页面宽度小于375px 55 | ------------------------------- */ 56 | @media screen and (max-width: $us) { 57 | .login-container { 58 | .login-content-title { 59 | font-size: 18px !important; 60 | transition: all 0.3s ease; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/theme/media/media.scss: -------------------------------------------------------------------------------- 1 | @import './login.scss'; 2 | @import './error.scss'; 3 | @import './layout.scss'; 4 | @import './personal.scss'; 5 | @import './tagsView.scss'; 6 | @import './home.scss'; 7 | @import './chart.scss'; 8 | @import './form.scss'; 9 | @import './scrollbar.scss'; 10 | @import './pagination.scss'; 11 | @import './dialog.scss'; 12 | @import './cityLinkage.scss'; 13 | @import './date.scss'; 14 | -------------------------------------------------------------------------------- /src/theme/media/pagination.scss: -------------------------------------------------------------------------------- 1 | @import './index.scss'; 2 | 3 | /* 页面宽度小于576px 4 | ------------------------------- */ 5 | @media screen and (max-width: $xs) { 6 | .el-pager, 7 | .el-pagination__jump { 8 | display: none !important; 9 | } 10 | } 11 | 12 | // 默认居中对齐 13 | .el-pagination { 14 | text-align: center !important; 15 | } 16 | -------------------------------------------------------------------------------- /src/theme/media/personal.scss: -------------------------------------------------------------------------------- 1 | @import './index.scss'; 2 | 3 | /* 页面宽度小于768px 4 | ------------------------------- */ 5 | @media screen and (max-width: $sm) { 6 | .personal-info { 7 | padding-left: 0 !important; 8 | margin-top: 15px; 9 | } 10 | .personal-recommend-col { 11 | margin-bottom: 15px; 12 | &:last-of-type { 13 | margin-bottom: 0; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/theme/media/scrollbar.scss: -------------------------------------------------------------------------------- 1 | @import './index.scss'; 2 | 3 | /* 页面宽度小于768px 4 | ------------------------------- */ 5 | @media screen and (max-width: $sm) { 6 | // 滚动条的宽度 7 | ::-webkit-scrollbar { 8 | width: 3px !important; 9 | height: 3px !important; 10 | } 11 | ::-webkit-scrollbar-track-piece { 12 | background-color: var(--next-bg-main-color); 13 | } 14 | // 滚动条的设置 15 | ::-webkit-scrollbar-thumb { 16 | background-color: rgba(144, 147, 153, 0.3); 17 | background-clip: padding-box; 18 | min-height: 28px; 19 | border-radius: 5px; 20 | transition: 0.3s background-color; 21 | } 22 | ::-webkit-scrollbar-thumb:hover { 23 | background-color: rgba(144, 147, 153, 0.5); 24 | } 25 | // element plus scrollbar 26 | .el-scrollbar__bar.is-vertical { 27 | width: 2px !important; 28 | } 29 | .el-scrollbar__bar.is-horizontal { 30 | height: 2px !important; 31 | } 32 | } 33 | 34 | /* 页面宽度大于768px 35 | ------------------------------- */ 36 | @media screen and (min-width: 769px) { 37 | // 滚动条的宽度 38 | ::-webkit-scrollbar { 39 | width: 7px; 40 | height: 7px; 41 | } 42 | ::-webkit-scrollbar-track-piece { 43 | background-color: var(--next-bg-main-color); 44 | } 45 | // 滚动条的设置 46 | ::-webkit-scrollbar-thumb { 47 | background-color: rgba(144, 147, 153, 0.3); 48 | background-clip: padding-box; 49 | min-height: 28px; 50 | border-radius: 5px; 51 | transition: 0.3s background-color; 52 | } 53 | ::-webkit-scrollbar-thumb:hover { 54 | background-color: rgba(144, 147, 153, 0.5); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/theme/media/tagsView.scss: -------------------------------------------------------------------------------- 1 | @import './index.scss'; 2 | 3 | /* 页面宽度小于768px 4 | ------------------------------- */ 5 | @media screen and (max-width: $sm) { 6 | .tags-view-form { 7 | .tags-view-form-col { 8 | margin-bottom: 20px; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/theme/mixins/index.scss: -------------------------------------------------------------------------------- 1 | /* 第三方图标字体间距/大小设置 2 | ------------------------------- */ 3 | @mixin generalIcon { 4 | font-size: 14px !important; 5 | display: inline-block; 6 | vertical-align: middle; 7 | margin-right: 5px; 8 | width: 24px; 9 | text-align: center; 10 | } 11 | 12 | /* 文本不换行 13 | ------------------------------- */ 14 | @mixin text-no-wrap() { 15 | text-overflow: ellipsis; 16 | overflow: hidden; 17 | white-space: nowrap; 18 | } 19 | 20 | /* 多行文本溢出 21 | ------------------------------- */ 22 | @mixin text-ellipsis($line: 2) { 23 | overflow: hidden; 24 | word-break: break-all; 25 | text-overflow: ellipsis; 26 | display: -webkit-box; 27 | -webkit-line-clamp: $line; 28 | -webkit-box-orient: vertical; 29 | } 30 | 31 | /* 滚动条(页面未使用) div 中使用: 32 | ------------------------------- */ 33 | // .test { 34 | // @include scrollBar; 35 | // } 36 | @mixin scrollBar { 37 | // 滚动条凹槽的颜色,还可以设置边框属性 38 | &::-webkit-scrollbar-track-piece { 39 | background-color: #f8f8f8; 40 | } 41 | // 滚动条的宽度 42 | &::-webkit-scrollbar { 43 | width: 9px; 44 | height: 9px; 45 | } 46 | // 滚动条的设置 47 | &::-webkit-scrollbar-thumb { 48 | background-color: #dddddd; 49 | background-clip: padding-box; 50 | min-height: 28px; 51 | } 52 | &::-webkit-scrollbar-thumb:hover { 53 | background-color: #bbb; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/theme/other.scss: -------------------------------------------------------------------------------- 1 | /* wangeditor富文本编辑器 2 | ------------------------------- */ 3 | .w-e-toolbar { 4 | border: 1px solid #ebeef5 !important; 5 | border-bottom: 1px solid #ebeef5 !important; 6 | border-top-left-radius: 3px; 7 | border-top-right-radius: 3px; 8 | z-index: 2 !important; 9 | } 10 | .w-e-text-container { 11 | border: 1px solid #ebeef5 !important; 12 | border-top: none !important; 13 | border-bottom-left-radius: 3px; 14 | border-bottom-right-radius: 3px; 15 | z-index: 1 !important; 16 | } 17 | 18 | /* web端自定义截屏 19 | ------------------------------- */ 20 | #screenShotContainer { 21 | z-index: 9998 !important; 22 | } 23 | #toolPanel { 24 | height: 42px !important; 25 | } 26 | #optionPanel { 27 | height: 37px !important; 28 | } 29 | -------------------------------------------------------------------------------- /src/theme/waves.scss: -------------------------------------------------------------------------------- 1 | /* Waves v0.6.0 2 | * http://fian.my.id/Waves 3 | * 4 | * Copyright 2014 Alfiana E. Sibuea and other contributors 5 | * Released under the MIT license 6 | * https://github.com/fians/Waves/blob/master/LICENSE 7 | */ 8 | .waves-effect { 9 | position: relative; 10 | cursor: pointer; 11 | display: inline-block; 12 | overflow: hidden; 13 | -webkit-user-select: none; 14 | -moz-user-select: none; 15 | -ms-user-select: none; 16 | user-select: none; 17 | -webkit-tap-highlight-color: transparent; 18 | vertical-align: middle; 19 | z-index: 1; 20 | will-change: opacity, transform; 21 | transition: all 0.3s ease-out; 22 | } 23 | .waves-effect .waves-ripple { 24 | position: absolute; 25 | border-radius: 50%; 26 | width: 20px; 27 | height: 20px; 28 | margin-top: -10px; 29 | margin-left: -10px; 30 | opacity: 0; 31 | background: rgba(0, 0, 0, 0.2); 32 | transition: all 0.7s ease-out; 33 | transition-property: opacity, -webkit-transform; 34 | transition-property: transform, opacity; 35 | transition-property: transform, opacity, -webkit-transform; 36 | -webkit-transform: scale(0); 37 | transform: scale(0); 38 | pointer-events: none; 39 | } 40 | .waves-effect.waves-light .waves-ripple { 41 | background-color: rgba(255, 255, 255, 0.45); 42 | } 43 | .waves-effect.waves-red .waves-ripple { 44 | background-color: rgba(244, 67, 54, 0.7); 45 | } 46 | .waves-effect.waves-yellow .waves-ripple { 47 | background-color: rgba(255, 235, 59, 0.7); 48 | } 49 | .waves-effect.waves-orange .waves-ripple { 50 | background-color: rgba(255, 152, 0, 0.7); 51 | } 52 | .waves-effect.waves-purple .waves-ripple { 53 | background-color: rgba(156, 39, 176, 0.7); 54 | } 55 | .waves-effect.waves-green .waves-ripple { 56 | background-color: rgba(76, 175, 80, 0.7); 57 | } 58 | .waves-effect.waves-teal .waves-ripple { 59 | background-color: rgba(0, 150, 136, 0.7); 60 | } 61 | .waves-effect input[type='button'], 62 | .waves-effect input[type='reset'], 63 | .waves-effect input[type='submit'] { 64 | border: 0; 65 | font-style: normal; 66 | font-size: inherit; 67 | text-transform: inherit; 68 | background: none; 69 | } 70 | .waves-notransition { 71 | transition: none !important; 72 | } 73 | .waves-circle { 74 | -webkit-transform: translateZ(0); 75 | transform: translateZ(0); 76 | -webkit-mask-image: -webkit-radial-gradient(circle, #fff 100%, #000 100%); 77 | } 78 | .waves-input-wrapper { 79 | border-radius: 0.2em; 80 | vertical-align: bottom; 81 | } 82 | .waves-input-wrapper .waves-button-input { 83 | position: relative; 84 | top: 0; 85 | left: 0; 86 | z-index: 1; 87 | } 88 | .waves-circle { 89 | text-align: center; 90 | width: 2.5em; 91 | height: 2.5em; 92 | line-height: 2.5em; 93 | border-radius: 50%; 94 | -webkit-mask-image: none; 95 | } 96 | .waves-block { 97 | display: block; 98 | } 99 | a.waves-effect .waves-ripple { 100 | z-index: -1; 101 | } 102 | -------------------------------------------------------------------------------- /src/utils/arrayOperation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 判断两数组是否相同 3 | * @param news 新数据 4 | * @param old 源数据 5 | * @returns 两数组相同返回 `true`,反之则反 6 | */ 7 | export function judementSameArr(news: unknown[] | string[], old: string[]): boolean { 8 | let count = 0; 9 | const leng = old.length; 10 | for (let i in old) { 11 | for (let j in news) { 12 | if (old[i] === news[j]) count++; 13 | } 14 | } 15 | return count === leng ? true : false; 16 | } 17 | 18 | /** 19 | * 判断两个对象是否相同 20 | * @param a 要比较的对象一 21 | * @param b 要比较的对象二 22 | * @returns 相同返回 true,反之则反 23 | */ 24 | export function isObjectValueEqual(a: { [key: string]: any }, b: { [key: string]: any }) { 25 | if (!a || !b) return false; 26 | let aProps = Object.getOwnPropertyNames(a); 27 | let bProps = Object.getOwnPropertyNames(b); 28 | if (aProps.length != bProps.length) return false; 29 | for (let i = 0; i < aProps.length; i++) { 30 | let propName = aProps[i]; 31 | let propA = a[propName]; 32 | let propB = b[propName]; 33 | if (!b.hasOwnProperty(propName)) return false; 34 | if (propA instanceof Object) { 35 | if (!isObjectValueEqual(propA, propB)) return false; 36 | } else if (propA !== propB) { 37 | return false; 38 | } 39 | } 40 | return true; 41 | } 42 | -------------------------------------------------------------------------------- /src/utils/authDirective.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue'; 2 | import { store } from '/@/store/index.ts'; 3 | import { judementSameArr } from '/@/utils/arrayOperation'; 4 | 5 | /** 6 | * 用户权限指令 7 | * @directive 单个权限验证(v-auth="xxx") 8 | * @directive 多个权限验证,满足一个则显示(v-auths="[xxx,xxx]") 9 | * @directive 多个权限验证,全部满足则显示(v-auth-all="[xxx,xxx]") 10 | */ 11 | export function authDirective(app: App) { 12 | // 单个权限验证(v-auth="xxx") 13 | app.directive('auth', { 14 | mounted(el, binding) { 15 | if (!store.state.userInfos.userInfos.authBtnList.some((v: string) => v === binding.value)) el.parentNode.removeChild(el); 16 | }, 17 | }); 18 | // 多个权限验证,满足一个则显示(v-auths="[xxx,xxx]") 19 | app.directive('auths', { 20 | mounted(el, binding) { 21 | let flag = false; 22 | store.state.userInfos.userInfos.authBtnList.map((val: string) => { 23 | binding.value.map((v: string) => { 24 | if (val === v) flag = true; 25 | }); 26 | }); 27 | if (!flag) el.parentNode.removeChild(el); 28 | }, 29 | }); 30 | // 多个权限验证,全部满足则显示(v-auth-all="[xxx,xxx]") 31 | app.directive('auth-all', { 32 | mounted(el, binding) { 33 | const flag = judementSameArr(binding.value, store.state.userInfos.userInfos.authBtnList); 34 | if (!flag) el.parentNode.removeChild(el); 35 | }, 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /src/utils/authFunction.ts: -------------------------------------------------------------------------------- 1 | import { store } from '/@/store/index.ts'; 2 | import { judementSameArr } from '/@/utils/arrayOperation'; 3 | 4 | /** 5 | * 单个权限验证 6 | * @param value 权限值 7 | * @returns 有权限,返回 `true`,反之则反 8 | */ 9 | export function auth(value: string): boolean { 10 | return store.state.userInfos.userInfos.authBtnList.some((v: string) => v === value); 11 | } 12 | 13 | /** 14 | * 多个权限验证,满足一个则为 true 15 | * @param value 权限值 16 | * @returns 有权限,返回 `true`,反之则反 17 | */ 18 | export function auths(value: Array): boolean { 19 | let flag = false; 20 | store.state.userInfos.userInfos.authBtnList.map((val: string) => { 21 | value.map((v: string) => { 22 | if (val === v) flag = true; 23 | }); 24 | }); 25 | return flag; 26 | } 27 | 28 | /** 29 | * 多个权限验证,全部满足则为 true 30 | * @param value 权限值 31 | * @returns 有权限,返回 `true`,反之则反 32 | */ 33 | export function authAll(value: Array): boolean { 34 | return judementSameArr(value, store.state.userInfos.userInfos.authBtnList); 35 | } 36 | -------------------------------------------------------------------------------- /src/utils/commonFunction.ts: -------------------------------------------------------------------------------- 1 | // 通用函数 2 | import useClipboard from 'vue-clipboard3'; 3 | import { ElMessage } from 'element-plus'; 4 | import { formatDate } from '/@/utils/formatTime'; 5 | 6 | export default function () { 7 | const { toClipboard } = useClipboard(); 8 | //百分比格式化 9 | const percentFormat = (row: any, column: number, cellValue: any) => { 10 | return cellValue ? `${cellValue}%` : '-'; 11 | }; 12 | //列表日期时间格式化 13 | const dateFormatYMD = (row: any, column: number, cellValue: any) => { 14 | if (!cellValue) return '-'; 15 | return formatDate(new Date(cellValue), 'YYYY-mm-dd'); 16 | }; 17 | //列表日期时间格式化 18 | const dateFormatYMDHMS = (row: any, column: number, cellValue: any) => { 19 | if (!cellValue) return '-'; 20 | return formatDate(new Date(cellValue), 'YYYY-mm-dd HH:MM:SS'); 21 | }; 22 | //列表日期时间格式化 23 | const dateFormatHMS = (row: any, column: number, cellValue: any) => { 24 | if (!cellValue) return '-'; 25 | let time = 0; 26 | if (typeof row === 'number') time = row; 27 | if (typeof cellValue === 'number') time = cellValue; 28 | return formatDate(new Date(time * 1000), 'HH:MM:SS'); 29 | }; 30 | // 小数格式化 31 | const scaleFormat = (value: any = 0, scale: number = 4) => { 32 | return Number.parseFloat(value).toFixed(scale); 33 | }; 34 | // 小数格式化 35 | const scale2Format = (value: any = 0) => { 36 | return Number.parseFloat(value).toFixed(2); 37 | }; 38 | // 点击复制文本 39 | const copyText = (text: string) => { 40 | return new Promise((resolve, reject) => { 41 | try { 42 | //复制 43 | toClipboard(text); 44 | //下面可以设置复制成功的提示框等操作 45 | ElMessage.success('复制成功!'); 46 | resolve(text); 47 | } catch (e) { 48 | //复制失败 49 | ElMessage.error('复制失败!'); 50 | reject(e); 51 | } 52 | }); 53 | }; 54 | return { 55 | percentFormat, 56 | dateFormatYMD, 57 | dateFormatYMDHMS, 58 | dateFormatHMS, 59 | scaleFormat, 60 | scale2Format, 61 | copyText, 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /src/utils/directive.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue'; 2 | import { authDirective } from '/@/utils/authDirective'; 3 | import { wavesDirective, dragDirective } from '/@/utils/customDirective'; 4 | 5 | /** 6 | * 导出指令方法:v-xxx 7 | * @methods authDirective 用户权限指令,用法:v-auth 8 | * @methods wavesDirective 按钮波浪指令,用法:v-waves 9 | * @methods dragDirective 自定义拖动指令,用法:v-drag 10 | */ 11 | export function directive(app: App) { 12 | // 用户权限指令 13 | authDirective(app); 14 | // 按钮波浪指令 15 | wavesDirective(app); 16 | // 自定义拖动指令 17 | dragDirective(app); 18 | } 19 | -------------------------------------------------------------------------------- /src/utils/getStyleSheets.ts: -------------------------------------------------------------------------------- 1 | import { nextTick } from 'vue'; 2 | import * as svg from '@element-plus/icons-vue'; 3 | 4 | // 获取阿里字体图标 5 | const getAlicdnIconfont = () => { 6 | return new Promise((resolve, reject) => { 7 | nextTick(() => { 8 | const styles: any = document.styleSheets; 9 | let sheetsList = []; 10 | let sheetsIconList = []; 11 | for (let i = 0; i < styles.length; i++) { 12 | if (styles[i].href && styles[i].href.indexOf('at.alicdn.com') > -1) { 13 | sheetsList.push(styles[i]); 14 | } 15 | } 16 | for (let i = 0; i < sheetsList.length; i++) { 17 | for (let j = 0; j < sheetsList[i].cssRules.length; j++) { 18 | if (sheetsList[i].cssRules[j].selectorText && sheetsList[i].cssRules[j].selectorText.indexOf('.icon-') > -1) { 19 | sheetsIconList.push( 20 | `${sheetsList[i].cssRules[j].selectorText.substring(1, sheetsList[i].cssRules[j].selectorText.length).replace(/\:\:before/gi, '')}` 21 | ); 22 | } 23 | } 24 | } 25 | if (sheetsIconList.length > 0) resolve(sheetsIconList); 26 | else reject('未获取到值,请刷新重试'); 27 | }); 28 | }); 29 | }; 30 | 31 | // 初始化获取 css 样式,获取 element plus 自带 svg 图标,增加了 ele- 前缀,使用时:ele-Aim 32 | const getElementPlusIconfont = () => { 33 | return new Promise((resolve, reject) => { 34 | nextTick(() => { 35 | const icons = svg as any; 36 | const sheetsIconList = []; 37 | for (const i in icons) { 38 | sheetsIconList.push(`ele-${icons[i].name}`); 39 | } 40 | if (sheetsIconList.length > 0) resolve(sheetsIconList); 41 | else reject('未获取到值,请刷新重试'); 42 | }); 43 | }); 44 | }; 45 | 46 | // 初始化获取 css 样式,这里使用 fontawesome 的图标 47 | const getAwesomeIconfont = () => { 48 | return new Promise((resolve, reject) => { 49 | nextTick(() => { 50 | const styles: any = document.styleSheets; 51 | let sheetsList = []; 52 | let sheetsIconList = []; 53 | for (let i = 0; i < styles.length; i++) { 54 | if (styles[i].href && styles[i].href.indexOf('netdna.bootstrapcdn.com') > -1) { 55 | sheetsList.push(styles[i]); 56 | } 57 | } 58 | for (let i = 0; i < sheetsList.length; i++) { 59 | for (let j = 0; j < sheetsList[i].cssRules.length; j++) { 60 | if ( 61 | sheetsList[i].cssRules[j].selectorText && 62 | sheetsList[i].cssRules[j].selectorText.indexOf('.fa-') === 0 && 63 | sheetsList[i].cssRules[j].selectorText.indexOf(',') === -1 64 | ) { 65 | if (/::before/.test(sheetsList[i].cssRules[j].selectorText)) { 66 | sheetsIconList.push( 67 | `${sheetsList[i].cssRules[j].selectorText.substring(1, sheetsList[i].cssRules[j].selectorText.length).replace(/\:\:before/gi, '')}` 68 | ); 69 | } 70 | } 71 | } 72 | } 73 | if (sheetsIconList.length > 0) resolve(sheetsIconList.reverse()); 74 | else reject('未获取到值,请刷新重试'); 75 | }); 76 | }); 77 | }; 78 | 79 | /** 80 | * 获取字体图标 `document.styleSheets` 81 | * @method ali 获取阿里字体图标 `` 82 | * @method ele 获取 element plus 自带图标 `` 83 | * @method ali 获取 fontawesome 的图标 `` 84 | */ 85 | const initIconfont = { 86 | // iconfont 87 | ali: () => { 88 | return getAlicdnIconfont(); 89 | }, 90 | // element plus 91 | ele: () => { 92 | return getElementPlusIconfont(); 93 | }, 94 | // fontawesome 95 | awe: () => { 96 | return getAwesomeIconfont(); 97 | }, 98 | }; 99 | 100 | // 导出方法 101 | export default initIconfont; 102 | -------------------------------------------------------------------------------- /src/utils/loading.ts: -------------------------------------------------------------------------------- 1 | import { nextTick } from 'vue'; 2 | import '/@/theme/loading.scss'; 3 | 4 | /** 5 | * 页面全局 Loading 6 | * @method start 创建 loading 7 | * @method done 移除 loading 8 | */ 9 | export const NextLoading = { 10 | // 创建 loading 11 | start: () => { 12 | const bodys: Element = document.body; 13 | const div = document.createElement('div'); 14 | div.setAttribute('class', 'loading-next'); 15 | const htmls = ` 16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | `; 30 | div.innerHTML = htmls; 31 | bodys.insertBefore(div, bodys.childNodes[0]); 32 | window.nextLoading = true; 33 | }, 34 | // 移除 loading 35 | done: () => { 36 | nextTick(() => { 37 | window.nextLoading = false; 38 | const el = document.querySelector('.loading-next'); 39 | el?.parentNode?.removeChild(el); 40 | }); 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /src/utils/lookup.ts: -------------------------------------------------------------------------------- 1 | import {store} from "/@/store"; 2 | 3 | /** 4 | * 返回数据字典 5 | * @param code 数据字典编码 6 | * @param lookup_code 数据字典值编码 7 | * @returns return 对应的字符串,否则返回原值 8 | */ 9 | export function formatLookup(code: string, lookup_code: string): string { 10 | let lookup: any = store.state.lookup.lookup[code] 11 | if (!lookup) return lookup_code; 12 | if (lookup[lookup_code]) return lookup[lookup_code]; 13 | return lookup_code; 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/other.ts: -------------------------------------------------------------------------------- 1 | import type {App} from 'vue'; 2 | import {nextTick} from 'vue'; 3 | import * as svg from '@element-plus/icons-vue'; 4 | import router from '/@/router/index'; 5 | import {store} from '/@/store'; 6 | import {Local} from '/@/utils/storage'; 7 | import svgIcon from '/@/components/svgIcon/index.vue'; 8 | 9 | /** 10 | * 导出全局注册 element plus svg 图标 11 | * @param app vue 实例 12 | * @description 使用:https://element-plus.gitee.io/zh-CN/component/icon.html 13 | */ 14 | export function elSvg(app: App) { 15 | const icons = svg as any; 16 | for (const i in icons) { 17 | app.component(`ele-${icons[i].name}`, icons[i]); 18 | } 19 | app.component('SvgIcon', svgIcon); 20 | } 21 | 22 | /** 23 | * 设置浏览器标题国际化 24 | * @method const title = useTitle(); ==> title() 25 | */ 26 | export function useTitle() { 27 | nextTick(() => { 28 | let webTitle = ''; 29 | let globalTitle: string = store.state.themeConfig.themeConfig.globalTitle; 30 | webTitle = router.currentRoute.value.meta.title as any; 31 | document.title = `${webTitle} - ${globalTitle}` || globalTitle; 32 | }); 33 | } 34 | 35 | /** 36 | * 图片懒加载 37 | * @param el dom 目标元素 38 | * @param arr 列表数据 39 | * @description data-xxx 属性用于存储页面或应用程序的私有自定义数据 40 | */ 41 | export const lazyImg = (el: any, arr: any) => { 42 | const io = new IntersectionObserver((res) => { 43 | res.forEach((v: any) => { 44 | if (v.isIntersecting) { 45 | const {img, key} = v.target.dataset; 46 | v.target.src = img; 47 | v.target.onload = () => { 48 | io.unobserve(v.target); 49 | arr[key]['loading'] = false; 50 | }; 51 | } 52 | }); 53 | }); 54 | nextTick(() => { 55 | document.querySelectorAll(el).forEach((img) => io.observe(img)); 56 | }); 57 | }; 58 | 59 | /** 60 | * 全局组件大小 61 | * @returns 返回 `window.localStorage` 中读取的缓存值 `globalComponentSize` 62 | */ 63 | export const globalComponentSize: string = Local.get('themeConfig')?.globalComponentSize || store.state.themeConfig.themeConfig?.globalComponentSize; 64 | 65 | /** 66 | * 对象深克隆 67 | * @param obj 源对象 68 | * @returns 克隆后的对象 69 | */ 70 | export function deepClone(obj: any) { 71 | let newObj: any; 72 | try { 73 | newObj = obj.push ? [] : {}; 74 | } catch (error) { 75 | newObj = {}; 76 | } 77 | for (let attr in obj) { 78 | if (typeof obj[attr] === 'object') { 79 | newObj[attr] = deepClone(obj[attr]); 80 | } else { 81 | newObj[attr] = obj[attr]; 82 | } 83 | } 84 | return newObj; 85 | } 86 | 87 | /** 88 | * 判断是否是移动端 89 | */ 90 | export function isMobile() { 91 | if ( 92 | navigator.userAgent.match( 93 | /('phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone')/i 94 | ) 95 | ) { 96 | return true; 97 | } else { 98 | return false; 99 | } 100 | } 101 | 102 | /** 103 | * 判断数组对象中所有属性是否为空,为空则删除当前行对象 104 | * @description @感谢大黄 105 | * @param list 数组对象 106 | * @returns 删除空值后的数组对象 107 | */ 108 | export function handleEmpty(list: any) { 109 | const arr = []; 110 | for (const i in list) { 111 | const d = []; 112 | for (const j in list[i]) { 113 | d.push(list[i][j]); 114 | } 115 | const leng = d.filter((item) => item === '').length; 116 | if (leng !== d.length) { 117 | arr.push(list[i]); 118 | } 119 | } 120 | return arr; 121 | } 122 | 123 | /** 124 | * 统一批量导出 125 | * @method elSvg 导出全局注册 element plus svg 图标 126 | * @method useTitle 设置浏览器标题国际化 127 | * @method lazyImg 图片懒加载 128 | * @method globalComponentSize element plus 全局组件大小 129 | * @method deepClone 对象深克隆 130 | * @method isMobile 判断是否是移动端 131 | * @method handleEmpty 判断数组对象中所有属性是否为空,为空则删除当前行对象 132 | */ 133 | const other = { 134 | elSvg: (app: App) => { 135 | elSvg(app); 136 | }, 137 | useTitle: () => { 138 | useTitle(); 139 | }, 140 | lazyImg: (el: any, arr: any) => { 141 | lazyImg(el, arr); 142 | }, 143 | globalComponentSize, 144 | deepClone: (obj: any) => { 145 | deepClone(obj); 146 | }, 147 | isMobile: () => { 148 | return isMobile(); 149 | }, 150 | handleEmpty: (list: any) => { 151 | return handleEmpty(list); 152 | }, 153 | }; 154 | 155 | // 统一批量导出 156 | export default other; 157 | -------------------------------------------------------------------------------- /src/utils/request.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import {ElMessage, ElMessageBox} from 'element-plus'; 3 | import {Session} from '/@/utils/storage'; 4 | import {BaseUrl} from '/@/config/config'; 5 | 6 | // 配置新建一个 axios 实例 7 | const service = axios.create({ 8 | baseURL: BaseUrl, 9 | timeout: 500000, 10 | headers: {'Content-Type': 'application/json'}, 11 | }); 12 | 13 | // 添加请求拦截器 14 | service.interceptors.request.use( 15 | (config) => { 16 | // 在发送请求之前做些什么 token 17 | if (Session.get('token')) { 18 | (config.headers).common['token'] = `${Session.get('token')}`; 19 | } 20 | return config; 21 | }, 22 | (error) => { 23 | // 对请求错误做些什么 24 | return Promise.reject(error); 25 | } 26 | ); 27 | 28 | // 添加响应拦截器 29 | service.interceptors.response.use( 30 | (response) => { 31 | // 对响应数据做点什么 32 | const res = response.data; 33 | if (res.code && res.code !== 0) { 34 | // `token` 过期或者账号已在别处登录 35 | if (res.code === 10201 || res.code === 4001) { 36 | Session.clear(); // 清除浏览器全部临时缓存 37 | window.location.href = '/'; // 去登录页 38 | ElMessageBox.alert('登录信息已失效,请重新登录', {}) 39 | .then(() => { 40 | }) 41 | .catch(() => { 42 | }); 43 | } else { 44 | ElMessage.error(res.msg || '接口错误!'); 45 | } 46 | return Promise.reject(service.interceptors.response); 47 | } else { 48 | return response.data; 49 | } 50 | }, 51 | (error) => { 52 | // 对响应错误做点什么 53 | if (error.message.indexOf('timeout') != -1) { 54 | ElMessage.error('网络超时'); 55 | } else if (error.message == 'Network Error') { 56 | ElMessage.error('网络连接错误'); 57 | } else { 58 | if (error.response.data) ElMessage.error(error.response.statusText); 59 | else ElMessage.error('接口路径找不到'); 60 | } 61 | return Promise.reject(error); 62 | } 63 | ); 64 | 65 | // 导出 axios 实例 66 | export default service; 67 | -------------------------------------------------------------------------------- /src/utils/setIconfont.ts: -------------------------------------------------------------------------------- 1 | // 字体图标 url 2 | const cssCdnUrlList: Array = [ 3 | '//at.alicdn.com/t/font_2298093_y6u00apwst.css', 4 | '//netdna.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css', 5 | ]; 6 | // 第三方 js url 7 | const jsCdnUrlList: Array = []; 8 | 9 | // 动态批量设置字体图标 10 | export function setCssCdn() { 11 | if (cssCdnUrlList.length <= 0) return false; 12 | cssCdnUrlList.map((v) => { 13 | let link = document.createElement('link'); 14 | link.rel = 'stylesheet'; 15 | link.href = v; 16 | link.crossOrigin = 'anonymous'; 17 | document.getElementsByTagName('head')[0].appendChild(link); 18 | }); 19 | } 20 | 21 | // 动态批量设置第三方js 22 | export function setJsCdn() { 23 | if (jsCdnUrlList.length <= 0) return false; 24 | jsCdnUrlList.map((v) => { 25 | let link = document.createElement('script'); 26 | link.src = v; 27 | document.body.appendChild(link); 28 | }); 29 | } 30 | 31 | /** 32 | * 批量设置字体图标、动态js 33 | * @method cssCdn 动态批量设置字体图标 34 | * @method jsCdn 动态批量设置第三方js 35 | */ 36 | const setIntroduction = { 37 | // 设置css 38 | cssCdn: () => { 39 | setCssCdn(); 40 | }, 41 | // 设置js 42 | jsCdn: () => { 43 | setJsCdn(); 44 | }, 45 | }; 46 | 47 | // 导出函数方法 48 | export default setIntroduction; 49 | -------------------------------------------------------------------------------- /src/utils/storage.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * window.localStorage 浏览器永久缓存 3 | * @method set 设置永久缓存 4 | * @method get 获取永久缓存 5 | * @method remove 移除永久缓存 6 | * @method clear 移除全部永久缓存 7 | */ 8 | export const Local = { 9 | // 设置永久缓存 10 | set(key: string, val: any) { 11 | window.localStorage.setItem(key, JSON.stringify(val)); 12 | }, 13 | // 获取永久缓存 14 | get(key: string) { 15 | let json: any = window.localStorage.getItem(key); 16 | return JSON.parse(json); 17 | }, 18 | // 移除永久缓存 19 | remove(key: string) { 20 | window.localStorage.removeItem(key); 21 | }, 22 | // 移除全部永久缓存 23 | clear() { 24 | window.localStorage.clear(); 25 | }, 26 | }; 27 | 28 | /** 29 | * window.sessionStorage 浏览器临时缓存 30 | * @method set 设置临时缓存 31 | * @method get 获取临时缓存 32 | * @method remove 移除临时缓存 33 | * @method clear 移除全部临时缓存 34 | */ 35 | export const Session = { 36 | // 设置临时缓存 37 | set(key: string, val: any) { 38 | window.sessionStorage.setItem(key, JSON.stringify(val)); 39 | }, 40 | // 获取临时缓存 41 | get(key: string) { 42 | let json: any = window.sessionStorage.getItem(key); 43 | return JSON.parse(json); 44 | }, 45 | // 移除临时缓存 46 | remove(key: string) { 47 | window.sessionStorage.removeItem(key); 48 | }, 49 | // 移除全部临时缓存 50 | clear() { 51 | window.sessionStorage.clear(); 52 | }, 53 | }; 54 | -------------------------------------------------------------------------------- /src/utils/textToImg.js: -------------------------------------------------------------------------------- 1 | export const textToImg = (str) => { 2 | let name = str.substring(0, 1), fsize = 70; 3 | if (new RegExp("[A-Za-z]+").test(name)) { 4 | name = name.toUpperCase() 5 | } 6 | let fontSize = 62; 7 | let fontWeight = "bold"; 8 | let canvas = document.createElement("canvas", {display: 'none', id: 'canvas_user_img'}); 9 | canvas.width = 120; 10 | canvas.height = 120; 11 | let context = canvas.getContext("2d"); 12 | context.fillStyle = getBG(); 13 | context.fillRect(0, 0, canvas.width, canvas.height); 14 | context.fillStyle = "#FFF"; 15 | context.font = fontWeight + " " + fsize + "px sans-serif"; 16 | context.textAlign = "center"; 17 | context.textBaseline = "middle"; 18 | context.fillText(name, fontSize, fontSize); 19 | return canvas.toDataURL("image/png", 1) 20 | } 21 | 22 | export const getBG = () => { 23 | let bgArray = ["#1abc9c", "#2ecc71", "#3498db", "#9b59b6", "#34495e", 24 | "#16a085", "#27ae60", "#2980b9", "#8e44ad", "#2c3e50", "#f1c40f", 25 | "#e67e22", "#e74c3c", "#eca0f1", "#95a5a6", "#f39c12", "#d35400", 26 | "#c0392b", "#bdc3c7", "#7f8c8d"]; 27 | return bgArray[Math.floor(Math.random() * bgArray.length)]; 28 | }; 29 | -------------------------------------------------------------------------------- /src/utils/theme.ts: -------------------------------------------------------------------------------- 1 | import { ElMessage } from 'element-plus'; 2 | 3 | /** 4 | * hex颜色转rgb颜色 5 | * @param str 颜色值字符串 6 | * @returns 返回处理后的颜色值 7 | */ 8 | export function hexToRgb(str: any) { 9 | let hexs: any = ''; 10 | let reg = /^\#?[0-9A-Fa-f]{6}$/; 11 | if (!reg.test(str)) return ElMessage.warning('输入错误的hex'); 12 | str = str.replace('#', ''); 13 | hexs = str.match(/../g); 14 | for (let i = 0; i < 3; i++) hexs[i] = parseInt(hexs[i], 16); 15 | return hexs; 16 | } 17 | 18 | /** 19 | * rgb颜色转Hex颜色 20 | * @param r 代表红色 21 | * @param g 代表绿色 22 | * @param b 代表蓝色 23 | * @returns 返回处理后的颜色值 24 | */ 25 | export function rgbToHex(r: any, g: any, b: any) { 26 | let reg = /^\d{1,3}$/; 27 | if (!reg.test(r) || !reg.test(g) || !reg.test(b)) return ElMessage.warning('输入错误的rgb颜色值'); 28 | let hexs = [r.toString(16), g.toString(16), b.toString(16)]; 29 | for (let i = 0; i < 3; i++) if (hexs[i].length == 1) hexs[i] = `0${hexs[i]}`; 30 | return `#${hexs.join('')}`; 31 | } 32 | 33 | /** 34 | * 加深颜色值 35 | * @param color 颜色值字符串 36 | * @param level 加深的程度,限0-1之间 37 | * @returns 返回处理后的颜色值 38 | */ 39 | export function getDarkColor(color: string, level: number) { 40 | let reg = /^\#?[0-9A-Fa-f]{6}$/; 41 | if (!reg.test(color)) return ElMessage.warning('输入错误的hex颜色值'); 42 | let rgb = hexToRgb(color); 43 | for (let i = 0; i < 3; i++) rgb[i] = Math.floor(rgb[i] * (1 - level)); 44 | return rgbToHex(rgb[0], rgb[1], rgb[2]); 45 | } 46 | 47 | /** 48 | * 变浅颜色值 49 | * @param color 颜色值字符串 50 | * @param level 加深的程度,限0-1之间 51 | * @returns 返回处理后的颜色值 52 | */ 53 | export function getLightColor(color: string, level: number) { 54 | let reg = /^\#?[0-9A-Fa-f]{6}$/; 55 | if (!reg.test(color)) return ElMessage.warning('输入错误的hex颜色值'); 56 | let rgb = hexToRgb(color); 57 | for (let i = 0; i < 3; i++) rgb[i] = Math.floor((255 - rgb[i]) * level + rgb[i]); 58 | return rgbToHex(rgb[0], rgb[1], rgb[2]); 59 | } 60 | -------------------------------------------------------------------------------- /src/utils/wartermark.ts: -------------------------------------------------------------------------------- 1 | // 页面添加水印效果 2 | const setWatermark = (str: string) => { 3 | const id = '1.23452384164.123412416'; 4 | if (document.getElementById(id) !== null) document.body.removeChild(document.getElementById(id)); 5 | const can = document.createElement('canvas'); 6 | can.width = 200; 7 | can.height = 130; 8 | const cans: any = can.getContext('2d'); 9 | cans.rotate((-20 * Math.PI) / 180); 10 | cans.font = '12px Vedana'; 11 | cans.fillStyle = 'rgba(200, 200, 200, 0.30)'; 12 | cans.textBaseline = 'Middle'; 13 | cans.fillText(str, can.width / 10, can.height / 2); 14 | const div = document.createElement('div'); 15 | div.id = id; 16 | div.style.pointerEvents = 'none'; 17 | div.style.top = '15px'; 18 | div.style.left = '0px'; 19 | div.style.position = 'fixed'; 20 | div.style.zIndex = '10000000'; 21 | div.style.width = `${document.documentElement.clientWidth}px`; 22 | div.style.height = `${document.documentElement.clientHeight}px`; 23 | div.style.background = `url(${can.toDataURL('image/png')}) left top repeat`; 24 | document.body.appendChild(div); 25 | return id; 26 | }; 27 | 28 | /** 29 | * 页面添加水印效果 30 | * @method set 设置水印 31 | * @method del 删除水印 32 | */ 33 | const watermark = { 34 | // 设置水印 35 | set: (str: string) => { 36 | let id = setWatermark(str); 37 | if (document.getElementById(id) === null) id = setWatermark(str); 38 | }, 39 | // 删除水印 40 | del: () => { 41 | let id = '1.23452384164.123412416'; 42 | if (document.getElementById(id) !== null) document.body.removeChild(document.getElementById(id)); 43 | }, 44 | }; 45 | 46 | // 导出方法 47 | export default watermark; 48 | -------------------------------------------------------------------------------- /src/views/api/case/components/outputList.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 114 | 115 | -------------------------------------------------------------------------------- /src/views/api/case/components/skip.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 99 | 100 | -------------------------------------------------------------------------------- /src/views/api/debugTalk/components/editDebugtalk.vue: -------------------------------------------------------------------------------- 1 | 18 | 131 | 132 | -------------------------------------------------------------------------------- /src/views/api/debugTalk/components/saveOrUpdate.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 99 | 100 | 120 | -------------------------------------------------------------------------------- /src/views/api/env/components/saveOrUpdate.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 111 | -------------------------------------------------------------------------------- /src/views/error/401.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 39 | 40 | 97 | -------------------------------------------------------------------------------- /src/views/error/404.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 37 | 38 | 95 | -------------------------------------------------------------------------------- /src/views/home/components/countStatistics.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 70 | 71 | 135 | -------------------------------------------------------------------------------- /src/views/home/components/extTopStatistics.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 120 | 121 | -------------------------------------------------------------------------------- /src/views/home/components/extTrendStatistics.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 136 | 137 | -------------------------------------------------------------------------------- /src/views/home/components/projectStatistics.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 152 | 153 | -------------------------------------------------------------------------------- /src/views/home/components/runTrendStatistics.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 147 | 148 | -------------------------------------------------------------------------------- /src/views/home/components/suiteTopStatistics.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 45 | 68 | 69 | -------------------------------------------------------------------------------- /src/views/home/index.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 91 | 92 | -------------------------------------------------------------------------------- /src/views/login/component/mobile.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 59 | 60 | 87 | -------------------------------------------------------------------------------- /src/views/login/component/scan.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 34 | 35 | 59 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import vue from '@vitejs/plugin-vue'; 2 | import { resolve } from 'path'; 3 | import { defineConfig, loadEnv, ConfigEnv } from 'vite'; 4 | 5 | const pathResolve = (dir: string): any => { 6 | return resolve(__dirname, '.', dir); 7 | }; 8 | 9 | const alias: Record = { 10 | '/@': pathResolve('./src/'), 11 | }; 12 | 13 | const viteConfig = defineConfig((mode: ConfigEnv) => { 14 | const env = loadEnv(mode.mode, process.cwd()); 15 | return { 16 | plugins: [vue()], 17 | root: process.cwd(), 18 | resolve: { alias }, 19 | base: mode.command === 'serve' ? '/' : env.VITE_PUBLIC_PATH, 20 | server: { 21 | host: '0.0.0.0', 22 | port: env.VITE_PORT as unknown as number, 23 | open: env.VITE_OPEN, 24 | proxy: { 25 | // '/gitee': { 26 | // target: 'https://gitee.com', 27 | // ws: true, 28 | // changeOrigin: true, 29 | // rewrite: (path) => path.replace(/^\/gitee/, ''), 30 | // }, 31 | }, 32 | }, 33 | build: { 34 | outDir: 'dist', 35 | sourcemap: false, 36 | chunkSizeWarningLimit: 1500, 37 | rollupOptions: { 38 | output: { 39 | entryFileNames: `assets/[name].${new Date().getTime()}.js`, 40 | chunkFileNames: `assets/[name].${new Date().getTime()}.js`, 41 | assetFileNames: `assets/[name].${new Date().getTime()}.[ext]`, 42 | compact: true, 43 | manualChunks: { 44 | vue: ['vue', 'vue-router', 'vuex'], 45 | echarts: ['echarts'], 46 | }, 47 | }, 48 | }, 49 | terserOptions: { 50 | compress: { 51 | drop_console: true, 52 | drop_debugger: true, 53 | }, 54 | ie8: true, 55 | output: { 56 | comments: true, 57 | }, 58 | }, 59 | }, 60 | css: { 61 | postcss: { 62 | plugins: [ 63 | { 64 | postcssPlugin: 'internal:charset-removal', 65 | AtRule: { 66 | charset: (atRule) => { 67 | if (atRule.name === 'charset') { 68 | atRule.remove(); 69 | } 70 | }, 71 | }, 72 | }, 73 | ], 74 | }, 75 | }, 76 | }; 77 | }); 78 | 79 | export default viteConfig; 80 | --------------------------------------------------------------------------------