├── .browserslistrc ├── .eslintignore ├── src ├── layouts │ ├── EmptyLayout.vue │ ├── export.js │ └── components │ │ ├── VabAd │ │ └── index.vue │ │ ├── VabBreadcrumb │ │ └── index.vue │ │ ├── VabLogo │ │ └── index.vue │ │ ├── VabAppMain │ │ └── index.vue │ │ ├── VabAvatar │ │ └── index.vue │ │ ├── VabNavBar │ │ └── index.vue │ │ └── VabThemeBar │ │ └── index.vue ├── assets │ ├── ewm.png │ ├── pro.png │ ├── ewm_vip.png │ ├── comparison │ │ ├── left.jpg │ │ └── right.jpg │ ├── error_images │ │ ├── 401.png │ │ ├── 404.png │ │ └── cloud.png │ ├── qr_logo │ │ └── lqr_logo.png │ └── login_images │ │ └── background.jpg ├── styles │ ├── themes │ │ └── default.scss │ ├── transition.scss │ ├── spinner │ │ ├── inner-circles.css │ │ ├── gauge.css │ │ └── dots.css │ ├── variables.scss │ ├── vab.scss │ ├── loading.scss │ └── normalize.scss ├── plugins │ ├── vabIcon.js │ ├── element.js │ ├── index.js │ └── support.js ├── api │ ├── publicKey.js │ ├── router.js │ ├── ad.js │ └── user.js ├── config │ ├── settings.js │ ├── index.js │ ├── theme.config.js │ ├── net.config.js │ ├── setting.config.js │ └── permission.js ├── App.vue ├── remixIcon │ ├── svg │ │ ├── vuejs-fill.svg │ │ └── qq-fill.svg │ └── index.js ├── utils │ ├── pageTitle.js │ ├── permission.js │ ├── errorLog.js │ ├── static.js │ ├── accessToken.js │ ├── handleRoutes.js │ ├── encrypt.js │ ├── request.js │ ├── vab.js │ ├── validate.js │ └── index.js ├── colorfulIcon │ ├── index.js │ └── svg │ │ ├── alphabetical_sorting.svg │ │ └── vab.svg ├── store │ ├── modules │ │ ├── table.js │ │ ├── errorLog.js │ │ ├── routes.js │ │ ├── settings.js │ │ ├── user.js │ │ └── tabsBar.js │ └── index.js ├── main.js ├── router │ └── index.js └── views │ ├── index │ └── index.vue │ ├── 401.vue │ └── 404.vue ├── babel.config.js ├── layouts ├── package.json ├── Permissions │ ├── index.js │ └── permissions.js ├── VabQueryForm │ ├── VabQueryFormTopPanel.vue │ ├── VabQueryFormBottomPanel.vue │ ├── VabQueryFormLeftPanel.vue │ ├── VabQueryFormRightPanel.vue │ └── index.vue ├── prettier.config.js ├── index.js ├── VabFullScreenBar │ └── index.vue ├── VabColorfullIcon │ └── index.vue ├── VabSideBar │ ├── components │ │ ├── VabSubmenu.vue │ │ ├── VabMenuItem.vue │ │ └── VabSideBarItem.vue │ └── index.vue ├── VabRemixIcon │ └── index.vue ├── VabGithubCorner │ └── index.vue ├── VabErrorLog │ └── index.vue └── VabTopBar │ └── index.vue ├── public ├── favicon.ico ├── favicon_backup.ico ├── static │ └── css │ │ └── loading.css └── index.html ├── .stylelintrc.js ├── vab-icon └── package.json ├── webstorm.config.js ├── .gitattributes ├── .editorconfig ├── push.sh ├── .gitignore ├── deploy.sh ├── mock ├── index.js ├── controller │ ├── ad.js │ ├── router.js │ └── user.js ├── utils │ └── index.js └── mockServer.js ├── prettier.config.js ├── plopfile.js ├── .eslintrc.js ├── .vscode └── settings.json ├── http └── mock.http ├── package.json ├── vue.config.js └── README.md /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | src/assets 2 | src/icons 3 | public 4 | dist 5 | node_modules 6 | -------------------------------------------------------------------------------- /src/layouts/EmptyLayout.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@vue/cli-plugin-babel/preset'], 3 | } 4 | -------------------------------------------------------------------------------- /layouts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "layouts", 3 | "version": "1.0.0", 4 | "main": "index.js" 5 | } 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zxwk1998/vue-admin-better-template/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/ewm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zxwk1998/vue-admin-better-template/HEAD/src/assets/ewm.png -------------------------------------------------------------------------------- /src/assets/pro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zxwk1998/vue-admin-better-template/HEAD/src/assets/pro.png -------------------------------------------------------------------------------- /src/assets/ewm_vip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zxwk1998/vue-admin-better-template/HEAD/src/assets/ewm_vip.png -------------------------------------------------------------------------------- /src/styles/themes/default.scss: -------------------------------------------------------------------------------- 1 | /* 绿荫草场主题、荣耀典藏主题、暗黑之子主题加QQ讨论群972435319、1139183756后私聊群主获取,获取后将主题放到themes文件夹根目录即可 */ 2 | -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['stylelint-config-recess-order', 'stylelint-config-prettier'], 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon_backup.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zxwk1998/vue-admin-better-template/HEAD/public/favicon_backup.ico -------------------------------------------------------------------------------- /src/plugins/vabIcon.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VabIcon from 'vab-icon' 3 | 4 | Vue.component('VabIcon', VabIcon) 5 | -------------------------------------------------------------------------------- /vab-icon/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vab-icon", 3 | "version": "0.0.1", 4 | "main": "lib/vab-icon.umd.min.js" 5 | } 6 | -------------------------------------------------------------------------------- /src/assets/comparison/left.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zxwk1998/vue-admin-better-template/HEAD/src/assets/comparison/left.jpg -------------------------------------------------------------------------------- /src/assets/comparison/right.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zxwk1998/vue-admin-better-template/HEAD/src/assets/comparison/right.jpg -------------------------------------------------------------------------------- /src/assets/error_images/401.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zxwk1998/vue-admin-better-template/HEAD/src/assets/error_images/401.png -------------------------------------------------------------------------------- /src/assets/error_images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zxwk1998/vue-admin-better-template/HEAD/src/assets/error_images/404.png -------------------------------------------------------------------------------- /src/assets/qr_logo/lqr_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zxwk1998/vue-admin-better-template/HEAD/src/assets/qr_logo/lqr_logo.png -------------------------------------------------------------------------------- /src/assets/error_images/cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zxwk1998/vue-admin-better-template/HEAD/src/assets/error_images/cloud.png -------------------------------------------------------------------------------- /webstorm.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const webpackConfig = require('@vue/cli-service/webpack.config.js') 3 | module.exports = webpackConfig 4 | -------------------------------------------------------------------------------- /src/assets/login_images/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zxwk1998/vue-admin-better-template/HEAD/src/assets/login_images/background.jpg -------------------------------------------------------------------------------- /src/api/publicKey.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function getPublicKey() { 4 | return request({ 5 | url: '/publicKey', 6 | method: 'post', 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /src/config/settings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 3个子配置,通用配置|主题配置|网络配置 3 | */ 4 | //默认配置 5 | const { setting, theme, network } = require('./') 6 | module.exports = Object.assign({}, setting, theme, network) 7 | -------------------------------------------------------------------------------- /src/api/router.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function getRouterList(data) { 4 | return request({ 5 | url: '/menu/navigate', 6 | method: 'post', 7 | data, 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | -------------------------------------------------------------------------------- /src/remixIcon/svg/vuejs-fill.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.html text eol=lf 2 | *.css text eol=lf 3 | *.js text eol=lf 4 | *.scss text eol=lf 5 | *.vue text eol=lf 6 | *.hbs text eol=lf 7 | *.sh text eol=lf 8 | *.md text eol=lf 9 | *.json text eol=lf 10 | *.yml text eol=lf 11 | -------------------------------------------------------------------------------- /src/plugins/element.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import ElementUI from 'element-ui' 3 | import 'element-ui/lib/theme-chalk/display.css' 4 | 5 | import '@/styles/element-variables.scss' 6 | 7 | Vue.use(ElementUI, { 8 | size: 'small', 9 | }) 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /src/config/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 3个子配置,通用配置|主题配置|网络配置导出 3 | */ 4 | const setting = require('./setting.config') 5 | const theme = require('./theme.config') 6 | const network = require('./net.config') 7 | module.exports = Object.assign({}, setting, theme, network) 8 | -------------------------------------------------------------------------------- /src/api/ad.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function getList(data) { 4 | return request({ 5 | //url: '/ad/getList', 6 | url: 'https://851edf02-46eb-43e6-828d-64c7e483ea41.bspapp.com/http/getAd', 7 | method: 'get', 8 | data, 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /push.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | git init 4 | git add -A 5 | git commit -m 'deploy' 6 | git push -f "https://${access_token}@github.com/chuzhixin/vue-admin-better-template.git" master 7 | start "https://github.com/chuzhixin/vue-admin-better-template" 8 | exec /bin/bash 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/config/theme.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 导出默认主题配置 3 | */ 4 | const theme = { 5 | //是否国定头部 固定fixed 不固定noFixed 6 | header: 'fixed', 7 | //横纵布局 horizontal vertical 8 | layout: 'vertical', 9 | //是否开启主题配置按钮 10 | themeBar: true, 11 | //是否显示多标签页 12 | tabsBar: true, 13 | } 14 | module.exports = theme 15 | -------------------------------------------------------------------------------- /layouts/Permissions/index.js: -------------------------------------------------------------------------------- 1 | import permissions from './permissions' 2 | 3 | const install = function (Vue) { 4 | Vue.directive('permissions', permissions) 5 | } 6 | 7 | if (window.Vue) { 8 | window['permissions'] = permissions 9 | Vue.use(install) 10 | } 11 | 12 | permissions.install = install 13 | export default permissions 14 | -------------------------------------------------------------------------------- /src/utils/pageTitle.js: -------------------------------------------------------------------------------- 1 | import { title } from '@/config' 2 | 3 | /** 4 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 5 | * @description 设置标题 6 | * @param pageTitle 7 | * @returns {string} 8 | */ 9 | export default function getPageTitle(pageTitle) { 10 | if (pageTitle) { 11 | return `${pageTitle}-${title}` 12 | } 13 | return `${title}` 14 | } 15 | -------------------------------------------------------------------------------- /src/styles/transition.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 3 | * @description vue过渡动画 4 | */ 5 | 6 | @charset "utf-8"; 7 | 8 | .fade-transform-leave-active, 9 | .fade-transform-enter-active { 10 | transition: $base-transition; 11 | } 12 | 13 | .fade-transform-enter { 14 | opacity: 0; 15 | } 16 | 17 | .fade-transform-leave-to { 18 | opacity: 0; 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | .env.local 5 | .env.*.local 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | .idea 10 | *.suo 11 | *.ntvs* 12 | *.njsproj 13 | *.sln 14 | *.sw? 15 | public/video 16 | *.zip 17 | *.7z 18 | /src/layouts/components/zx-layouts 19 | /zx-templates 20 | /src/styles/themes/glory.scss 21 | /src/styles/themes/green.scss 22 | /src/styles/themes/dark.scss 23 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | npm run build 4 | cd dist 5 | touch .nojekyll 6 | git init 7 | git add -A 8 | git commit -m 'deploy' 9 | git push -f "https://${access_token}@gitee.com/chu1204505056/vue-admin-better-template.git" master:gh-pages 10 | start "https://gitee.com/chu1204505056/vue-admin-better-template/pages" 11 | cd - 12 | cd - 13 | rimraf dist 14 | exec /bin/bash 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /layouts/VabQueryForm/VabQueryFormTopPanel.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 21 | -------------------------------------------------------------------------------- /mock/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author chuzhixin 1204505056@qq.com 3 | * @description 导入所有 controller 模块,npm run serve时在node环境中自动输出controller文件夹下Mock接口,请勿修改。 4 | */ 5 | 6 | const { handleMockArray } = require('./utils') 7 | 8 | const mocks = [] 9 | const mockArray = handleMockArray() 10 | mockArray.forEach((item) => { 11 | const obj = require(item) 12 | mocks.push(...obj) 13 | }) 14 | module.exports = { 15 | mocks, 16 | } 17 | -------------------------------------------------------------------------------- /src/plugins/index.js: -------------------------------------------------------------------------------- 1 | /* 公共引入,勿随意修改,修改时需经过确认 */ 2 | import Vue from 'vue' 3 | import './element' 4 | import './support' 5 | import '@/styles/vab.scss' 6 | import '@/remixIcon' 7 | import '@/colorfulIcon' 8 | import '@/config/permission' 9 | import '@/utils/errorLog' 10 | import './vabIcon' 11 | 12 | import Vab from '@/utils/vab' 13 | import VabPermissions from 'layouts/Permissions' 14 | 15 | Vue.use(Vab) 16 | Vue.use(VabPermissions) 17 | -------------------------------------------------------------------------------- /layouts/VabQueryForm/VabQueryFormBottomPanel.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 21 | -------------------------------------------------------------------------------- /layouts/prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 80, 3 | tabWidth: 2, 4 | useTabs: false, 5 | semi: false, 6 | singleQuote: true, 7 | quoteProps: 'as-needed', 8 | jsxSingleQuote: false, 9 | trailingComma: 'es5', 10 | bracketSpacing: true, 11 | jsxBracketSameLine: false, 12 | arrowParens: 'always', 13 | htmlWhitespaceSensitivity: 'ignore', 14 | vueIndentScriptAndStyle: true, 15 | endOfLine: 'lf', 16 | } 17 | -------------------------------------------------------------------------------- /src/remixIcon/index.js: -------------------------------------------------------------------------------- 1 | const req = require.context('./svg', false, /\.svg$/), 2 | requireAll = (requireContext) => { 3 | /*let a = requireContext.keys().map(requireContext); 4 | let arr = []; 5 | for (let i = 0; i < a.length; i++) { 6 | console.log(); 7 | let icon = a[i].default.id; 8 | arr.push(icon); 9 | } 10 | console.log(JSON.stringify(arr));*/ 11 | return requireContext.keys().map(requireContext) 12 | } 13 | requireAll(req) 14 | -------------------------------------------------------------------------------- /src/colorfulIcon/index.js: -------------------------------------------------------------------------------- 1 | const req = require.context('./svg', false, /\.svg$/), 2 | requireAll = (requireContext) => { 3 | /*let a = requireContext.keys().map(requireContext); 4 | let arr = []; 5 | for (let i = 0; i < a.length; i++) { 6 | console.log(); 7 | let icon = a[i].default.id; 8 | arr.push(icon); 9 | } 10 | console.log(JSON.stringify(arr));*/ 11 | return requireContext.keys().map(requireContext) 12 | } 13 | requireAll(req) 14 | -------------------------------------------------------------------------------- /src/colorfulIcon/svg/alphabetical_sorting.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /layouts/Permissions/permissions.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | 3 | export default { 4 | inserted(element, binding) { 5 | const { value } = binding 6 | const permissions = store.getters['user/permissions'] 7 | if (value && value instanceof Array && value.length > 0) { 8 | const hasPermission = permissions.some((role) => value.includes(role)) 9 | if (!hasPermission) 10 | element.parentNode && element.parentNode.removeChild(element) 11 | } 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /src/config/net.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 导出默认网路配置 3 | **/ 4 | const network = { 5 | //配后端数据的接收方式application/json;charset=UTF-8或者application/x-www-form-urlencoded;charset=UTF-8 6 | contentType: 'application/json;charset=UTF-8', 7 | //消息框消失时间 8 | messageDuration: 3000, 9 | //最长请求时间 10 | requestTimeout: 5000, 11 | //操作正常code,支持String、Array、int多种类型 12 | successCode: [200, 0], 13 | //登录失效code 14 | invalidCode: 402, 15 | //无权限code 16 | noPermissionCode: 401, 17 | } 18 | module.exports = network 19 | -------------------------------------------------------------------------------- /src/store/modules/table.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 3 | * @description 代码生成机状态管理 4 | */ 5 | 6 | const state = { srcCode: '' } 7 | const getters = { 8 | srcTableCode: (state) => state.srcCode, 9 | } 10 | 11 | const mutations = { 12 | setTableCode(state, srcCode) { 13 | state.srcCode = srcCode 14 | }, 15 | } 16 | const actions = { 17 | setTableCode({ commit }, srcCode) { 18 | commit('setTableCode', srcCode) 19 | }, 20 | } 21 | export default { state, getters, mutations, actions } 22 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 3 | * @description 代码规范 4 | */ 5 | 6 | module.exports = { 7 | printWidth: 80, 8 | tabWidth: 2, 9 | useTabs: false, 10 | semi: false, 11 | singleQuote: true, 12 | quoteProps: 'as-needed', 13 | jsxSingleQuote: false, 14 | trailingComma: 'es5', 15 | bracketSpacing: true, 16 | jsxBracketSameLine: false, 17 | arrowParens: 'always', 18 | htmlWhitespaceSensitivity: 'ignore', 19 | vueIndentScriptAndStyle: true, 20 | endOfLine: 'lf', 21 | } 22 | -------------------------------------------------------------------------------- /layouts/VabQueryForm/VabQueryFormLeftPanel.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 26 | -------------------------------------------------------------------------------- /layouts/VabQueryForm/VabQueryFormRightPanel.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 26 | -------------------------------------------------------------------------------- /mock/controller/ad.js: -------------------------------------------------------------------------------- 1 | const data = [ 2 | { 3 | title: 'vue-admin-better-pro 1.7版本已发布,点我提前体验', 4 | url: 'https://chu1204505056.gitee.io/vue-admin-better-pro/#/index', 5 | }, 6 | { 7 | title: 'vue-admin-better(antdv) vue3.0版本已发布,点我提前体验', 8 | url: 'https://chu1204505056.gitee.io/vue-admin-better-mini/#/index', 9 | }, 10 | ] 11 | module.exports = [ 12 | { 13 | url: '/ad/getList', 14 | type: 'get', 15 | response() { 16 | return { 17 | code: 200, 18 | msg: 'success', 19 | data, 20 | } 21 | }, 22 | }, 23 | ] 24 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App' 3 | import store from './store' 4 | import router from './router' 5 | import './plugins' 6 | import '@/layouts/export' 7 | /** 8 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 9 | * @description 生产环境默认都使用mock,如果正式用于生产环境时,记得去掉 10 | */ 11 | 12 | if (process.env.NODE_ENV === 'production') { 13 | const { mockXHR } = require('@/utils/static') 14 | mockXHR() 15 | } 16 | 17 | Vue.config.productionTip = false 18 | 19 | new Vue({ 20 | el: '#vue-admin-better', 21 | router, 22 | store, 23 | render: (h) => h(App), 24 | }) 25 | -------------------------------------------------------------------------------- /src/utils/permission.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | 3 | /** 4 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 5 | * @description 检查权限 6 | * @param value 7 | * @returns {boolean} 8 | */ 9 | export default function checkPermission(value) { 10 | if (value && value instanceof Array && value.length > 0) { 11 | const permissions = store.getters['user/permissions'] 12 | const permissionPermissions = value 13 | 14 | return permissions.some((role) => { 15 | return permissionPermissions.includes(role) 16 | }) 17 | } else { 18 | return false 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /plopfile.js: -------------------------------------------------------------------------------- 1 | const viewGenerator = require('zx-templates/view/prompt') 2 | const curdGenerator = require('zx-templates/curd/prompt') 3 | const componentGenerator = require('zx-templates/component/prompt') 4 | const mockGenerator = require('zx-templates/mock/prompt') 5 | const vuexGenerator = require('zx-templates/vuex/prompt') 6 | 7 | module.exports = (plop) => { 8 | plop.setGenerator('view', viewGenerator) 9 | plop.setGenerator('curd', curdGenerator) 10 | plop.setGenerator('component', componentGenerator) 11 | plop.setGenerator('mock&api', mockGenerator) 12 | plop.setGenerator('vuex', vuexGenerator) 13 | } 14 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 3 | * @description 导入所有 vuex 模块,自动加入namespaced:true,用于解决vuex命名冲突,请勿修改。 4 | */ 5 | 6 | import Vue from 'vue' 7 | import Vuex from 'vuex' 8 | 9 | Vue.use(Vuex) 10 | const files = require.context('./modules', false, /\.js$/) 11 | const modules = {} 12 | 13 | files.keys().forEach((key) => { 14 | modules[key.replace(/(\.\/|\.js)/g, '')] = files(key).default 15 | }) 16 | Object.keys(modules).forEach((key) => { 17 | modules[key]['namespaced'] = true 18 | }) 19 | const store = new Vuex.Store({ 20 | modules, 21 | }) 22 | export default store 23 | -------------------------------------------------------------------------------- /src/remixIcon/svg/qq-fill.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: ['plugin:vue/recommended', '@vue/prettier'], 7 | rules: { 8 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 9 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 10 | 'vue/no-v-html': 'off', 11 | }, 12 | parserOptions: { 13 | parser: 'babel-eslint', 14 | }, 15 | overrides: [ 16 | { 17 | files: [ 18 | '**/__tests__/*.{j,t}s?(x)', 19 | '**/tests/unit/**/*.spec.{j,t}s?(x)', 20 | ], 21 | env: { 22 | jest: true, 23 | }, 24 | }, 25 | ], 26 | } 27 | -------------------------------------------------------------------------------- /src/store/modules/errorLog.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 3 | * @description 异常捕获的状态拦截,请勿修改 4 | */ 5 | 6 | const state = { errorLogs: [] } 7 | const getters = { 8 | errorLogs: (state) => state.errorLogs, 9 | } 10 | const mutations = { 11 | addErrorLog(state, errorLog) { 12 | state.errorLogs.push(errorLog) 13 | }, 14 | clearErrorLog: (state) => { 15 | state.errorLogs.splice(0) 16 | }, 17 | } 18 | const actions = { 19 | addErrorLog({ commit }, errorLog) { 20 | commit('addErrorLog', errorLog) 21 | }, 22 | clearErrorLog({ commit }) { 23 | commit('clearErrorLog') 24 | }, 25 | } 26 | export default { state, getters, mutations, actions } 27 | -------------------------------------------------------------------------------- /src/utils/errorLog.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import store from '@/store' 3 | import { isArray, isString } from '@/utils/validate' 4 | import { errorLog } from '@/config' 5 | 6 | const needErrorLog = errorLog 7 | const checkNeed = () => { 8 | const env = process.env.NODE_ENV 9 | if (isString(needErrorLog)) { 10 | return env === needErrorLog 11 | } 12 | if (isArray(needErrorLog)) { 13 | return needErrorLog.includes(env) 14 | } 15 | return false 16 | } 17 | if (checkNeed()) { 18 | Vue.config.errorHandler = (err, vm, info) => { 19 | console.error('vue-admin-better错误拦截:', err, vm, info) 20 | const url = window.location.href 21 | Vue.nextTick(() => { 22 | store.dispatch('errorLog/addErrorLog', { err, vm, info, url }) 23 | }) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/plugins/support.js: -------------------------------------------------------------------------------- 1 | import { MessageBox } from 'element-ui' 2 | import { donation } from '@/config' 3 | import { dependencies, repository } from '../../package.json' 4 | 5 | if (!!window.ActiveXObject || 'ActiveXObject' in window) { 6 | MessageBox({ 7 | title: '温馨提示', 8 | message: 9 | '自2015年3月起,微软已宣布弃用IE,且不再对IE提供任何更新维护,请点击此处访问微软官网更新浏览器,如果您使用的是双核浏览器,请您切换浏览器内核为极速模式', 10 | type: 'warning', 11 | showClose: false, 12 | showConfirmButton: false, 13 | closeOnClickModal: false, 14 | closeOnPressEscape: false, 15 | closeOnHashChange: false, 16 | dangerouslyUseHTMLString: true, 17 | }) 18 | } 19 | if (!dependencies['vab-icon'] || !dependencies['layouts']) 20 | document.body.innerHTML = '' 21 | -------------------------------------------------------------------------------- /src/api/user.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | import { encryptedData } from '@/utils/encrypt' 3 | import { loginRSA, tokenName } from '@/config' 4 | 5 | export async function login(data) { 6 | if (loginRSA) { 7 | data = await encryptedData(data) 8 | } 9 | return request({ 10 | url: '/login', 11 | method: 'post', 12 | data, 13 | }) 14 | } 15 | 16 | export function getUserInfo(accessToken) { 17 | return request({ 18 | url: '/userInfo', 19 | method: 'post', 20 | data: { 21 | [tokenName]: accessToken, 22 | }, 23 | }) 24 | } 25 | 26 | export function logout() { 27 | return request({ 28 | url: '/logout', 29 | method: 'post', 30 | }) 31 | } 32 | 33 | export function register() { 34 | return request({ 35 | url: '/register', 36 | method: 'post', 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /src/layouts/export.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author https://vue-admin-better.com (不想保留author可删除) 3 | * @description 公共布局及样式自动引入 4 | */ 5 | 6 | import Vue from 'vue' 7 | 8 | const requireComponents = require.context('./components', true, /\.vue$/) 9 | requireComponents.keys().forEach((fileName) => { 10 | const componentConfig = requireComponents(fileName) 11 | const componentName = componentConfig.default.name 12 | Vue.component(componentName, componentConfig.default || componentConfig) 13 | }) 14 | 15 | const requireZxLayouts = require.context('layouts', true, /\.vue$/) 16 | requireZxLayouts.keys().forEach((fileName) => { 17 | const componentConfig = requireZxLayouts(fileName) 18 | const componentName = componentConfig.default.name 19 | Vue.component(componentName, componentConfig.default || componentConfig) 20 | }) 21 | 22 | const requireThemes = require.context('@/styles/themes', true, /\.scss$/) 23 | requireThemes.keys().forEach((fileName) => { 24 | require(`@/styles/themes/${fileName.slice(2)}`) 25 | }) 26 | -------------------------------------------------------------------------------- /mock/controller/router.js: -------------------------------------------------------------------------------- 1 | const data = [ 2 | { 3 | path: '/', 4 | component: 'Layout', 5 | redirect: 'index', 6 | children: [ 7 | { 8 | path: 'index', 9 | name: 'Index', 10 | component: '@/views/index/index', 11 | meta: { 12 | title: '首页', 13 | icon: 'home', 14 | affix: true, 15 | }, 16 | }, 17 | ], 18 | }, 19 | { 20 | path: '/error', 21 | component: 'EmptyLayout', 22 | redirect: 'noRedirect', 23 | name: 'Error', 24 | meta: { title: '错误页', icon: 'bug' }, 25 | children: [ 26 | { 27 | path: '401', 28 | name: 'Error401', 29 | component: '@/views/401', 30 | meta: { title: '401' }, 31 | }, 32 | { 33 | path: '404', 34 | name: 'Error404', 35 | component: '@/views/404', 36 | meta: { title: '404' }, 37 | }, 38 | ], 39 | }, 40 | ] 41 | module.exports = [ 42 | { 43 | url: '/menu/navigate', 44 | type: 'post', 45 | response() { 46 | return { code: 200, msg: 'success', data: data } 47 | }, 48 | }, 49 | ] 50 | -------------------------------------------------------------------------------- /layouts/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | webpackBarName: 'vue-admin-better', 3 | webpackBanner: 4 | ' build: vue-admin-better \n vue-admin-better.com \n https://gitee.com/chu1204505056/vue-admin-better \n time: ', 5 | donationConsole() { 6 | const chalk = require('chalk') 7 | console.log( 8 | chalk.green( 9 | `> 欢迎使用vue-admin-better,github开源地址:https://github.com/chuzhixin/vue-admin-better` 10 | ) 11 | ) 12 | console.log( 13 | chalk.green( 14 | `> 欢迎使用vue-admin-better,码云开源地址:https://gitee.com/chu1204505056/vue-admin-better` 15 | ) 16 | ) 17 | 18 | console.log( 19 | chalk.green(`> pro版演示地址:http://vue-admin-better.com/admin-pro`) 20 | ) 21 | 22 | console.log( 23 | chalk.green(`> plus版演示地址:http://vue-admin-better.com/admin-plus`) 24 | ) 25 | 26 | console.log( 27 | chalk.green( 28 | `> 使用中出现任何问题可加QQ群反馈,获取基础版、文档,请我们喝杯咖啡(如若情况不允许,请勿勉强):https://gitee.com/chu1204505056/vue-admin-better#-%E5%89%8D%E7%AB%AF%E8%AE%A8%E8%AE%BA-qq-%E7%BE%A4` 29 | ) 30 | ) 31 | 32 | console.log(chalk.green(`> 如果您不希望显示以上信息,可在config中配置关闭`)) 33 | console.log('\n') 34 | }, 35 | } 36 | -------------------------------------------------------------------------------- /src/styles/spinner/inner-circles.css: -------------------------------------------------------------------------------- 1 | .inner-circles-loader:not(:required) { 2 | position: relative; 3 | display: inline-block; 4 | width: 50px; 5 | height: 50px; 6 | margin-bottom: 10px; 7 | overflow: hidden; 8 | text-indent: -9999px; 9 | background: rgba(25, 165, 152, 0.5); 10 | border-radius: 50%; 11 | transform: translate3d(0, 0, 0); 12 | } 13 | 14 | .inner-circles-loader:not(:required)::before, 15 | .inner-circles-loader:not(:required)::after { 16 | position: absolute; 17 | top: 0; 18 | display: inline-block; 19 | width: 50px; 20 | height: 50px; 21 | content: ""; 22 | border-radius: 50%; 23 | } 24 | 25 | .inner-circles-loader:not(:required)::before { 26 | left: 0; 27 | background: #c7efcf; 28 | transform-origin: 0 50%; 29 | animation: inner-circles-loader 3s infinite; 30 | } 31 | 32 | .inner-circles-loader:not(:required)::after { 33 | right: 0; 34 | background: #eef5db; 35 | transform-origin: 100% 50%; 36 | animation: inner-circles-loader 3s 0.2s reverse infinite; 37 | } 38 | 39 | @keyframes inner-circles-loader { 40 | 0% { 41 | transform: rotate(0deg); 42 | } 43 | 44 | 50% { 45 | transform: rotate(360deg); 46 | } 47 | 48 | 100% { 49 | transform: rotate(0deg); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /layouts/VabQueryForm/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 19 | 20 | 64 | -------------------------------------------------------------------------------- /layouts/VabFullScreenBar/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 54 | -------------------------------------------------------------------------------- /src/layouts/components/VabAd/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 39 | 53 | -------------------------------------------------------------------------------- /mock/utils/index.js: -------------------------------------------------------------------------------- 1 | const { Random } = require('mockjs') 2 | const { join } = require('path') 3 | const fs = require('fs') 4 | 5 | /** 6 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 7 | * @description 随机生成图片url。 8 | * @param width 9 | * @param height 10 | * @returns {string} 11 | */ 12 | function handleRandomImage(width = 50, height = 50) { 13 | return `https://picsum.photos/${width}/${height}?random=${Random.guid()}` 14 | } 15 | 16 | /** 17 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 18 | * @description 处理所有 controller 模块,npm run serve时在node环境中自动输出controller文件夹下Mock接口,请勿修改。 19 | * @returns {[]} 20 | */ 21 | function handleMockArray() { 22 | const mockArray = [] 23 | const getFiles = (jsonPath) => { 24 | const jsonFiles = [] 25 | const findJsonFile = (path) => { 26 | const files = fs.readdirSync(path) 27 | files.forEach((item) => { 28 | const fPath = join(path, item) 29 | const stat = fs.statSync(fPath) 30 | if (stat.isDirectory() === true) findJsonFile(item) 31 | if (stat.isFile() === true) jsonFiles.push(item) 32 | }) 33 | } 34 | findJsonFile(jsonPath) 35 | jsonFiles.forEach((item) => mockArray.push(`./controller/${item}`)) 36 | } 37 | getFiles('mock/controller') 38 | return mockArray 39 | } 40 | module.exports = { 41 | handleRandomImage, 42 | handleMockArray, 43 | } 44 | -------------------------------------------------------------------------------- /src/utils/static.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author chuzhixin 1204505056@qq.com 3 | * @description 导入所有 controller 模块,浏览器环境中自动输出controller文件夹下Mock接口,请勿修改。 4 | */ 5 | import Mock from 'mockjs' 6 | import { paramObj } from '@/utils/index' 7 | 8 | const mocks = [] 9 | const files = require.context('../../mock/controller', false, /\.js$/) 10 | 11 | files.keys().forEach((key) => { 12 | mocks.push(...files(key)) 13 | }) 14 | 15 | export function mockXHR() { 16 | Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send 17 | Mock.XHR.prototype.send = function () { 18 | if (this.custom.xhr) { 19 | this.custom.xhr.withCredentials = this.withCredentials || false 20 | 21 | if (this.responseType) { 22 | this.custom.xhr.responseType = this.responseType 23 | } 24 | } 25 | this.proxy_send(...arguments) 26 | } 27 | 28 | function XHRHttpRequst(respond) { 29 | return function (options) { 30 | let result 31 | if (respond instanceof Function) { 32 | const { body, type, url } = options 33 | result = respond({ 34 | method: type, 35 | body: JSON.parse(body), 36 | query: paramObj(url), 37 | }) 38 | } else { 39 | result = respond 40 | } 41 | return Mock.mock(result) 42 | } 43 | } 44 | 45 | mocks.forEach((item) => { 46 | Mock.mock( 47 | new RegExp(item.url), 48 | item.type || 'get', 49 | XHRHttpRequst(item.response) 50 | ) 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /layouts/VabColorfullIcon/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 48 | 49 | 66 | -------------------------------------------------------------------------------- /layouts/VabSideBar/components/VabSubmenu.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 61 | -------------------------------------------------------------------------------- /src/layouts/components/VabBreadcrumb/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 31 | 32 | 64 | -------------------------------------------------------------------------------- /src/store/modules/routes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 3 | * @description 路由拦截状态管理,目前两种模式:all模式与intelligence模式,其中partialRoutes是菜单暂未使用 4 | */ 5 | import { asyncRoutes, constantRoutes } from '@/router' 6 | import { getRouterList } from '@/api/router' 7 | import { convertRouter, filterAsyncRoutes } from '@/utils/handleRoutes' 8 | 9 | const state = { routes: [], partialRoutes: [] } 10 | const getters = { 11 | routes: (state) => state.routes, 12 | partialRoutes: (state) => state.partialRoutes, 13 | } 14 | const mutations = { 15 | setRoutes(state, routes) { 16 | state.routes = constantRoutes.concat(routes) 17 | }, 18 | setAllRoutes(state, routes) { 19 | state.routes = constantRoutes.concat(routes) 20 | }, 21 | setPartialRoutes(state, routes) { 22 | state.partialRoutes = constantRoutes.concat(routes) 23 | }, 24 | } 25 | const actions = { 26 | async setRoutes({ commit }, permissions) { 27 | //开源版只过滤动态路由permissions,admin不再默认拥有全部权限 28 | const finallyAsyncRoutes = await filterAsyncRoutes( 29 | [...asyncRoutes], 30 | permissions 31 | ) 32 | commit('setRoutes', finallyAsyncRoutes) 33 | return finallyAsyncRoutes 34 | }, 35 | async setAllRoutes({ commit }) { 36 | let { data } = await getRouterList() 37 | data.push({ path: '*', redirect: '/404', hidden: true }) 38 | let accessRoutes = convertRouter(data) 39 | commit('setAllRoutes', accessRoutes) 40 | return accessRoutes 41 | }, 42 | setPartialRoutes({ commit }, accessRoutes) { 43 | commit('setPartialRoutes', accessRoutes) 44 | return accessRoutes 45 | }, 46 | } 47 | export default { state, getters, mutations, actions } 48 | -------------------------------------------------------------------------------- /layouts/VabRemixIcon/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 51 | 52 | 70 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[vue]": { 3 | "editor.defaultFormatter": "esbenp.prettier-vscode" 4 | }, 5 | "editor.quickSuggestions": { 6 | "strings": true 7 | }, 8 | "workbench.colorTheme": "One Monokai", 9 | "editor.tabSize": 2, 10 | "editor.detectIndentation": false, 11 | "emmet.triggerExpansionOnTab": true, 12 | "editor.formatOnSave": true, 13 | "javascript.format.enable": true, 14 | "git.enableSmartCommit": true, 15 | "git.autofetch": true, 16 | "git.confirmSync": false, 17 | "[json]": { 18 | "editor.defaultFormatter": "esbenp.prettier-vscode" 19 | }, 20 | "liveServer.settings.donotShowInfoMsg": true, 21 | "explorer.confirmDelete": false, 22 | "javascript.updateImportsOnFileMove.enabled": "always", 23 | "typescript.updateImportsOnFileMove.enabled": "always", 24 | "files.exclude": { 25 | "**/.idea": true 26 | }, 27 | "editor.codeActionsOnSave": { 28 | "source.fixAll.stylelint": true, 29 | "source.fixAll.eslint": true 30 | }, 31 | "[javascript]": { 32 | "editor.defaultFormatter": "esbenp.prettier-vscode" 33 | }, 34 | "[scss]": { 35 | "editor.defaultFormatter": "esbenp.prettier-vscode" 36 | }, 37 | "[jsonc]": { 38 | "editor.defaultFormatter": "esbenp.prettier-vscode" 39 | }, 40 | "[html]": { 41 | "editor.defaultFormatter": "esbenp.prettier-vscode" 42 | }, 43 | "editor.suggest.snippetsPreventQuickSuggestions": false, 44 | "prettier.htmlWhitespaceSensitivity": "ignore", 45 | "prettier.vueIndentScriptAndStyle": true, 46 | "docthis.authorName": "chuzhixin 1204505056@qq.com", 47 | "docthis.includeAuthorTag": true, 48 | "docthis.includeDescriptionTag": true, 49 | "docthis.enableHungarianNotationEvaluation": true, 50 | "docthis.inferTypesFromNames": true, 51 | "vetur.format.defaultFormatter.html": "prettier" 52 | } 53 | -------------------------------------------------------------------------------- /public/static/css/loading.css: -------------------------------------------------------------------------------- 1 | .first-loading-wrp { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | justify-content: center; 6 | height: 90vh; 7 | min-height: 90vh; 8 | } 9 | 10 | .first-loading-wrp > h1 { 11 | font-size: 30px; 12 | font-weight: bolder; 13 | } 14 | 15 | .first-loading-wrp .loading-wrp { 16 | display: flex; 17 | align-items: center; 18 | justify-content: center; 19 | padding: 98px; 20 | } 21 | 22 | .dot { 23 | position: relative; 24 | box-sizing: border-box; 25 | display: inline-block; 26 | width: 64px; 27 | height: 64px; 28 | font-size: 64px; 29 | transform: rotate(45deg); 30 | animation: antRotate 1.2s infinite linear; 31 | } 32 | 33 | .dot i { 34 | position: absolute; 35 | display: block; 36 | width: 28px; 37 | height: 28px; 38 | background-color: #1890ff; 39 | border-radius: 100%; 40 | opacity: 0.3; 41 | transform: scale(0.75); 42 | transform-origin: 50% 50%; 43 | animation: antSpinMove 1s infinite linear alternate; 44 | } 45 | 46 | .dot i:nth-child(1) { 47 | top: 0; 48 | left: 0; 49 | } 50 | 51 | .dot i:nth-child(2) { 52 | top: 0; 53 | right: 0; 54 | -webkit-animation-delay: 0.4s; 55 | animation-delay: 0.4s; 56 | } 57 | 58 | .dot i:nth-child(3) { 59 | right: 0; 60 | bottom: 0; 61 | -webkit-animation-delay: 0.8s; 62 | animation-delay: 0.8s; 63 | } 64 | 65 | .dot i:nth-child(4) { 66 | bottom: 0; 67 | left: 0; 68 | -webkit-animation-delay: 1.2s; 69 | animation-delay: 1.2s; 70 | } 71 | 72 | @keyframes antRotate { 73 | to { 74 | -webkit-transform: rotate(405deg); 75 | transform: rotate(405deg); 76 | } 77 | } 78 | 79 | @-webkit-keyframes antRotate { 80 | to { 81 | -webkit-transform: rotate(405deg); 82 | transform: rotate(405deg); 83 | } 84 | } 85 | 86 | @keyframes antSpinMove { 87 | to { 88 | opacity: 1; 89 | } 90 | } 91 | 92 | @-webkit-keyframes antSpinMove { 93 | to { 94 | opacity: 1; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= VUE_APP_TITLE %> 9 | 13 | 17 | 18 | 19 | 28 | 29 | 30 | 34 |
35 |
36 |
37 | 38 | 39 | 40 | 41 | 42 | 43 |
44 |

<%= VUE_APP_TITLE %>

45 |
46 |
47 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/utils/accessToken.js: -------------------------------------------------------------------------------- 1 | import { storage, tokenTableName } from '@/config' 2 | 3 | /** 4 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 5 | * @description 获取accessToken 6 | * @returns {string|ActiveX.IXMLDOMNode|Promise|any|IDBRequest|MediaKeyStatus|FormDataEntryValue|Function|Promise} 7 | */ 8 | export function getAccessToken() { 9 | if (storage) { 10 | if ('localStorage' === storage) { 11 | return localStorage.getItem(tokenTableName) 12 | } else if ('sessionStorage' === storage) { 13 | return sessionStorage.getItem(tokenTableName) 14 | } else { 15 | return localStorage.getItem(tokenTableName) 16 | } 17 | } else { 18 | return localStorage.getItem(tokenTableName) 19 | } 20 | } 21 | 22 | /** 23 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 24 | * @description 存储accessToken 25 | * @param accessToken 26 | * @returns {void|*} 27 | */ 28 | export function setAccessToken(accessToken) { 29 | if (storage) { 30 | if ('localStorage' === storage) { 31 | return localStorage.setItem(tokenTableName, accessToken) 32 | } else if ('sessionStorage' === storage) { 33 | return sessionStorage.setItem(tokenTableName, accessToken) 34 | } else { 35 | return localStorage.setItem(tokenTableName, accessToken) 36 | } 37 | } else { 38 | return localStorage.setItem(tokenTableName, accessToken) 39 | } 40 | } 41 | 42 | /** 43 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 44 | * @description 移除accessToken 45 | * @returns {void|Promise} 46 | */ 47 | export function removeAccessToken() { 48 | if (storage) { 49 | if ('localStorage' === storage) { 50 | return localStorage.removeItem(tokenTableName) 51 | } else if ('sessionStorage' === storage) { 52 | return sessionStorage.clear() 53 | } else { 54 | return localStorage.removeItem(tokenTableName) 55 | } 56 | } else { 57 | return localStorage.removeItem(tokenTableName) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/styles/spinner/gauge.css: -------------------------------------------------------------------------------- 1 | .gauge-loader:not(:required) { 2 | position: relative; 3 | display: inline-block; 4 | width: 64px; 5 | height: 32px; 6 | margin-bottom: 10px; 7 | overflow: hidden; 8 | text-indent: -9999px; 9 | background: #6ca; 10 | border-top-left-radius: 32px; 11 | border-top-right-radius: 32px; 12 | } 13 | 14 | .gauge-loader:not(:required)::before { 15 | position: absolute; 16 | top: 5px; 17 | left: 30px; 18 | width: 4px; 19 | height: 27px; 20 | content: ""; 21 | background: white; 22 | border-radius: 2px; 23 | transform-origin: 50% 100%; 24 | animation: gauge-loader 4000ms infinite ease; 25 | } 26 | 27 | .gauge-loader:not(:required)::after { 28 | position: absolute; 29 | top: 26px; 30 | left: 26px; 31 | width: 13px; 32 | height: 13px; 33 | content: ""; 34 | background: white; 35 | -moz-border-radius: 8px; 36 | -webkit-border-radius: 8px; 37 | border-radius: 8px; 38 | } 39 | 40 | @keyframes gauge-loader { 41 | 0% { 42 | transform: rotate(-50deg); 43 | } 44 | 45 | 10% { 46 | transform: rotate(20deg); 47 | } 48 | 49 | 20% { 50 | transform: rotate(60deg); 51 | } 52 | 53 | 24% { 54 | transform: rotate(60deg); 55 | } 56 | 57 | 40% { 58 | transform: rotate(-20deg); 59 | } 60 | 61 | 54% { 62 | transform: rotate(70deg); 63 | } 64 | 65 | 56% { 66 | transform: rotate(78deg); 67 | } 68 | 69 | 58% { 70 | transform: rotate(73deg); 71 | } 72 | 73 | 60% { 74 | transform: rotate(75deg); 75 | } 76 | 77 | 62% { 78 | transform: rotate(70deg); 79 | } 80 | 81 | 70% { 82 | transform: rotate(-20deg); 83 | } 84 | 85 | 80% { 86 | transform: rotate(20deg); 87 | } 88 | 89 | 83% { 90 | transform: rotate(25deg); 91 | } 92 | 93 | 86% { 94 | transform: rotate(20deg); 95 | } 96 | 97 | 89% { 98 | transform: rotate(25deg); 99 | } 100 | 101 | 100% { 102 | transform: rotate(-50deg); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /http/mock.http: -------------------------------------------------------------------------------- 1 | 2 | ###/changeLog/getList###mockServer 3 | POST http://localhost:80/mock-server/changeLog/getList 4 | Content-Type: application/x-www-form-urlencoded 5 | ### 6 | mockServer 7 | ###/colorfulIcon/list### 8 | POST http://localhost:80/mock-server/colorfulIcon/list 9 | Content-Type: application/x-www-form-urlencoded 10 | ###mockServer 11 | 12 | ###/menu/navigate### 13 | POST http://localhost:80/mock-server/menu/navigate 14 | Content-Type: application/x-www-form-urlenmockServer 15 | ### 16 | 17 | ###/icon/list### 18 | POST http://localhost:80/mock-server/icon/mockServer 19 | Content-Type: application/x-www-form-urlencoded 20 | ### 21 | 22 | ###/face/list###mockServer 23 | POST http://localhost:80/mock-server/face/list 24 | Content-Type: application/x-www-form-urlencoded 25 | ### 26 | mockServer 27 | ###/table/list### 28 | POST http://localhost:80/mock-server/table/list 29 | Content-Type: application/x-www-form-urlencoded 30 | ###mockServer 31 | 32 | ###/remixicon/getList### 33 | POST http://localhost:80/mock-server/remixicon/getList 34 | Content-Type: application/x-www-form-urlenmockServer 35 | ### 36 | 37 | ###/publicKey### 38 | POST http://localhost:80/mock-server/pumockServer 39 | Content-Type: application/x-www-form-urlencoded 40 | ### 41 | 42 | ###/tree/list###mockServer 43 | POST http://localhost:80/mock-server/tree/list 44 | Content-Type: application/x-www-form-urlencoded 45 | ### 46 | mockServer 47 | ###/upload### 48 | POST http://localhost:80/mock-server/upload 49 | Content-Type: application/x-www-form-urlencoded 50 | ###mockServer 51 | 52 | ###/login### 53 | POST http://localhost:80/mock-server/login 54 | Content-Type: application/x-www-form-urlenmockServer 55 | ### 56 | 57 | ###/waterfall/list### 58 | POST http://localhost:80/mock-server/waterfall/list 59 | Content-Type: application/x-www-form-urlencoded 60 | ### 61 | 62 | ###/logout### 63 | POST http://localhost:80/mock-server/logout 64 | Content-Type: application/x-www-form-urlencoded 65 | ### 66 | 67 | ###/user/info### 68 | POST http://localhost:80/mock-server/user/info 69 | Content-Type: application/x-www-form-urlencoded 70 | ### 71 | -------------------------------------------------------------------------------- /src/utils/handleRoutes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 3 | * @description all模式渲染后端返回路由 4 | * @param constantRoutes 5 | * @returns {*} 6 | */ 7 | export function convertRouter(asyncRoutes) { 8 | return asyncRoutes.map((route) => { 9 | if (route.component) { 10 | if (route.component === 'Layout') { 11 | route.component = (resolve) => require(['@/layouts'], resolve) 12 | } else if (route.component === 'EmptyLayout') { 13 | route.component = (resolve) => 14 | require(['@/layouts/EmptyLayout'], resolve) 15 | } else { 16 | const index = route.component.indexOf('views') 17 | const path = 18 | index > 0 ? route.component.slice(index) : `views/${route.component}` 19 | route.component = (resolve) => require([`@/${path}`], resolve) 20 | } 21 | } 22 | if (route.children && route.children.length) 23 | route.children = convertRouter(route.children) 24 | if (route.children && route.children.length === 0) delete route.children 25 | return route 26 | }) 27 | } 28 | 29 | /** 30 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 31 | * @description 判断当前路由是否包含权限 32 | * @param permissions 33 | * @param route 34 | * @returns {boolean|*} 35 | */ 36 | function hasPermission(permissions, route) { 37 | if (route.meta && route.meta.permissions) { 38 | return permissions.some((role) => route.meta.permissions.includes(role)) 39 | } else { 40 | return true 41 | } 42 | } 43 | 44 | /** 45 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 46 | * @description intelligence模式根据permissions数组拦截路由 47 | * @param routes 48 | * @param permissions 49 | * @returns {[]} 50 | */ 51 | export function filterAsyncRoutes(routes, permissions) { 52 | const finallyRoutes = [] 53 | routes.forEach((route) => { 54 | const item = { ...route } 55 | if (hasPermission(permissions, item)) { 56 | if (item.children) { 57 | item.children = filterAsyncRoutes(item.children, permissions) 58 | } 59 | finallyRoutes.push(item) 60 | } 61 | }) 62 | return finallyRoutes 63 | } 64 | -------------------------------------------------------------------------------- /src/utils/encrypt.js: -------------------------------------------------------------------------------- 1 | import { JSEncrypt } from 'jsencrypt' 2 | import { getPublicKey } from '@/api/publicKey' 3 | 4 | const privateKey = 5 | 'MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAMFPa+v52FkSUXvcUnrGI/XzW3EpZRI0s9BCWJ3oNQmEYA5luWW5p8h0uadTIoTyYweFPdH4hveyxlwmS7oefvbIdiP+o+QIYW/R4Wjsb4Yl8MhR4PJqUE3RCy6IT9fM8ckG4kN9ECs6Ja8fQFc6/mSl5dJczzJO3k1rWMBhKJD/AgMBAAECgYEAucMakH9dWeryhrYoRHcXo4giPVJsH9ypVt4KzmOQY/7jV7KFQK3x//27UoHfUCak51sxFw9ek7UmTPM4HjikA9LkYeE7S381b4QRvFuf3L6IbMP3ywJnJ8pPr2l5SqQ00W+oKv+w/VmEsyUHr+k4Z+4ik+FheTkVWp566WbqFsECQQDjYaMcaKw3j2Zecl8T6eUe7fdaRMIzp/gcpPMfT/9rDzIQk+7ORvm1NI9AUmFv/FAlfpuAMrdL2n7p9uznWb7RAkEA2aP934kbXg5bdV0R313MrL+7WTK/qdcYxATUbMsMuWWQBoS5irrt80WCZbG48hpocJavLNjbtrjmUX3CuJBmzwJAOJg8uP10n/+ZQzjEYXh+BszEHDuw+pp8LuT/fnOy5zrJA0dO0RjpXijO3vuiNPVgHXT9z1LQPJkNrb5ACPVVgQJBALPeb4uV0bNrJDUb5RB4ghZnIxv18CcaqNIft7vuGCcFBAIPIRTBprR+RuVq+xHDt3sNXdsvom4h49+Hky1b0ksCQBBwUtVaqH6ztCtwUF1j2c/Zcrt5P/uN7IHAd44K0gIJc1+Csr3qPG+G2yoqRM8KVqLI8Z2ZYn9c+AvEE+L9OQY=' 6 | 7 | /** 8 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 9 | * @description RSA加密 10 | * @param data 11 | * @returns {Promise<{param: PromiseLike}|*>} 12 | */ 13 | export async function encryptedData(data) { 14 | let publicKey = '' 15 | const res = await getPublicKey() 16 | publicKey = res.data.publicKey 17 | if (res.data.mockServer) { 18 | publicKey = '' 19 | } 20 | if (publicKey == '') { 21 | return data 22 | } 23 | const encrypt = new JSEncrypt() 24 | encrypt.setPublicKey( 25 | `-----BEGIN PUBLIC KEY-----${publicKey}-----END PUBLIC KEY-----` 26 | ) 27 | data = encrypt.encrypt(JSON.stringify(data)) 28 | return { 29 | param: data, 30 | } 31 | } 32 | 33 | /** 34 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 35 | * @description RSA解密 36 | * @param data 37 | * @returns {PromiseLike} 38 | */ 39 | export function decryptedData(data) { 40 | const decrypt = new JSEncrypt() 41 | decrypt.setPrivateKey( 42 | `-----BEGIN RSA PRIVATE KEY-----${privateKey}-----END RSA PRIVATE KEY-----` 43 | ) 44 | data = decrypt.decrypt(JSON.stringify(data)) 45 | return data 46 | } 47 | -------------------------------------------------------------------------------- /src/layouts/components/VabLogo/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 34 | 93 | -------------------------------------------------------------------------------- /src/store/modules/settings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 3 | * @description 所有全局配置的状态管理,如无必要请勿修改 4 | */ 5 | 6 | import defaultSettings from '@/config' 7 | 8 | const { tabsBar, logo, layout, header, themeBar } = defaultSettings 9 | const theme = JSON.parse(localStorage.getItem('vue-admin-better-theme')) || '' 10 | const state = { 11 | tabsBar: theme.tabsBar || tabsBar, 12 | logo, 13 | collapse: false, 14 | layout: theme.layout || layout, 15 | header: theme.header || header, 16 | device: 'desktop', 17 | themeBar, 18 | } 19 | const getters = { 20 | collapse: (state) => state.collapse, 21 | device: (state) => state.device, 22 | header: (state) => state.header, 23 | layout: (state) => state.layout, 24 | logo: (state) => state.logo, 25 | tabsBar: (state) => state.tabsBar, 26 | themeBar: (state) => state.themeBar, 27 | } 28 | const mutations = { 29 | changeLayout: (state, layout) => { 30 | if (layout) state.layout = layout 31 | }, 32 | changeHeader: (state, header) => { 33 | if (header) state.header = header 34 | }, 35 | changeTabsBar: (state, tabsBar) => { 36 | if (tabsBar) state.tabsBar = tabsBar 37 | }, 38 | changeCollapse: (state) => { 39 | state.collapse = !state.collapse 40 | }, 41 | foldSideBar: (state) => { 42 | state.collapse = true 43 | }, 44 | openSideBar: (state) => { 45 | state.collapse = false 46 | }, 47 | toggleDevice: (state, device) => { 48 | state.device = device 49 | }, 50 | } 51 | const actions = { 52 | changeLayout({ commit }, layout) { 53 | commit('changeLayout', layout) 54 | }, 55 | changeHeader({ commit }, header) { 56 | commit('changeHeader', header) 57 | }, 58 | changeTabsBar({ commit }, tabsBar) { 59 | commit('changeTabsBar', tabsBar) 60 | }, 61 | changeCollapse({ commit }) { 62 | commit('changeCollapse') 63 | }, 64 | foldSideBar({ commit }) { 65 | commit('foldSideBar') 66 | }, 67 | openSideBar({ commit }) { 68 | commit('openSideBar') 69 | }, 70 | toggleDevice({ commit }, device) { 71 | commit('toggleDevice', device) 72 | }, 73 | } 74 | export default { state, getters, mutations, actions } 75 | -------------------------------------------------------------------------------- /layouts/VabGithubCorner/index.vue: -------------------------------------------------------------------------------- 1 | 30 | 35 | 36 | 76 | -------------------------------------------------------------------------------- /src/config/setting.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 导出默认通用配置 3 | */ 4 | const setting = { 5 | // 开发以及部署时的URL 6 | publicPath: '', 7 | // 生产环境构建文件的目录名 8 | outputDir: 'dist', 9 | // 放置生成的静态资源 (js、css、img、fonts) 的 (相对于 outputDir 的) 目录。 10 | assetsDir: 'static', 11 | // 开发环境每次保存时是否输出为eslint编译警告 12 | lintOnSave: true, 13 | // 进行编译的依赖 14 | transpileDependencies: ['vue-echarts', 'resize-detector'], 15 | // 默认的接口地址 如果是开发环境和生产环境走vab-mock-server,当然你也可以选择自己配置成需要的接口地址 16 | baseURL: 17 | process.env.NODE_ENV === 'development' 18 | ? 'vab-mock-server' 19 | : 'vab-mock-server', 20 | //标题 (包括初次加载雪花屏的标题 页面的标题 浏览器的标题) 21 | title: 'vue-admin-better', 22 | //简写 23 | abbreviation: 'vab', 24 | //开发环境端口号 25 | devPort: '82', 26 | //版本号 27 | version: process.env.VUE_APP_VERSION, 28 | //这一项非常重要!请务必保留MIT协议下package.json及copyright作者信息 即可免费商用,不遵守此项约定你将无法使用该框架,如需自定义版权信息请联系QQ1204505056 29 | copyright: 'vab', 30 | //是否显示页面底部自定义版权信息 31 | footerCopyright: true, 32 | //是否显示顶部进度条 33 | progressBar: true, 34 | //缓存路由的最大数量 35 | keepAliveMaxNum: 99, 36 | // 路由模式,可选值为 history 或 hash 37 | routerMode: 'hash', 38 | //不经过token校验的路由 39 | routesWhiteList: ['/login', '/register', '/404', '/401'], 40 | //加载时显示文字 41 | loadingText: '正在加载中...', 42 | //token名称 43 | tokenName: 'accessToken', 44 | //token在localStorage、sessionStorage存储的key的名称 45 | tokenTableName: 'vue-admin-better', 46 | //token存储位置localStorage sessionStorage 47 | storage: 'localStorage', 48 | //token失效回退到登录页时是否记录本次的路由 49 | recordRoute: true, 50 | //是否显示logo,不显示时设置false,显示时请填写remixIcon图标名称,暂时只支持设置remixIcon 51 | logo: 'vuejs-fill', 52 | //是否显示在页面高亮错误 53 | errorLog: ['development'], 54 | //是否开启登录拦截 55 | loginInterception: true, 56 | //是否开启登录RSA加密 57 | loginRSA: false, 58 | //intelligence和all两种方式,前者后端权限只控制permissions不控制view文件的import(前后端配合,减轻后端工作量),all方式完全交给后端前端只负责加载 59 | authentication: 'intelligence', 60 | //vertical布局时是否只保持一个子菜单的展开 61 | uniqueOpened: true, 62 | //vertical布局时默认展开的菜单path,使用逗号隔开建议只展开一个 63 | defaultOopeneds: ['/vab'], 64 | //需要加loading层的请求,防止重复提交 65 | debounce: ['doEdit'], 66 | //需要自动注入并加载的模块 67 | providePlugin: { maptalks: 'maptalks', 'window.maptalks': 'maptalks' }, 68 | //npm run build时是否自动生成7z压缩包 69 | build7z: false, 70 | //代码生成机生成在view下的文件夹名称 71 | templateFolder: 'project', 72 | //是否显示终端donation打印 73 | donation: true, 74 | } 75 | module.exports = setting 76 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright chuzhixin 1204505056@qq.com 3 | * @description router全局配置,如有必要可分文件抽离 4 | */ 5 | 6 | import Vue from 'vue' 7 | import VueRouter from 'vue-router' 8 | import Layout from '@/layouts' 9 | import EmptyLayout from '@/layouts/EmptyLayout' 10 | import { publicPath, routerMode } from '@/config' 11 | 12 | Vue.use(VueRouter) 13 | 14 | export const constantRoutes = [ 15 | { 16 | path: '/login', 17 | component: () => import('@/views/login/index'), 18 | hidden: true, 19 | }, 20 | { 21 | path: '/register', 22 | component: () => import('@/views/register/index'), 23 | hidden: true, 24 | }, 25 | { 26 | path: '/401', 27 | name: '401', 28 | component: () => import('@/views/401'), 29 | hidden: true, 30 | }, 31 | { 32 | path: '/404', 33 | name: '404', 34 | component: () => import('@/views/404'), 35 | hidden: true, 36 | }, 37 | ] 38 | 39 | /*当settings.js里authentication配置的是intelligence时,views引入交给前端配置*/ 40 | export const asyncRoutes = [ 41 | { 42 | path: '/', 43 | component: Layout, 44 | redirect: '/index', 45 | children: [ 46 | { 47 | path: '/index', 48 | name: 'Index', 49 | component: () => import('@/views/index/index'), 50 | meta: { 51 | title: '首页', 52 | icon: 'home', 53 | affix: true, 54 | noKeepAlive: true, 55 | }, 56 | }, 57 | ], 58 | }, 59 | { 60 | path: '*', 61 | redirect: '/404', 62 | hidden: true, 63 | }, 64 | ] 65 | 66 | const router = new VueRouter({ 67 | base: routerMode === 'history' ? publicPath : '', 68 | mode: routerMode, 69 | scrollBehavior: () => ({ 70 | y: 0, 71 | }), 72 | routes: constantRoutes, 73 | }) 74 | //注释的地方是允许路由重复点击,如果你觉得框架路由跳转规范太过严格可选择放开 75 | /* const originalPush = VueRouter.prototype.push; 76 | VueRouter.prototype.push = function push(location, onResolve, onReject) { 77 | if (onResolve || onReject) 78 | return originalPush.call(this, location, onResolve, onReject); 79 | return originalPush.call(this, location).catch((err) => err); 80 | }; */ 81 | 82 | export function resetRouter() { 83 | router.matcher = new VueRouter({ 84 | base: routerMode === 'history' ? publicPath : '', 85 | mode: routerMode, 86 | scrollBehavior: () => ({ 87 | y: 0, 88 | }), 89 | routes: constantRoutes, 90 | }).matcher 91 | } 92 | 93 | export default router 94 | -------------------------------------------------------------------------------- /mock/controller/user.js: -------------------------------------------------------------------------------- 1 | const accessTokens = { 2 | admin: 'admin-accessToken', 3 | editor: 'editor-accessToken', 4 | test: 'test-accessToken', 5 | } 6 | 7 | module.exports = [ 8 | { 9 | url: '/publicKey', 10 | type: 'post', 11 | response() { 12 | return { 13 | code: 200, 14 | msg: 'success', 15 | data: { 16 | mockServer: true, 17 | publicKey: 18 | 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDBT2vr+dhZElF73FJ6xiP181txKWUSNLPQQlid6DUJhGAOZblluafIdLmnUyKE8mMHhT3R+Ib3ssZcJku6Hn72yHYj/qPkCGFv0eFo7G+GJfDIUeDyalBN0QsuiE/XzPHJBuJDfRArOiWvH0BXOv5kpeXSXM8yTt5Na1jAYSiQ/wIDAQAB', 19 | }, 20 | } 21 | }, 22 | }, 23 | { 24 | url: '/login', 25 | type: 'post', 26 | response(config) { 27 | const { username } = config.body 28 | const accessToken = accessTokens[username] 29 | if (!accessToken) { 30 | return { 31 | code: 500, 32 | msg: '帐户或密码不正确。', 33 | } 34 | } 35 | return { 36 | code: 200, 37 | msg: 'success', 38 | data: { accessToken }, 39 | } 40 | }, 41 | }, 42 | { 43 | url: '/register', 44 | type: 'post', 45 | response() { 46 | return { 47 | code: 200, 48 | msg: '模拟注册成功', 49 | } 50 | }, 51 | }, 52 | { 53 | url: '/userInfo', 54 | type: 'post', 55 | response(config) { 56 | const { accessToken } = config.body 57 | let permissions = ['admin'] 58 | let username = 'admin' 59 | if ('admin-accessToken' === accessToken) { 60 | permissions = ['admin'] 61 | username = 'admin' 62 | } 63 | if ('editor-accessToken' === accessToken) { 64 | permissions = ['editor'] 65 | username = 'editor' 66 | } 67 | if ('test-accessToken' === accessToken) { 68 | permissions = ['admin', 'editor'] 69 | username = 'test' 70 | } 71 | return { 72 | code: 200, 73 | msg: 'success', 74 | data: { 75 | permissions, 76 | username, 77 | 'avatar|1': [ 78 | 'https://i.gtimg.cn/club/item/face/img/2/15922_100.gif', 79 | 'https://i.gtimg.cn/club/item/face/img/8/15918_100.gif', 80 | ], 81 | }, 82 | } 83 | }, 84 | }, 85 | { 86 | url: '/logout', 87 | type: 'post', 88 | response() { 89 | return { 90 | code: 200, 91 | msg: 'success', 92 | } 93 | }, 94 | }, 95 | ] 96 | -------------------------------------------------------------------------------- /src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * @author https://vue-admin-better.com (不想保留author可删除) 3 | * @description 全局主题变量配置 4 | */ 5 | /* stylelint-disable */ 6 | @charset "utf-8"; 7 | //框架默认主题色 8 | $base-color-default: #409eff; 9 | //默认层级 10 | $base-z-index: 999; 11 | //横向布局纵向布局时菜单背景色 12 | $base-menu-background: #21252b; 13 | //菜单文字颜色 14 | $base-menu-color: hsla(0, 0%, 100%, 0.95); 15 | //菜单选中文字颜色 16 | $base-menu-color-active: hsla(0, 0%, 100%, 0.95); 17 | //菜单选中背景色 18 | $base-menu-background-active: $base-color-default; 19 | //标题颜色 20 | $base-title-color: #fff; 21 | //字体大小配置 22 | $base-font-size-small: 12px; 23 | $base-font-size-default: 14px; 24 | $base-font-size-big: 16px; 25 | $base-font-size-bigger: 18px; 26 | $base-font-size-max: 22px; 27 | $base-font-color: #606266; 28 | $base-color-blue: $base-color-default; 29 | $base-color-green: #41b882; 30 | $base-color-white: #fff; 31 | $base-color-black: #000; 32 | $base-color-yellow: #ffa91b; 33 | $base-color-orange: #ff6700; 34 | $base-color-red: #f34d37; 35 | $base-color-gray: rgba(0, 0, 0, 0.65); 36 | $base-main-width: 1279px; 37 | $base-border-radius: 4px; 38 | $base-border-color: #dcdfe6; 39 | //输入框高度 40 | $base-input-height: 32px; 41 | //默认paddiing 42 | $base-padding: 20px; 43 | //默认阴影 44 | $base-box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08); 45 | //横向布局时top-bar、logo、一级菜单的高度 46 | $base-top-bar-height: 65px; 47 | //纵向布局时logo的高度 48 | $base-logo-height: 75px; 49 | //顶部nav-bar的高度 50 | $base-nav-bar-height: 60px; 51 | //顶部多标签页tabs-bar的高度 52 | $base-tabs-bar-height: 55px; 53 | //顶部多标签页tabs-bar中每一个item的高度 54 | $base-tag-item-height: 34px; 55 | //菜单li标签的高度 56 | $base-menu-item-height: 50px; 57 | //app-main的高度 58 | $base-app-main-height: calc( 59 | 100vh - #{$base-nav-bar-height} - #{$base-tabs-bar-height} - #{$base-padding} - 60 | #{$base-padding} - 55px - 55px 61 | ); 62 | //纵向布局时左侧导航未折叠时的宽度 63 | $base-left-menu-width: 256px; 64 | //纵向布局时左侧导航未折叠时右侧内容的宽度 65 | $base-right-content-width: calc(100% - #{$base-left-menu-width}); 66 | //纵向布局时左侧导航已折叠时的宽度 67 | $base-left-menu-width-min: 65px; 68 | //纵向布局时左侧导航已折叠时右侧内容的宽度 69 | $base-right-content-width-min: calc(100% - #{$base-left-menu-width-min}); 70 | //默认动画 71 | $base-transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), border 0s, 72 | background 0s, color 0s, font-size 0s; 73 | //默认动画长 74 | $base-transition-time: 0.3s; 75 | 76 | :export { 77 | //菜单文字颜色变量导出 78 | menu-color: $base-menu-color; 79 | //菜单选中文字颜色变量导出 80 | menu-color-active: $base-menu-color-active; 81 | //菜单背景色变量导出 82 | menu-background: $base-menu-background; 83 | } 84 | -------------------------------------------------------------------------------- /src/colorfulIcon/svg/vab.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /layouts/VabSideBar/components/VabMenuItem.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 85 | -------------------------------------------------------------------------------- /src/config/permission.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 3 | * @description 路由守卫,目前两种模式:all模式与intelligence模式 4 | */ 5 | import router from '@/router' 6 | import store from '@/store' 7 | import VabProgress from 'nprogress' 8 | import 'nprogress/nprogress.css' 9 | import getPageTitle from '@/utils/pageTitle' 10 | import { 11 | authentication, 12 | loginInterception, 13 | progressBar, 14 | recordRoute, 15 | routesWhiteList, 16 | } from '@/config' 17 | 18 | VabProgress.configure({ 19 | easing: 'ease', 20 | speed: 500, 21 | trickleSpeed: 200, 22 | showSpinner: false, 23 | }) 24 | router.beforeResolve(async (to, from, next) => { 25 | if (progressBar) VabProgress.start() 26 | let hasToken = store.getters['user/accessToken'] 27 | 28 | if (!loginInterception) hasToken = true 29 | 30 | if (hasToken) { 31 | if (to.path === '/login') { 32 | next({ path: '/' }) 33 | if (progressBar) VabProgress.done() 34 | } else { 35 | const hasPermissions = 36 | store.getters['user/permissions'] && 37 | store.getters['user/permissions'].length > 0 38 | if (hasPermissions) { 39 | next() 40 | } else { 41 | try { 42 | let permissions 43 | if (!loginInterception) { 44 | //settings.js loginInterception为false时,创建虚拟权限 45 | await store.dispatch('user/setPermissions', ['admin']) 46 | permissions = ['admin'] 47 | } else { 48 | permissions = await store.dispatch('user/getUserInfo') 49 | } 50 | 51 | let accessRoutes = [] 52 | if (authentication === 'intelligence') { 53 | accessRoutes = await store.dispatch('routes/setRoutes', permissions) 54 | } else if (authentication === 'all') { 55 | accessRoutes = await store.dispatch('routes/setAllRoutes') 56 | } 57 | router.addRoutes(accessRoutes) 58 | next({ ...to, replace: true }) 59 | } catch { 60 | await store.dispatch('user/resetAccessToken') 61 | if (progressBar) VabProgress.done() 62 | } 63 | } 64 | } 65 | } else { 66 | if (routesWhiteList.indexOf(to.path) !== -1) { 67 | next() 68 | } else { 69 | if (recordRoute) { 70 | next(`/login?redirect=${to.path}`) 71 | } else { 72 | next('/login') 73 | } 74 | 75 | if (progressBar) VabProgress.done() 76 | } 77 | } 78 | document.title = getPageTitle(to.meta.title) 79 | }) 80 | router.afterEach(() => { 81 | if (progressBar) VabProgress.done() 82 | }) 83 | -------------------------------------------------------------------------------- /src/layouts/components/VabAppMain/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 77 | 78 | 99 | -------------------------------------------------------------------------------- /src/styles/spinner/dots.css: -------------------------------------------------------------------------------- 1 | .dots-loader:not(:required) { 2 | position: relative; 3 | display: inline-block; 4 | width: 7px; 5 | height: 7px; 6 | margin-bottom: 30px; 7 | overflow: hidden; 8 | text-indent: -9999px; 9 | background: transparent; 10 | border-radius: 100%; 11 | box-shadow: #f86 -14px -14px 0 7px, 12 | #fc6 14px -14px 0 7px, 13 | #6d7 14px 14px 0 7px, 14 | #4ae -14px 14px 0 7px; 15 | transform-origin: 50% 50%; 16 | animation: dots-loader 5s infinite ease-in-out; 17 | } 18 | 19 | @keyframes dots-loader { 20 | 0% { 21 | box-shadow: #f86 -14px -14px 0 7px, 22 | #fc6 14px -14px 0 7px, 23 | #6d7 14px 14px 0 7px, 24 | #4ae -14px 14px 0 7px; 25 | } 26 | 27 | 8.33% { 28 | box-shadow: #f86 14px -14px 0 7px, 29 | #fc6 14px -14px 0 7px, 30 | #6d7 14px 14px 0 7px, 31 | #4ae -14px 14px 0 7px; 32 | } 33 | 34 | 16.67% { 35 | box-shadow: #f86 14px 14px 0 7px, 36 | #fc6 14px 14px 0 7px, 37 | #6d7 14px 14px 0 7px, 38 | #4ae -14px 14px 0 7px; 39 | } 40 | 41 | 25% { 42 | box-shadow: #f86 -14px 14px 0 7px, 43 | #fc6 -14px 14px 0 7px, 44 | #6d7 -14px 14px 0 7px, 45 | #4ae -14px 14px 0 7px; 46 | } 47 | 48 | 33.33% { 49 | box-shadow: #f86 -14px -14px 0 7px, 50 | #fc6 -14px 14px 0 7px, 51 | #6d7 -14px -14px 0 7px, 52 | #4ae -14px -14px 0 7px; 53 | } 54 | 55 | 41.67% { 56 | box-shadow: #f86 14px -14px 0 7px, 57 | #fc6 -14px 14px 0 7px, 58 | #6d7 -14px -14px 0 7px, 59 | #4ae 14px -14px 0 7px; 60 | } 61 | 62 | 50% { 63 | box-shadow: #f86 14px 14px 0 7px, 64 | #fc6 -14px 14px 0 7px, 65 | #6d7 -14px -14px 0 7px, 66 | #4ae 14px -14px 0 7px; 67 | } 68 | 69 | 58.33% { 70 | box-shadow: #f86 -14px 14px 0 7px, 71 | #fc6 -14px 14px 0 7px, 72 | #6d7 -14px -14px 0 7px, 73 | #4ae 14px -14px 0 7px; 74 | } 75 | 76 | 66.67% { 77 | box-shadow: #f86 -14px -14px 0 7px, 78 | #fc6 -14px -14px 0 7px, 79 | #6d7 -14px -14px 0 7px, 80 | #4ae 14px -14px 0 7px; 81 | } 82 | 83 | 75% { 84 | box-shadow: #f86 14px -14px 0 7px, 85 | #fc6 14px -14px 0 7px, 86 | #6d7 14px -14px 0 7px, 87 | #4ae 14px -14px 0 7px; 88 | } 89 | 90 | 83.33% { 91 | box-shadow: #f86 14px 14px 0 7px, 92 | #fc6 14px -14px 0 7px, 93 | #6d7 14px 14px 0 7px, 94 | #4ae 14px 14px 0 7px; 95 | } 96 | 97 | 91.67% { 98 | box-shadow: #f86 -14px 14px 0 7px, 99 | #fc6 14px -14px 0 7px, 100 | #6d7 14px 14px 0 7px, 101 | #4ae -14px 14px 0 7px; 102 | } 103 | 104 | 100% { 105 | box-shadow: #f86 -14px -14px 0 7px, 106 | #fc6 14px -14px 0 7px, 107 | #6d7 14px 14px 0 7px, 108 | #4ae -14px 14px 0 7px; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /mock/mockServer.js: -------------------------------------------------------------------------------- 1 | const chokidar = require('chokidar') 2 | const bodyParser = require('body-parser') 3 | const chalk = require('chalk') 4 | const path = require('path') 5 | const Mock = require('mockjs') 6 | const { baseURL } = require('../src/config') 7 | const mockDir = path.join(process.cwd(), 'mock') 8 | 9 | /** 10 | * 11 | * @param app 12 | * @returns {{mockStartIndex: number, mockRoutesLength: number}} 13 | */ 14 | const registerRoutes = (app) => { 15 | let mockLastIndex 16 | const { mocks } = require('./index.js') 17 | const mocksForServer = mocks.map((route) => { 18 | return responseFake(route.url, route.type, route.response) 19 | }) 20 | for (const mock of mocksForServer) { 21 | app[mock.type](mock.url, mock.response) 22 | mockLastIndex = app._router.stack.length 23 | } 24 | const mockRoutesLength = Object.keys(mocksForServer).length 25 | return { 26 | mockRoutesLength: mockRoutesLength, 27 | mockStartIndex: mockLastIndex - mockRoutesLength, 28 | } 29 | } 30 | 31 | /** 32 | * 33 | * @param url 34 | * @param type 35 | * @param respond 36 | * @returns {{response(*=, *=): void, type: (*|string), url: RegExp}} 37 | */ 38 | const responseFake = (url, type, respond) => { 39 | return { 40 | url: new RegExp(`${baseURL}${url}`), 41 | type: type || 'get', 42 | response(req, res) { 43 | res.status(200) 44 | if (JSON.stringify(req.body) !== '{}') { 45 | console.log(chalk.green(`> 请求地址:${req.path}`)) 46 | console.log(chalk.green(`> 请求参数:${JSON.stringify(req.body)}\n`)) 47 | } else { 48 | console.log(chalk.green(`> 请求地址:${req.path}\n`)) 49 | } 50 | res.json( 51 | Mock.mock(respond instanceof Function ? respond(req, res) : respond) 52 | ) 53 | }, 54 | } 55 | } 56 | /** 57 | * 58 | * @param app 59 | */ 60 | module.exports = (app) => { 61 | app.use(bodyParser.json()) 62 | app.use( 63 | bodyParser.urlencoded({ 64 | extended: true, 65 | }) 66 | ) 67 | 68 | const mockRoutes = registerRoutes(app) 69 | let mockRoutesLength = mockRoutes.mockRoutesLength 70 | let mockStartIndex = mockRoutes.mockStartIndex 71 | chokidar 72 | .watch(mockDir, { 73 | ignoreInitial: true, 74 | }) 75 | .on('all', (event) => { 76 | if (event === 'change' || event === 'add') { 77 | try { 78 | app._router.stack.splice(mockStartIndex, mockRoutesLength) 79 | 80 | Object.keys(require.cache).forEach((item) => { 81 | if (item.includes(mockDir)) { 82 | delete require.cache[require.resolve(item)] 83 | } 84 | }) 85 | const mockRoutes = registerRoutes(app) 86 | mockRoutesLength = mockRoutes.mockRoutesLength 87 | mockStartIndex = mockRoutes.mockStartIndex 88 | } catch (error) { 89 | console.log(chalk.red(error)) 90 | } 91 | } 92 | }) 93 | } 94 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-admin-better-template", 3 | "version": "1.0.0", 4 | "private": true, 5 | "author": "vue-admin-better", 6 | "participants": [], 7 | "homepage": "https://chu1204505056.gitee.io/vue-admin-better", 8 | "scripts": { 9 | "serve": "vue-cli-service serve", 10 | "serve:node18": "set NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve", 11 | "build": "vue-cli-service build", 12 | "build:node18": "set NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service build", 13 | "lint": "vue-cli-service lint", 14 | "clear": "rimraf node_modules&&npm install --registry=--registry=https://registry.npmmirror.com", 15 | "image-webpack-loader": "cnpm i image-webpack-loader -D", 16 | "update": "ncu -u --reject layouts,sass-loader,sass,screenfull,eslint,chalk,vue-echarts,vue,vue-template-compiler,vue-router,vuex,@vue/cli-plugin-babel,@vue/cli-plugin-eslint,@vue/cli-service,eslint-plugin-vue --registry=https://registry.npmmirror.com&&cnpm i", 17 | "push": "start ./push.sh" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/chuzhixin/vue-admin-better-template.git" 22 | }, 23 | "husky": { 24 | "hooks": { 25 | "pre-commit": "lint-staged" 26 | } 27 | }, 28 | "lint-staged": { 29 | "src/**/*.{js,vue}": [ 30 | "eslint --fix", 31 | "git add" 32 | ] 33 | }, 34 | "dependencies": { 35 | "axios": "^0.21.1", 36 | "core-js": "^3.15.2", 37 | "dayjs": "^1.10.6", 38 | "element-ui": "^2.15.3", 39 | "js-cookie": "^3.0.0", 40 | "jsencrypt": "3.2.1", 41 | "lodash": "^4.17.21", 42 | "mockjs": "^1.1.0", 43 | "nprogress": "^0.2.0", 44 | "qs": "^6.10.1", 45 | "screenfull": "^5.1.0", 46 | "vab-icon": "file:vab-icon", 47 | "vue": "^2.6.14", 48 | "vue-router": "^3.5.2", 49 | "vuex": "^3.6.2", 50 | "layouts": "file:layouts" 51 | }, 52 | "devDependencies": { 53 | "@vue/cli-plugin-babel": "^4.5.13", 54 | "@vue/cli-plugin-eslint": "^4.5.13", 55 | "@vue/cli-service": "^4.5.13", 56 | "@vue/eslint-config-prettier": "^6.0.0", 57 | "babel-eslint": "^10.1.0", 58 | "body-parser": "^1.19.0", 59 | "chalk": "^4.1.1", 60 | "chokidar": "^3.5.2", 61 | "eslint": "^7.31.0", 62 | "eslint-plugin-prettier": "^3.4.0", 63 | "eslint-plugin-vue": "^7.14.0", 64 | "filemanager-webpack-plugin": "^6.1.4", 65 | "image-webpack-loader": "^7.0.1", 66 | "lint-staged": "^11.1.1", 67 | "plop": "^2.7.4", 68 | "prettier": "^2.3.2", 69 | "sass": "^1.32.8", 70 | "sass-loader": "^10.1.1", 71 | "stylelint": "^13.13.1", 72 | "stylelint-config-prettier": "^8.0.2", 73 | "stylelint-config-recess-order": "^2.4.0", 74 | "svg-sprite-loader": "^6.0.9", 75 | "vue-template-compiler": "^2.6.14", 76 | "webpackbar": "^4.0.0" 77 | }, 78 | "engines": { 79 | "node": ">=8.9", 80 | "npm": ">= 3.0.0" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /layouts/VabSideBar/components/VabSideBarItem.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 91 | 92 | 109 | -------------------------------------------------------------------------------- /src/store/modules/user.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 3 | * @description 登录、获取用户信息、退出登录、清除accessToken逻辑,不建议修改 4 | */ 5 | 6 | import Vue from 'vue' 7 | import { getUserInfo, login, logout } from '@/api/user' 8 | import { 9 | getAccessToken, 10 | removeAccessToken, 11 | setAccessToken, 12 | } from '@/utils/accessToken' 13 | import { resetRouter } from '@/router' 14 | import { title, tokenName } from '@/config' 15 | 16 | const state = { 17 | accessToken: getAccessToken(), 18 | username: '', 19 | avatar: '', 20 | permissions: [], 21 | } 22 | const getters = { 23 | accessToken: (state) => state.accessToken, 24 | username: (state) => state.username, 25 | avatar: (state) => state.avatar, 26 | permissions: (state) => state.permissions, 27 | } 28 | const mutations = { 29 | setAccessToken(state, accessToken) { 30 | state.accessToken = accessToken 31 | setAccessToken(accessToken) 32 | }, 33 | setUsername(state, username) { 34 | state.username = username 35 | }, 36 | setAvatar(state, avatar) { 37 | state.avatar = avatar 38 | }, 39 | setPermissions(state, permissions) { 40 | state.permissions = permissions 41 | }, 42 | } 43 | const actions = { 44 | setPermissions({ commit }, permissions) { 45 | commit('setPermissions', permissions) 46 | }, 47 | async login({ commit }, userInfo) { 48 | const { data } = await login(userInfo) 49 | const accessToken = data[tokenName] 50 | if (accessToken) { 51 | commit('setAccessToken', accessToken) 52 | const hour = new Date().getHours() 53 | const thisTime = 54 | hour < 8 55 | ? '早上好' 56 | : hour <= 11 57 | ? '上午好' 58 | : hour <= 13 59 | ? '中午好' 60 | : hour < 18 61 | ? '下午好' 62 | : '晚上好' 63 | Vue.prototype.$baseNotify(`欢迎登录${title}`, `${thisTime}!`) 64 | } else { 65 | Vue.prototype.$baseMessage( 66 | `登录接口异常,未正确返回${tokenName}...`, 67 | 'error' 68 | ) 69 | } 70 | }, 71 | async getUserInfo({ commit, state }) { 72 | const { data } = await getUserInfo(state.accessToken) 73 | if (!data) { 74 | Vue.prototype.$baseMessage('验证失败,请重新登录...', 'error') 75 | return false 76 | } 77 | let { permissions, username, avatar } = data 78 | if (permissions && username && Array.isArray(permissions)) { 79 | commit('setPermissions', permissions) 80 | commit('setUsername', username) 81 | commit('setAvatar', avatar) 82 | return permissions 83 | } else { 84 | Vue.prototype.$baseMessage('用户信息接口异常', 'error') 85 | return false 86 | } 87 | }, 88 | async logout({ dispatch }) { 89 | await logout(state.accessToken) 90 | await dispatch('resetAccessToken') 91 | await resetRouter() 92 | }, 93 | resetAccessToken({ commit }) { 94 | commit('setPermissions', []) 95 | commit('setAccessToken', '') 96 | removeAccessToken() 97 | }, 98 | } 99 | export default { state, getters, mutations, actions } 100 | -------------------------------------------------------------------------------- /src/layouts/components/VabAvatar/index.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 88 | 113 | -------------------------------------------------------------------------------- /src/utils/request.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import axios from 'axios' 3 | import { 4 | baseURL, 5 | contentType, 6 | debounce, 7 | invalidCode, 8 | noPermissionCode, 9 | requestTimeout, 10 | successCode, 11 | tokenName, 12 | loginInterception, 13 | } from '@/config' 14 | import store from '@/store' 15 | import qs from 'qs' 16 | import router from '@/router' 17 | import { isArray } from '@/utils/validate' 18 | 19 | let loadingInstance 20 | 21 | /** 22 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 23 | * @description 处理code异常 24 | * @param {*} code 25 | * @param {*} msg 26 | */ 27 | const handleCode = (code, msg) => { 28 | switch (code) { 29 | case invalidCode: 30 | Vue.prototype.$baseMessage(msg || `后端接口${code}异常`, 'error') 31 | store.dispatch('user/resetAccessToken').catch(() => {}) 32 | if (loginInterception) { 33 | location.reload() 34 | } 35 | break 36 | case noPermissionCode: 37 | router.push({ path: '/401' }).catch(() => {}) 38 | break 39 | default: 40 | Vue.prototype.$baseMessage(msg || `后端接口${code}异常`, 'error') 41 | break 42 | } 43 | } 44 | 45 | const instance = axios.create({ 46 | baseURL, 47 | timeout: requestTimeout, 48 | headers: { 49 | 'Content-Type': contentType, 50 | }, 51 | }) 52 | 53 | instance.interceptors.request.use( 54 | (config) => { 55 | if (store.getters['user/accessToken']) { 56 | config.headers[tokenName] = store.getters['user/accessToken'] 57 | } 58 | //这里会过滤所有为空、0、false的key,如果不需要请自行注释 59 | if (config.data) 60 | config.data = Vue.prototype.$baseLodash.pickBy( 61 | config.data, 62 | Vue.prototype.$baseLodash.identity 63 | ) 64 | if ( 65 | config.data && 66 | config.headers['Content-Type'] === 67 | 'application/x-www-form-urlencoded;charset=UTF-8' 68 | ) 69 | config.data = qs.stringify(config.data) 70 | if (debounce.some((item) => config.url.includes(item))) 71 | loadingInstance = Vue.prototype.$baseLoading() 72 | return config 73 | }, 74 | (error) => { 75 | return Promise.reject(error) 76 | } 77 | ) 78 | 79 | instance.interceptors.response.use( 80 | (response) => { 81 | if (loadingInstance) loadingInstance.close() 82 | 83 | const { data, config } = response 84 | const { code, msg } = data 85 | // 操作正常Code数组 86 | const codeVerificationArray = isArray(successCode) 87 | ? [...successCode] 88 | : [...[successCode]] 89 | // 是否操作正常 90 | if (codeVerificationArray.includes(code)) { 91 | return data 92 | } else { 93 | handleCode(code, msg) 94 | return Promise.reject( 95 | 'vue-admin-better请求异常拦截:' + 96 | JSON.stringify({ url: config.url, code, msg }) || 'Error' 97 | ) 98 | } 99 | }, 100 | (error) => { 101 | if (loadingInstance) loadingInstance.close() 102 | const { response, message } = error 103 | if (error.response && error.response.data) { 104 | const { status, data } = response 105 | handleCode(status, data.msg || message) 106 | return Promise.reject(error) 107 | } else { 108 | let { message } = error 109 | if (message === 'Network Error') { 110 | message = '后端接口连接异常' 111 | } 112 | if (message.includes('timeout')) { 113 | message = '后端接口请求超时' 114 | } 115 | if (message.includes('Request failed with status code')) { 116 | const code = message.substr(message.length - 3) 117 | message = '后端接口' + code + '异常' 118 | } 119 | Vue.prototype.$baseMessage(message || `后端接口未知异常`, 'error') 120 | return Promise.reject(error) 121 | } 122 | } 123 | ) 124 | 125 | export default instance 126 | -------------------------------------------------------------------------------- /src/layouts/components/VabNavBar/index.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 73 | 74 | 137 | -------------------------------------------------------------------------------- /layouts/VabSideBar/index.vue: -------------------------------------------------------------------------------- 1 | 25 | 61 | 145 | -------------------------------------------------------------------------------- /layouts/VabErrorLog/index.vue: -------------------------------------------------------------------------------- 1 | 66 | 67 | 116 | 117 | 129 | -------------------------------------------------------------------------------- /src/store/modules/tabsBar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 3 | * @description tabsBar多标签页逻辑,前期借鉴了很多开源项目发现都有个共同的特点很繁琐并不符合框架设计的初衷,后来在github用户cyea的启发下完成了重构,请勿修改 4 | */ 5 | 6 | const state = { 7 | visitedRoutes: [], 8 | } 9 | const getters = { 10 | visitedRoutes: (state) => state.visitedRoutes, 11 | } 12 | const mutations = { 13 | addVisitedRoute(state, route) { 14 | let target = state.visitedRoutes.find((item) => item.path === route.path) 15 | if (target) { 16 | if (route.fullPath !== target.fullPath) Object.assign(target, route) 17 | return 18 | } 19 | state.visitedRoutes.push(Object.assign({}, route)) 20 | }, 21 | delVisitedRoute(state, route) { 22 | state.visitedRoutes.forEach((item, index) => { 23 | if (item.path === route.path) state.visitedRoutes.splice(index, 1) 24 | }) 25 | }, 26 | delOthersVisitedRoute(state, route) { 27 | state.visitedRoutes = state.visitedRoutes.filter( 28 | (item) => item.meta.affix || item.path === route.path 29 | ) 30 | }, 31 | delLeftVisitedRoute(state, route) { 32 | let index = state.visitedRoutes.length 33 | state.visitedRoutes = state.visitedRoutes.filter((item) => { 34 | if (item.name === route.name) index = state.visitedRoutes.indexOf(item) 35 | return item.meta.affix || index <= state.visitedRoutes.indexOf(item) 36 | }) 37 | }, 38 | delRightVisitedRoute(state, route) { 39 | let index = state.visitedRoutes.length 40 | state.visitedRoutes = state.visitedRoutes.filter((item) => { 41 | if (item.name === route.name) index = state.visitedRoutes.indexOf(item) 42 | return item.meta.affix || index >= state.visitedRoutes.indexOf(item) 43 | }) 44 | }, 45 | delAllVisitedRoutes(state) { 46 | state.visitedRoutes = state.visitedRoutes.filter((item) => item.meta.affix) 47 | }, 48 | updateVisitedRoute(state, route) { 49 | state.visitedRoutes.forEach((item) => { 50 | if (item.path === route.path) item = Object.assign(item, route) 51 | }) 52 | }, 53 | } 54 | const actions = { 55 | addVisitedRoute({ commit }, route) { 56 | commit('addVisitedRoute', route) 57 | }, 58 | async delRoute({ dispatch, state }, route) { 59 | await dispatch('delVisitedRoute', route) 60 | return { 61 | visitedRoutes: [...state.visitedRoutes], 62 | } 63 | }, 64 | delVisitedRoute({ commit, state }, route) { 65 | commit('delVisitedRoute', route) 66 | return [...state.visitedRoutes] 67 | }, 68 | async delOthersRoutes({ dispatch, state }, route) { 69 | await dispatch('delOthersVisitedRoute', route) 70 | return { 71 | visitedRoutes: [...state.visitedRoutes], 72 | } 73 | }, 74 | async delLeftRoutes({ dispatch, state }, route) { 75 | await dispatch('delLeftVisitedRoute', route) 76 | return { 77 | visitedRoutes: [...state.visitedRoutes], 78 | } 79 | }, 80 | async delRightRoutes({ dispatch, state }, route) { 81 | await dispatch('delRightVisitedRoute', route) 82 | return { 83 | visitedRoutes: [...state.visitedRoutes], 84 | } 85 | }, 86 | delOthersVisitedRoute({ commit, state }, route) { 87 | commit('delOthersVisitedRoute', route) 88 | return [...state.visitedRoutes] 89 | }, 90 | delLeftVisitedRoute({ commit, state }, route) { 91 | commit('delLeftVisitedRoute', route) 92 | return [...state.visitedRoutes] 93 | }, 94 | delRightVisitedRoute({ commit, state }, route) { 95 | commit('delRightVisitedRoute', route) 96 | return [...state.visitedRoutes] 97 | }, 98 | async delAllRoutes({ dispatch, state }, route) { 99 | await dispatch('delAllVisitedRoutes', route) 100 | return { 101 | visitedRoutes: [...state.visitedRoutes], 102 | } 103 | }, 104 | delAllVisitedRoutes({ commit, state }) { 105 | commit('delAllVisitedRoutes') 106 | return [...state.visitedRoutes] 107 | }, 108 | updateVisitedRoute({ commit }, route) { 109 | commit('updateVisitedRoute', route) 110 | }, 111 | } 112 | export default { state, getters, mutations, actions } 113 | -------------------------------------------------------------------------------- /src/views/index/index.vue: -------------------------------------------------------------------------------- 1 | 114 | 115 | 127 | 151 | -------------------------------------------------------------------------------- /src/utils/vab.js: -------------------------------------------------------------------------------- 1 | import { loadingText, messageDuration, title } from '@/config' 2 | import * as lodash from 'lodash' 3 | import { Loading, Message, MessageBox, Notification } from 'element-ui' 4 | import store from '@/store' 5 | import { getAccessToken } from '@/utils/accessToken' 6 | 7 | const accessToken = store.getters['user/accessToken'] 8 | const layout = store.getters['settings/layout'] 9 | 10 | const install = (Vue, opts = {}) => { 11 | /* 全局accessToken */ 12 | Vue.prototype.$baseAccessToken = () => { 13 | return accessToken || getAccessToken() 14 | } 15 | /* 全局标题 */ 16 | Vue.prototype.$baseTitle = (() => { 17 | return title 18 | })() 19 | /* 全局加载层 */ 20 | Vue.prototype.$baseLoading = (index, text) => { 21 | let loading 22 | if (!index) { 23 | loading = Loading.service({ 24 | lock: true, 25 | text: text || loadingText, 26 | background: 'hsla(0,0%,100%,.8)', 27 | }) 28 | } else { 29 | loading = Loading.service({ 30 | lock: true, 31 | text: text || loadingText, 32 | spinner: 'vab-loading-type' + index, 33 | background: 'hsla(0,0%,100%,.8)', 34 | }) 35 | } 36 | return loading 37 | } 38 | /* 全局多彩加载层 */ 39 | Vue.prototype.$baseColorfullLoading = (index, text) => { 40 | let loading 41 | if (!index) { 42 | loading = Loading.service({ 43 | lock: true, 44 | text: text || loadingText, 45 | spinner: 'dots-loader', 46 | background: 'hsla(0,0%,100%,.8)', 47 | }) 48 | } else { 49 | switch (index) { 50 | case 1: 51 | index = 'dots' 52 | break 53 | case 2: 54 | index = 'gauge' 55 | break 56 | case 3: 57 | index = 'inner-circles' 58 | break 59 | case 4: 60 | index = 'plus' 61 | break 62 | } 63 | loading = Loading.service({ 64 | lock: true, 65 | text: text || loadingText, 66 | spinner: index + '-loader', 67 | background: 'hsla(0,0%,100%,.8)', 68 | }) 69 | } 70 | return loading 71 | } 72 | /* 全局Message */ 73 | Vue.prototype.$baseMessage = (message, type) => { 74 | Message({ 75 | offset: 60, 76 | showClose: true, 77 | message: message, 78 | type: type, 79 | dangerouslyUseHTMLString: true, 80 | duration: messageDuration, 81 | }) 82 | } 83 | 84 | /* 全局Alert */ 85 | Vue.prototype.$baseAlert = (content, title, callback) => { 86 | MessageBox.alert(content, title || '温馨提示', { 87 | confirmButtonText: '确定', 88 | dangerouslyUseHTMLString: true, 89 | callback: (action) => { 90 | if (callback) { 91 | callback() 92 | } 93 | }, 94 | }) 95 | } 96 | 97 | /* 全局Confirm */ 98 | Vue.prototype.$baseConfirm = (content, title, callback1, callback2) => { 99 | MessageBox.confirm(content, title || '温馨提示', { 100 | confirmButtonText: '确定', 101 | cancelButtonText: '取消', 102 | closeOnClickModal: false, 103 | type: 'warning', 104 | }) 105 | .then(() => { 106 | if (callback1) { 107 | callback1() 108 | } 109 | }) 110 | .catch(() => { 111 | if (callback2) { 112 | callback2() 113 | } 114 | }) 115 | } 116 | 117 | /* 全局Notification */ 118 | Vue.prototype.$baseNotify = (message, title, type, position) => { 119 | Notification({ 120 | title: title, 121 | message: message, 122 | position: position || 'top-right', 123 | type: type || 'success', 124 | duration: messageDuration, 125 | }) 126 | } 127 | 128 | /* 全局TableHeight */ 129 | Vue.prototype.$baseTableHeight = (formType) => { 130 | let height = window.innerHeight 131 | let paddingHeight = 400 132 | const formHeight = 50 133 | 134 | if (layout === 'vertical') { 135 | paddingHeight = 340 136 | } 137 | 138 | if ('number' == typeof formType) { 139 | height = height - paddingHeight - formHeight * formType 140 | } else { 141 | height = height - paddingHeight 142 | } 143 | return height 144 | } 145 | 146 | /* 全局lodash */ 147 | Vue.prototype.$baseLodash = lodash 148 | /* 全局事件总线 */ 149 | Vue.prototype.$baseEventBus = new Vue() 150 | } 151 | 152 | if (typeof window !== 'undefined' && window.Vue) { 153 | install(window.Vue) 154 | } 155 | 156 | export default install 157 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 3 | * @description cli配置 4 | */ 5 | 6 | const path = require('path') 7 | const { 8 | publicPath, 9 | assetsDir, 10 | outputDir, 11 | lintOnSave, 12 | transpileDependencies, 13 | title, 14 | abbreviation, 15 | devPort, 16 | providePlugin, 17 | build7z, 18 | donation, 19 | } = require('./src/config') 20 | const { webpackBarName, webpackBanner, donationConsole } = require('layouts') 21 | 22 | if (donation) donationConsole() 23 | const { version, author } = require('./package.json') 24 | const Webpack = require('webpack') 25 | const WebpackBar = require('webpackbar') 26 | const FileManagerPlugin = require('filemanager-webpack-plugin') 27 | const dayjs = require('dayjs') 28 | const date = dayjs().format('YYYY_M_D') 29 | const time = dayjs().format('YYYY-M-D HH:mm:ss') 30 | const productionGzipExtensions = ['html', 'js', 'css', 'svg'] 31 | process.env.VUE_APP_TITLE = title || 'vue-admin-better' 32 | process.env.VUE_APP_AUTHOR = author || 'chuzhixin 1204505056@qq.com' 33 | process.env.VUE_APP_UPDATE_TIME = time 34 | process.env.VUE_APP_VERSION = version 35 | 36 | const resolve = (dir) => path.join(__dirname, dir) 37 | const mockServer = () => { 38 | if (process.env.NODE_ENV === 'development') 39 | return require('./mock/mockServer.js') 40 | else return '' 41 | } 42 | 43 | module.exports = { 44 | publicPath, 45 | assetsDir, 46 | outputDir, 47 | lintOnSave, 48 | transpileDependencies, 49 | devServer: { 50 | hot: true, 51 | port: devPort, 52 | open: true, 53 | noInfo: false, 54 | overlay: { 55 | warnings: true, 56 | errors: true, 57 | }, 58 | after: mockServer(), 59 | }, 60 | configureWebpack() { 61 | return { 62 | resolve: { 63 | alias: { 64 | '@': resolve('src'), 65 | }, 66 | }, 67 | plugins: [ 68 | new Webpack.ProvidePlugin(providePlugin), 69 | new WebpackBar({ 70 | name: webpackBarName, 71 | }), 72 | ], 73 | } 74 | }, 75 | chainWebpack(config) { 76 | config.plugins.delete('preload') 77 | config.plugins.delete('prefetch') 78 | config.module 79 | .rule('svg') 80 | .exclude.add(resolve('src/remixIcon')) 81 | .add(resolve('src/colorfulIcon')) 82 | .end() 83 | 84 | config.module 85 | .rule('remixIcon') 86 | .test(/\.svg$/) 87 | .include.add(resolve('src/remixIcon')) 88 | .end() 89 | .use('svg-sprite-loader') 90 | .loader('svg-sprite-loader') 91 | .options({ symbolId: 'remix-icon-[name]' }) 92 | .end() 93 | 94 | config.module 95 | .rule('colorfulIcon') 96 | .test(/\.svg$/) 97 | .include.add(resolve('src/colorfulIcon')) 98 | .end() 99 | .use('svg-sprite-loader') 100 | .loader('svg-sprite-loader') 101 | .options({ symbolId: 'colorful-icon-[name]' }) 102 | .end() 103 | 104 | /* config.when(process.env.NODE_ENV === "development", (config) => { 105 | config.devtool("source-map"); 106 | }); */ 107 | config.when(process.env.NODE_ENV !== 'development', (config) => { 108 | config.performance.set('hints', false) 109 | config.devtool('none') 110 | config.optimization.splitChunks({ 111 | chunks: 'all', 112 | cacheGroups: { 113 | libs: { 114 | name: 'chunk-libs', 115 | test: /[\\/]node_modules[\\/]/, 116 | priority: 10, 117 | chunks: 'initial', 118 | }, 119 | elementUI: { 120 | name: 'chunk-elementUI', 121 | priority: 20, 122 | test: /[\\/]node_modules[\\/]_?element-ui(.*)/, 123 | }, 124 | fortawesome: { 125 | name: 'chunk-fortawesome', 126 | priority: 20, 127 | test: /[\\/]node_modules[\\/]_?@fortawesome(.*)/, 128 | }, 129 | }, 130 | }) 131 | config 132 | .plugin('banner') 133 | .use(Webpack.BannerPlugin, [`${webpackBanner}${time}`]) 134 | .end() 135 | config.module 136 | .rule('images') 137 | .use('image-webpack-loader') 138 | .loader('image-webpack-loader') 139 | .options({ 140 | bypassOnDebug: true, 141 | }) 142 | .end() 143 | }) 144 | 145 | if (build7z) { 146 | config.when(process.env.NODE_ENV === 'production', (config) => { 147 | config 148 | .plugin('fileManager') 149 | .use(FileManagerPlugin, [ 150 | { 151 | onEnd: { 152 | delete: [`./${outputDir}/video`, `./${outputDir}/data`], 153 | archive: [ 154 | { 155 | source: `./${outputDir}`, 156 | destination: `./${outputDir}/${abbreviation}_${outputDir}_${date}.7z`, 157 | }, 158 | ], 159 | }, 160 | }, 161 | ]) 162 | .end() 163 | }) 164 | } 165 | }, 166 | runtimeCompiler: true, 167 | productionSourceMap: false, 168 | css: { 169 | requireModuleExtension: true, 170 | sourceMap: true, 171 | loaderOptions: { 172 | scss: { 173 | /*sass-loader 8.0语法 */ 174 | //prependData: '@import "~@/styles/variables.scss";', 175 | 176 | /*sass-loader 9.0写法,感谢github用户 shaonialife*/ 177 | additionalData(content, loaderContext) { 178 | const { resourcePath, rootContext } = loaderContext 179 | const relativePath = path.relative(rootContext, resourcePath) 180 | if ( 181 | relativePath.replace(/\\/g, '/') !== 'src/styles/variables.scss' 182 | ) { 183 | return '@import "~@/styles/variables.scss";' + content 184 | } 185 | return content 186 | }, 187 | }, 188 | }, 189 | }, 190 | } 191 | -------------------------------------------------------------------------------- /src/utils/validate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 3 | * @description 判读是否为外链 4 | * @param path 5 | * @returns {boolean} 6 | */ 7 | export function isExternal(path) { 8 | return /^(https?:|mailto:|tel:)/.test(path) 9 | } 10 | 11 | /** 12 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 13 | * @description 校验密码是否小于6位 14 | * @param str 15 | * @returns {boolean} 16 | */ 17 | export function isPassword(str) { 18 | return str.length >= 6 19 | } 20 | 21 | /** 22 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 23 | * @description 判断是否为数字 24 | * @param value 25 | * @returns {boolean} 26 | */ 27 | export function isNumber(value) { 28 | const reg = /^[0-9]*$/ 29 | return reg.test(value) 30 | } 31 | 32 | /** 33 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 34 | * @description 判断是否是名称 35 | * @param value 36 | * @returns {boolean} 37 | */ 38 | export function isName(value) { 39 | const reg = /^[\u4e00-\u9fa5a-zA-Z0-9]+$/ 40 | return reg.test(value) 41 | } 42 | 43 | /** 44 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 45 | * @description 判断是否为IP 46 | * @param ip 47 | * @returns {boolean} 48 | */ 49 | export function isIP(ip) { 50 | const reg = 51 | /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/ 52 | return reg.test(ip) 53 | } 54 | 55 | /** 56 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 57 | * @description 判断是否是传统网站 58 | * @param url 59 | * @returns {boolean} 60 | */ 61 | export function isUrl(url) { 62 | const reg = 63 | /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/ 64 | return reg.test(url) 65 | } 66 | 67 | /** 68 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 69 | * @description 判断是否是小写字母 70 | * @param str 71 | * @returns {boolean} 72 | */ 73 | export function isLowerCase(str) { 74 | const reg = /^[a-z]+$/ 75 | return reg.test(str) 76 | } 77 | 78 | /** 79 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 80 | * @description 判断是否是大写字母 81 | * @param str 82 | * @returns {boolean} 83 | */ 84 | export function isUpperCase(str) { 85 | const reg = /^[A-Z]+$/ 86 | return reg.test(str) 87 | } 88 | 89 | /** 90 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 91 | * @description 判断是否是大写字母开头 92 | * @param str 93 | * @returns {boolean} 94 | */ 95 | export function isAlphabets(str) { 96 | const reg = /^[A-Za-z]+$/ 97 | return reg.test(str) 98 | } 99 | 100 | /** 101 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 102 | * @description 判断是否是字符串 103 | * @param str 104 | * @returns {boolean} 105 | */ 106 | export function isString(str) { 107 | return typeof str === 'string' || str instanceof String 108 | } 109 | 110 | /** 111 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 112 | * @description 判断是否是数组 113 | * @param arg 114 | * @returns {arg is any[]|boolean} 115 | */ 116 | export function isArray(arg) { 117 | if (typeof Array.isArray === 'undefined') { 118 | return Object.prototype.toString.call(arg) === '[object Array]' 119 | } 120 | return Array.isArray(arg) 121 | } 122 | 123 | /** 124 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 125 | * @description 判断是否是端口号 126 | * @param str 127 | * @returns {boolean} 128 | */ 129 | export function isPort(str) { 130 | const reg = 131 | /^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$/ 132 | return reg.test(str) 133 | } 134 | 135 | /** 136 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 137 | * @description 判断是否是手机号 138 | * @param str 139 | * @returns {boolean} 140 | */ 141 | export function isPhone(str) { 142 | const reg = /^1\d{10}$/ 143 | return reg.test(str) 144 | } 145 | 146 | /** 147 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 148 | * @description 判断是否是身份证号(第二代) 149 | * @param str 150 | * @returns {boolean} 151 | */ 152 | export function isIdCard(str) { 153 | const reg = 154 | /^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/ 155 | return reg.test(str) 156 | } 157 | 158 | /** 159 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 160 | * @description 判断是否是邮箱 161 | * @param str 162 | * @returns {boolean} 163 | */ 164 | export function isEmail(str) { 165 | const reg = /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/ 166 | return reg.test(str) 167 | } 168 | 169 | /** 170 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 171 | * @description 判断是否中文 172 | * @param str 173 | * @returns {boolean} 174 | */ 175 | export function isChina(str) { 176 | const reg = /^[\u4E00-\u9FA5]{2,4}$/ 177 | return reg.test(str) 178 | } 179 | 180 | /** 181 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 182 | * @description 判断是否为空 183 | * @param str 184 | * @returns {boolean} 185 | */ 186 | export function isBlank(str) { 187 | return ( 188 | str == null || 189 | false || 190 | str === '' || 191 | str.trim() === '' || 192 | str.toLocaleLowerCase().trim() === 'null' 193 | ) 194 | } 195 | 196 | /** 197 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 198 | * @description 判断是否为固话 199 | * @param str 200 | * @returns {boolean} 201 | */ 202 | export function isTel(str) { 203 | const reg = 204 | /^(400|800)([0-9\\-]{7,10})|(([0-9]{4}|[0-9]{3})(-| )?)?([0-9]{7,8})((-| |转)*([0-9]{1,4}))?$/ 205 | return reg.test(str) 206 | } 207 | 208 | /** 209 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 210 | * @description 判断是否为数字且最多两位小数 211 | * @param str 212 | * @returns {boolean} 213 | */ 214 | export function isNum(str) { 215 | const reg = /^\d+(\.\d{1,2})?$/ 216 | return reg.test(str) 217 | } 218 | -------------------------------------------------------------------------------- /src/styles/vab.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 3 | * @description 全局样式 4 | */ 5 | 6 | @charset "utf-8"; 7 | 8 | @import './normalize.scss'; 9 | @import './transition.scss'; 10 | @import './loading.scss'; 11 | $base: '.vab'; 12 | 13 | @mixin scrollbar { 14 | max-height: 88vh; 15 | margin-bottom: 0.5vh; 16 | overflow-y: auto; 17 | 18 | &::-webkit-scrollbar { 19 | width: 0; 20 | height: 0; 21 | background: transparent; 22 | } 23 | 24 | &::-webkit-scrollbar-thumb { 25 | background-color: rgba(144, 147, 153, 0.3); 26 | border-radius: 10px; 27 | } 28 | 29 | &::-webkit-scrollbar-thumb:hover { 30 | background-color: rgba(144, 147, 153, 0.3); 31 | } 32 | } 33 | 34 | @mixin base-scrollbar { 35 | &::-webkit-scrollbar { 36 | width: 13px; 37 | height: 13px; 38 | } 39 | 40 | &::-webkit-scrollbar-thumb { 41 | background-color: rgba(0, 0, 0, 0.4); 42 | background-clip: padding-box; 43 | border: 3px solid transparent; 44 | border-radius: 7px; 45 | } 46 | 47 | &::-webkit-scrollbar-thumb:hover { 48 | background-color: rgba(0, 0, 0, 0.5); 49 | } 50 | 51 | &::-webkit-scrollbar-track { 52 | background-color: transparent; 53 | } 54 | 55 | &::-webkit-scrollbar-track:hover { 56 | background-color: #f8fafc; 57 | } 58 | } 59 | 60 | img { 61 | object-fit: cover; 62 | } 63 | 64 | a { 65 | color: $base-color-blue; 66 | text-decoration: none; 67 | cursor: pointer; 68 | } 69 | 70 | * { 71 | transition: $base-transition; 72 | } 73 | svg { 74 | transition: none; 75 | * { 76 | transition: none; 77 | } 78 | } 79 | 80 | html { 81 | body { 82 | position: relative; 83 | height: 100vh; 84 | padding: 0; 85 | margin: 0; 86 | font-family: Avenir, Helvetica, Arial, sans-serif; 87 | font-size: $base-font-size-default; 88 | color: #2c3e50; 89 | background: #f6f8f9; 90 | -webkit-font-smoothing: antialiased; 91 | -moz-osx-font-smoothing: grayscale; 92 | 93 | @include base-scrollbar; 94 | 95 | div { 96 | @include base-scrollbar; 97 | } 98 | 99 | svg, 100 | i { 101 | &:hover { 102 | opacity: 0.8; 103 | } 104 | } 105 | 106 | .v-modal { 107 | backdrop-filter: blur(10px); 108 | } 109 | 110 | /* el-tag开始 */ 111 | .el-tag + .el-tag { 112 | margin-left: 10px; 113 | } 114 | 115 | /* el-tag结束 */ 116 | 117 | /* markdown编辑器开始 */ 118 | .editor-toolbar { 119 | .no-mobile, 120 | .fa-question-circle { 121 | display: none; 122 | } 123 | } 124 | 125 | /* markdown编辑器结束 */ 126 | 127 | /* 间隔线开始 */ 128 | .el-divider--horizontal { 129 | margin: 10px 0 25px 0; 130 | 131 | .el-divider__text { 132 | display: -webkit-box; 133 | overflow: hidden; 134 | text-overflow: ellipsis; 135 | -webkit-line-clamp: 1; 136 | -webkit-box-orient: vertical; 137 | } 138 | } 139 | 140 | /* 间隔线结束 */ 141 | 142 | /* 大图展示开始 */ 143 | .el-image-viewer { 144 | &__close { 145 | .el-icon-circle-close { 146 | color: $base-color-white; 147 | } 148 | } 149 | } 150 | 151 | /* 大图展示结束 */ 152 | 153 | .vue-admin-better-wrapper { 154 | .app-main-container { 155 | @include base-scrollbar; 156 | 157 | > [class*='-container'] { 158 | * { 159 | transition: none; 160 | } 161 | padding: $base-padding; 162 | background: $base-color-white; 163 | } 164 | } 165 | } 166 | 167 | /* 进度条开始 */ 168 | #nprogress { 169 | position: fixed; 170 | z-index: $base-z-index; 171 | 172 | .bar { 173 | background: $base-color-blue !important; 174 | } 175 | 176 | .peg { 177 | box-shadow: 0 0 10px $base-color-blue, 0 0 5px $base-color-blue !important; 178 | } 179 | } 180 | 181 | /* 进度条结束 */ 182 | 183 | /* 表格开始 */ 184 | 185 | .el-table { 186 | .el-table__body-wrapper { 187 | @include base-scrollbar; 188 | } 189 | 190 | th { 191 | background: #f5f7fa; 192 | } 193 | 194 | td, 195 | th { 196 | position: relative; 197 | box-sizing: border-box; 198 | padding: 7.5px 0; 199 | 200 | .cell { 201 | font-size: $base-font-size-default; 202 | font-weight: normal; 203 | color: #606266; 204 | 205 | .el-image { 206 | width: 50px; 207 | height: 50px; 208 | border-radius: $base-border-radius; 209 | } 210 | } 211 | } 212 | } 213 | 214 | /* 表格结束 */ 215 | 216 | /* 分页开始 */ 217 | .el-pagination { 218 | padding: 2px 5px; 219 | margin: 15px 0 0 0; 220 | font-weight: normal; 221 | color: $base-color-black; 222 | text-align: center; 223 | } 224 | 225 | /* 分页结束 */ 226 | 227 | /* 菜单开始 */ 228 | .el-menu.el-menu--popup.el-menu--popup-right-start { 229 | @include scrollbar; 230 | } 231 | 232 | .el-menu.el-menu--popup.el-menu--popup-bottom-start { 233 | @include scrollbar; 234 | } 235 | 236 | .el-submenu__title i { 237 | color: $base-color-white; 238 | } 239 | 240 | /* 菜单结束 */ 241 | 242 | /* 弹窗开始 */ 243 | 244 | .el-dialog, 245 | .el-message-box { 246 | &__body { 247 | border-top: 1px solid $base-border-color; 248 | 249 | .el-form { 250 | padding-right: 30px; 251 | } 252 | } 253 | 254 | &__footer { 255 | padding: $base-padding; 256 | text-align: right; 257 | border-top: 1px solid $base-border-color; 258 | } 259 | 260 | &__content { 261 | padding: 20px 20px 20px 20px; 262 | } 263 | } 264 | 265 | /* 弹窗结束 */ 266 | 267 | /* 卡片开始 */ 268 | .el-card { 269 | margin-bottom: 15px; 270 | 271 | &__body { 272 | padding: $base-padding; 273 | } 274 | } 275 | 276 | /* 卡片结束 */ 277 | 278 | /* 下拉树样式-----------开始 */ 279 | .select-tree-popper { 280 | .el-scrollbar { 281 | .el-scrollbar__view { 282 | .el-select-dropdown__item { 283 | height: auto; 284 | max-height: 274px; 285 | padding: 0; 286 | overflow-y: auto; 287 | line-height: 26px; 288 | } 289 | } 290 | } 291 | } 292 | 293 | /* 下拉树样式-----------结束 */ 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /layouts/VabTopBar/index.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 86 | 225 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 3 | * @description 格式化时间 4 | * @param time 5 | * @param cFormat 6 | * @returns {string|null} 7 | */ 8 | export function parseTime(time, cFormat) { 9 | if (arguments.length === 0) { 10 | return null 11 | } 12 | const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}' 13 | let date 14 | if (typeof time === 'object') { 15 | date = time 16 | } else { 17 | if (typeof time === 'string' && /^[0-9]+$/.test(time)) { 18 | time = parseInt(time) 19 | } 20 | if (typeof time === 'number' && time.toString().length === 10) { 21 | time = time * 1000 22 | } 23 | date = new Date(time) 24 | } 25 | const formatObj = { 26 | y: date.getFullYear(), 27 | m: date.getMonth() + 1, 28 | d: date.getDate(), 29 | h: date.getHours(), 30 | i: date.getMinutes(), 31 | s: date.getSeconds(), 32 | a: date.getDay(), 33 | } 34 | const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => { 35 | let value = formatObj[key] 36 | if (key === 'a') { 37 | return ['日', '一', '二', '三', '四', '五', '六'][value] 38 | } 39 | if (result.length > 0 && value < 10) { 40 | value = '0' + value 41 | } 42 | return value || 0 43 | }) 44 | return time_str 45 | } 46 | 47 | /** 48 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 49 | * @description 格式化时间 50 | * @param time 51 | * @param option 52 | * @returns {string} 53 | */ 54 | export function formatTime(time, option) { 55 | if (('' + time).length === 10) { 56 | time = parseInt(time) * 1000 57 | } else { 58 | time = +time 59 | } 60 | const d = new Date(time) 61 | const now = Date.now() 62 | 63 | const diff = (now - d) / 1000 64 | 65 | if (diff < 30) { 66 | return '刚刚' 67 | } else if (diff < 3600) { 68 | // less 1 hour 69 | return Math.ceil(diff / 60) + '分钟前' 70 | } else if (diff < 3600 * 24) { 71 | return Math.ceil(diff / 3600) + '小时前' 72 | } else if (diff < 3600 * 24 * 2) { 73 | return '1天前' 74 | } 75 | if (option) { 76 | return parseTime(time, option) 77 | } else { 78 | return ( 79 | d.getMonth() + 80 | 1 + 81 | '月' + 82 | d.getDate() + 83 | '日' + 84 | d.getHours() + 85 | '时' + 86 | d.getMinutes() + 87 | '分' 88 | ) 89 | } 90 | } 91 | 92 | /** 93 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 94 | * @description 将url请求参数转为json格式 95 | * @param url 96 | * @returns {{}|any} 97 | */ 98 | export function paramObj(url) { 99 | const search = url.split('?')[1] 100 | if (!search) { 101 | return {} 102 | } 103 | return JSON.parse( 104 | '{"' + 105 | decodeURIComponent(search) 106 | .replace(/"/g, '\\"') 107 | .replace(/&/g, '","') 108 | .replace(/=/g, '":"') 109 | .replace(/\+/g, ' ') + 110 | '"}' 111 | ) 112 | } 113 | 114 | /** 115 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 116 | * @description 父子关系的数组转换成树形结构数据 117 | * @param data 118 | * @returns {*} 119 | */ 120 | export function translateDataToTree(data) { 121 | const parent = data.filter( 122 | (value) => value.parentId === 'undefined' || value.parentId == null 123 | ) 124 | const children = data.filter( 125 | (value) => value.parentId !== 'undefined' && value.parentId != null 126 | ) 127 | const translator = (parent, children) => { 128 | parent.forEach((parent) => { 129 | children.forEach((current, index) => { 130 | if (current.parentId === parent.id) { 131 | const temp = JSON.parse(JSON.stringify(children)) 132 | temp.splice(index, 1) 133 | translator([current], temp) 134 | typeof parent.children !== 'undefined' 135 | ? parent.children.push(current) 136 | : (parent.children = [current]) 137 | } 138 | }) 139 | }) 140 | } 141 | translator(parent, children) 142 | return parent 143 | } 144 | 145 | /** 146 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 147 | * @description 树形结构数据转换成父子关系的数组 148 | * @param data 149 | * @returns {[]} 150 | */ 151 | export function translateTreeToData(data) { 152 | const result = [] 153 | data.forEach((item) => { 154 | const loop = (data) => { 155 | result.push({ 156 | id: data.id, 157 | name: data.name, 158 | parentId: data.parentId, 159 | }) 160 | const child = data.children 161 | if (child) { 162 | for (let i = 0; i < child.length; i++) { 163 | loop(child[i]) 164 | } 165 | } 166 | } 167 | loop(item) 168 | }) 169 | return result 170 | } 171 | 172 | /** 173 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 174 | * @description 10位时间戳转换 175 | * @param time 176 | * @returns {string} 177 | */ 178 | export function tenBitTimestamp(time) { 179 | const date = new Date(time * 1000) 180 | const y = date.getFullYear() 181 | let m = date.getMonth() + 1 182 | m = m < 10 ? '' + m : m 183 | let d = date.getDate() 184 | d = d < 10 ? '' + d : d 185 | let h = date.getHours() 186 | h = h < 10 ? '0' + h : h 187 | let minute = date.getMinutes() 188 | let second = date.getSeconds() 189 | minute = minute < 10 ? '0' + minute : minute 190 | second = second < 10 ? '0' + second : second 191 | return y + '年' + m + '月' + d + '日 ' + h + ':' + minute + ':' + second //组合 192 | } 193 | 194 | /** 195 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 196 | * @description 13位时间戳转换 197 | * @param time 198 | * @returns {string} 199 | */ 200 | export function thirteenBitTimestamp(time) { 201 | const date = new Date(time / 1) 202 | const y = date.getFullYear() 203 | let m = date.getMonth() + 1 204 | m = m < 10 ? '' + m : m 205 | let d = date.getDate() 206 | d = d < 10 ? '' + d : d 207 | let h = date.getHours() 208 | h = h < 10 ? '0' + h : h 209 | let minute = date.getMinutes() 210 | let second = date.getSeconds() 211 | minute = minute < 10 ? '0' + minute : minute 212 | second = second < 10 ? '0' + second : second 213 | return y + '年' + m + '月' + d + '日 ' + h + ':' + minute + ':' + second //组合 214 | } 215 | 216 | /** 217 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 218 | * @description 获取随机id 219 | * @param length 220 | * @returns {string} 221 | */ 222 | export function uuid(length = 32) { 223 | const num = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890' 224 | let str = '' 225 | for (let i = 0; i < length; i++) { 226 | str += num.charAt(Math.floor(Math.random() * num.length)) 227 | } 228 | return str 229 | } 230 | 231 | /** 232 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 233 | * @description m到n的随机数 234 | * @param m 235 | * @param n 236 | * @returns {number} 237 | */ 238 | export function random(m, n) { 239 | return Math.floor(Math.random() * (m - n) + n) 240 | } 241 | 242 | /** 243 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 244 | * @description addEventListener 245 | * @type {function(...[*]=)} 246 | */ 247 | export const on = (function () { 248 | return function (element, event, handler, useCapture = false) { 249 | if (element && event && handler) { 250 | element.addEventListener(event, handler, useCapture) 251 | } 252 | } 253 | })() 254 | 255 | /** 256 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 257 | * @description removeEventListener 258 | * @type {function(...[*]=)} 259 | */ 260 | export const off = (function () { 261 | return function (element, event, handler, useCapture = false) { 262 | if (element && event) { 263 | element.removeEventListener(event, handler, useCapture) 264 | } 265 | } 266 | })() 267 | -------------------------------------------------------------------------------- /src/styles/loading.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * @author chuzhixin 1204505056@qq.com (不想保留author可删除) 3 | * @description 全局加载动画 4 | */ 5 | 6 | @charset "utf-8"; 7 | 8 | @import "./spinner/dots.css"; 9 | @import "./spinner/gauge.css"; 10 | @import "./spinner/inner-circles.css"; 11 | @import "./spinner/plus.css"; 12 | 13 | $base-loading: ".vab-loading-type"; 14 | 15 | /* 自定义loading开始 */ 16 | #{$base-loading}1 { 17 | display: flex; 18 | width: 36px; 19 | height: 36px; 20 | margin: 0 auto 15px; 21 | border: 3px solid transparent; 22 | border-top-color: $base-color-blue; 23 | border-bottom-color: $base-color-blue; 24 | border-radius: 50%; 25 | animation: vabLoading1-0 0.8s linear infinite; 26 | } 27 | 28 | #{$base-loading}1::before { 29 | display: block; 30 | width: 8px; 31 | height: 8px; 32 | margin: auto; 33 | content: ""; 34 | border: 3px solid $base-color-blue; 35 | border-radius: 50%; 36 | animation: vabLoading1 0.5s alternate ease-in infinite; 37 | } 38 | 39 | @keyframes vabLoading1-0 { 40 | to { 41 | transform: rotate(360deg); 42 | } 43 | } 44 | 45 | @keyframes vabLoading1 { 46 | from { 47 | transform: scale(0.5); 48 | } 49 | 50 | to { 51 | transform: scale(1.2); 52 | } 53 | } 54 | 55 | #{$base-loading}2 { 56 | width: 20px; 57 | height: 20px; 58 | margin-top: -40px; 59 | margin-left: -10px; 60 | animation: vabLoading2 1s linear reverse infinite; 61 | } 62 | 63 | #{$base-loading}2::before { 64 | display: block; 65 | width: 36px; 66 | height: 36px; 67 | margin-top: -17px; 68 | margin-left: -18px; 69 | content: ""; 70 | animation: vabLoading2 0.4s linear infinite; 71 | } 72 | 73 | #{$base-loading}2::after { 74 | display: block; 75 | width: 8px; 76 | height: 8px; 77 | margin-top: -3px; 78 | margin-left: -4px; 79 | content: ""; 80 | animation: vabLoading2 0.4s linear infinite; 81 | } 82 | 83 | #{$base-loading}2::before, 84 | #{$base-loading}2, 85 | #{$base-loading}2::after { 86 | position: absolute; 87 | top: 40%; 88 | left: 50%; 89 | border: 3px solid transparent; 90 | border-top-color: $base-color-blue; 91 | border-right-color: $base-color-blue; 92 | border-radius: 50%; 93 | } 94 | 95 | @keyframes vabLoading2 { 96 | to { 97 | transform: rotate(360deg); 98 | } 99 | } 100 | 101 | #{$base-loading}3 { 102 | display: inline-block; 103 | width: 2.5em; 104 | height: 3em; 105 | margin-bottom: 15px; 106 | border: 3px solid transparent; 107 | border-top-color: $base-color-blue; 108 | border-bottom-color: $base-color-blue; 109 | border-radius: 50%; 110 | animation: vabLoading3 2s ease infinite; 111 | } 112 | 113 | @keyframes vabLoading3 { 114 | 50% { 115 | border-width: 8px; 116 | transform: rotate(360deg) scale(0.4, 0.33); 117 | } 118 | 119 | 100% { 120 | border-width: 3px; 121 | transform: rotate(720deg) scale(1, 1); 122 | } 123 | } 124 | 125 | #{$base-loading}4 { 126 | display: inline-block; 127 | width: 30px; 128 | height: 30px; 129 | margin: 0 auto 10px; 130 | border: 8px solid transparent; 131 | border-bottom-color: $base-color-blue; 132 | border-left-color: $base-color-blue; 133 | border-radius: 50%; 134 | animation: vabLoading4 1s linear infinite normal; 135 | } 136 | 137 | #{$base-loading}4::after { 138 | display: block; 139 | width: 15px; 140 | height: 15px; 141 | margin: 0; 142 | content: " "; 143 | border: 6px solid $base-color-blue; 144 | border-bottom-color: transparent; 145 | border-left-color: transparent; 146 | border-radius: 50%; 147 | } 148 | 149 | @keyframes vabLoading4 { 150 | 0% { 151 | opacity: 0.2; 152 | transform: rotate(0deg); 153 | } 154 | 155 | 50% { 156 | opacity: 1; 157 | transform: rotate(180deg); 158 | } 159 | 160 | 100% { 161 | opacity: 0.2; 162 | transform: rotate(360deg); 163 | } 164 | } 165 | 166 | #{$base-loading}5 { 167 | display: block; 168 | width: 0; 169 | height: 0; 170 | margin: 0 auto 15px; 171 | border: solid 1.5em $base-color-blue; 172 | border-right: solid 1.5em transparent; 173 | border-left: solid 1.5em transparent; 174 | border-radius: 100%; 175 | animation: vabLoading5 1s linear infinite; 176 | } 177 | 178 | @keyframes vabLoading5 { 179 | 0% { 180 | transform: rotate(0deg); 181 | } 182 | 183 | 50% { 184 | transform: rotate(60deg); 185 | } 186 | 187 | 100% { 188 | transform: rotate(360deg); 189 | } 190 | } 191 | 192 | #{$base-loading}6 { 193 | display: block; 194 | width: 0; 195 | height: 0; 196 | margin: 0 auto 25px auto; 197 | perspective: 200px; 198 | } 199 | 200 | #{$base-loading}6::before, 201 | #{$base-loading}6::after { 202 | position: absolute; 203 | width: 20px; 204 | height: 20px; 205 | content: ""; 206 | background: rgba(0, 0, 0, 0); 207 | animation: vabLoading6 0.5s infinite alternate; 208 | } 209 | 210 | #{$base-loading}6::before { 211 | left: 0; 212 | } 213 | 214 | #{$base-loading}6::after { 215 | right: 0; 216 | animation-delay: 0.15s; 217 | } 218 | 219 | @keyframes vabLoading6 { 220 | 0% { 221 | box-shadow: 0 0 0 rgba(0, 0, 0, 0); 222 | transform: scale(1) translateY(0) rotateX(0deg); 223 | } 224 | 225 | 100% { 226 | background: $base-color-blue; 227 | box-shadow: 0 25px 40px rgba($base-color-blue, 0.5); 228 | transform: scale(1.2) translateY(-25px) rotateX(45deg); 229 | } 230 | } 231 | 232 | #{$base-loading}7 { 233 | display: block; 234 | width: 25px; 235 | height: 25px; 236 | margin: 0 auto 15px auto; 237 | border: 2px solid $base-color-blue; 238 | border-top-color: rgba($base-color-blue, 0.2); 239 | border-right-color: rgba($base-color-blue, 0.2); 240 | border-bottom-color: rgba($base-color-blue, 0.2); 241 | border-radius: 100%; 242 | animation: vabLoading7 infinite 0.75s linear; 243 | } 244 | 245 | @keyframes vabLoading7 { 246 | 0% { 247 | transform: rotate(0); 248 | } 249 | 250 | 100% { 251 | transform: rotate(360deg); 252 | } 253 | } 254 | 255 | #{$base-loading}8 { 256 | position: relative; 257 | box-sizing: border-box; 258 | display: block; 259 | width: 20px; 260 | height: 20px; 261 | margin: 0 auto 15px auto; 262 | background-color: $base-color-blue; 263 | border-radius: 50%; 264 | box-shadow: 30px 0 0 0 $base-color-blue; 265 | transform: translateX(-15px); 266 | } 267 | 268 | #{$base-loading}8::after { 269 | position: absolute; 270 | top: 8px; 271 | left: 9px; 272 | width: 10px; 273 | height: 10px; 274 | content: ""; 275 | background-color: $base-color-white; 276 | border-radius: 50%; 277 | box-shadow: 30px 0 0 0 $base-color-white; 278 | animation: vabLoading8 2s ease-in-out infinite alternate; 279 | } 280 | 281 | @keyframes vabLoading8 { 282 | 0% { 283 | left: 9px; 284 | } 285 | 286 | 100% { 287 | left: 1px; 288 | } 289 | } 290 | 291 | #{$base-loading}9 { 292 | position: relative; 293 | box-sizing: border-box; 294 | display: block; 295 | width: 20px; 296 | height: 20px; 297 | margin: 0 auto 15px auto; 298 | border: 1px $base-color-blue solid; 299 | animation: vabLoading9 5s linear infinite; 300 | } 301 | 302 | #{$base-loading}9::after { 303 | position: absolute; 304 | top: -8px; 305 | left: 0; 306 | width: 4px; 307 | height: 4px; 308 | content: ""; 309 | background-color: $base-color-blue; 310 | animation: vabLoading9_check 1s ease-in-out infinite; 311 | } 312 | 313 | @keyframes vabLoading9_check { 314 | 25% { 315 | top: -8px; 316 | left: 22px; 317 | } 318 | 319 | 50% { 320 | top: 22px; 321 | left: 22px; 322 | } 323 | 324 | 75% { 325 | top: 22px; 326 | left: -9px; 327 | } 328 | 329 | 100% { 330 | top: -7px; 331 | left: -9px; 332 | } 333 | } 334 | 335 | @keyframes vabLoading9 { 336 | 0% { 337 | box-shadow: inset 0 0 0 0 rgba($base-color-blue, 0.5); 338 | opacity: 0.5; 339 | } 340 | 341 | 100% { 342 | box-shadow: inset 0 -20px 0 0 $base-color-blue; 343 | } 344 | } 345 | 346 | /* 自定义loading结束 */ 347 | -------------------------------------------------------------------------------- /src/styles/normalize.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 4 | 5 | /* Document 6 | ========================================================================== */ 7 | 8 | /** 9 | * 1. Correct the line height in all browsers. 10 | * 2. Prevent adjustments of font size after orientation changes in iOS. 11 | */ 12 | 13 | html { 14 | line-height: 1.15; /* 1 */ 15 | -webkit-text-size-adjust: 100%; /* 2 */ 16 | } 17 | 18 | /* Sections 19 | ========================================================================== */ 20 | 21 | /** 22 | * Remove the margin in all browsers. 23 | */ 24 | 25 | body { 26 | margin: 0; 27 | } 28 | 29 | /** 30 | * Render the `main` element consistently in IE. 31 | */ 32 | 33 | main { 34 | display: block; 35 | } 36 | 37 | /** 38 | * Correct the font size and margin on `h1` elements within `section` and 39 | * `article` contexts in Chrome, Firefox, and Safari. 40 | */ 41 | 42 | h1 { 43 | margin: 0.67em 0; 44 | font-size: 2em; 45 | } 46 | 47 | /* Grouping content 48 | ========================================================================== */ 49 | 50 | /** 51 | * 1. Add the correct box sizing in Firefox. 52 | * 2. Show the overflow in Edge and IE. 53 | */ 54 | 55 | hr { 56 | box-sizing: content-box; /* 1 */ 57 | height: 0; /* 1 */ 58 | overflow: visible; /* 2 */ 59 | } 60 | 61 | /** 62 | * 1. Correct the inheritance and scaling of font size in all browsers. 63 | * 2. Correct the odd `em` font sizing in all browsers. 64 | */ 65 | 66 | pre { 67 | font-family: monospace; /* 1 */ 68 | font-size: 1em; /* 2 */ 69 | } 70 | 71 | /* Text-level semantics 72 | ========================================================================== */ 73 | 74 | /** 75 | * Remove the gray background on active links in IE 10. 76 | */ 77 | 78 | a { 79 | background-color: transparent; 80 | } 81 | 82 | /** 83 | * 1. Remove the bottom border in Chrome 57- 84 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 85 | */ 86 | 87 | abbr[title] { 88 | text-decoration: underline; /* 2 */ 89 | text-decoration: underline dotted; /* 2 */ 90 | border-bottom: none; /* 1 */ 91 | } 92 | 93 | /** 94 | * Add the correct font weight in Chrome, Edge, and Safari. 95 | */ 96 | 97 | b, 98 | strong { 99 | font-weight: bolder; 100 | } 101 | 102 | /** 103 | * 1. Correct the inheritance and scaling of font size in all browsers. 104 | * 2. Correct the odd `em` font sizing in all browsers. 105 | */ 106 | 107 | code, 108 | kbd, 109 | samp { 110 | font-family: monospace; /* 1 */ 111 | font-size: 1em; /* 2 */ 112 | } 113 | 114 | /** 115 | * Add the correct font size in all browsers. 116 | */ 117 | 118 | small { 119 | font-size: 80%; 120 | } 121 | 122 | /** 123 | * Prevent `sub` and `sup` elements from affecting the line height in 124 | * all browsers. 125 | */ 126 | 127 | sub, 128 | sup { 129 | position: relative; 130 | font-size: 75%; 131 | line-height: 0; 132 | vertical-align: baseline; 133 | } 134 | 135 | sub { 136 | bottom: -0.25em; 137 | } 138 | 139 | sup { 140 | top: -0.5em; 141 | } 142 | 143 | /* Embedded content 144 | ========================================================================== */ 145 | 146 | /** 147 | * Remove the border on images inside links in IE 10. 148 | */ 149 | 150 | img { 151 | border-style: none; 152 | } 153 | 154 | /* Forms 155 | ========================================================================== */ 156 | 157 | /** 158 | * 1. Change the font styles in all browsers. 159 | * 2. Remove the margin in Firefox and Safari. 160 | */ 161 | 162 | button, 163 | input, 164 | optgroup, 165 | select, 166 | textarea { 167 | margin: 0; /* 2 */ 168 | font-family: inherit; /* 1 */ 169 | font-size: 100%; /* 1 */ 170 | line-height: 1.15; /* 1 */ 171 | } 172 | 173 | /** 174 | * Show the overflow in IE. 175 | * 1. Show the overflow in Edge. 176 | */ 177 | 178 | button, 179 | input { 180 | /* 1 */ 181 | overflow: visible; 182 | } 183 | 184 | /** 185 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 186 | * 1. Remove the inheritance of text transform in Firefox. 187 | */ 188 | 189 | button, 190 | select { 191 | /* 1 */ 192 | text-transform: none; 193 | } 194 | 195 | /** 196 | * Correct the inability to style clickable types in iOS and Safari. 197 | */ 198 | 199 | button, 200 | [type="button"], 201 | [type="reset"], 202 | [type="submit"] { 203 | -webkit-appearance: button; 204 | } 205 | 206 | /** 207 | * Remove the inner border and padding in Firefox. 208 | */ 209 | 210 | button::-moz-focus-inner, 211 | [type="button"]::-moz-focus-inner, 212 | [type="reset"]::-moz-focus-inner, 213 | [type="submit"]::-moz-focus-inner { 214 | padding: 0; 215 | border-style: none; 216 | } 217 | 218 | /** 219 | * Restore the focus styles unset by the previous rule. 220 | */ 221 | 222 | button:-moz-focusring, 223 | [type="button"]:-moz-focusring, 224 | [type="reset"]:-moz-focusring, 225 | [type="submit"]:-moz-focusring { 226 | outline: 1px dotted ButtonText; 227 | } 228 | 229 | /** 230 | * Correct the padding in Firefox. 231 | */ 232 | 233 | fieldset { 234 | padding: 0.35em 0.75em 0.625em; 235 | } 236 | 237 | /** 238 | * 1. Correct the text wrapping in Edge and IE. 239 | * 2. Correct the color inheritance from `fieldset` elements in IE. 240 | * 3. Remove the padding so developers are not caught out when they zero out 241 | * `fieldset` elements in all browsers. 242 | */ 243 | 244 | legend { 245 | box-sizing: border-box; /* 1 */ 246 | display: table; /* 1 */ 247 | max-width: 100%; /* 1 */ 248 | padding: 0; /* 3 */ 249 | color: inherit; /* 2 */ 250 | white-space: normal; /* 1 */ 251 | } 252 | 253 | /** 254 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 255 | */ 256 | 257 | progress { 258 | vertical-align: baseline; 259 | } 260 | 261 | /** 262 | * Remove the default vertical scrollbar in IE 10+. 263 | */ 264 | 265 | textarea { 266 | overflow: auto; 267 | } 268 | 269 | /** 270 | * 1. Add the correct box sizing in IE 10. 271 | * 2. Remove the padding in IE 10. 272 | */ 273 | 274 | [type="checkbox"], 275 | [type="radio"] { 276 | box-sizing: border-box; /* 1 */ 277 | padding: 0; /* 2 */ 278 | } 279 | 280 | /** 281 | * Correct the cursor style of increment and decrement buttons in Chrome. 282 | */ 283 | 284 | [type="number"]::-webkit-inner-spin-button, 285 | [type="number"]::-webkit-outer-spin-button { 286 | height: auto; 287 | } 288 | 289 | /** 290 | * 1. Correct the odd appearance in Chrome and Safari. 291 | * 2. Correct the outline style in Safari. 292 | */ 293 | 294 | [type="search"] { 295 | -webkit-appearance: textfield; /* 1 */ 296 | outline-offset: -2px; /* 2 */ 297 | } 298 | 299 | /** 300 | * Remove the inner padding in Chrome and Safari on macOS. 301 | */ 302 | 303 | [type="search"]::-webkit-search-decoration { 304 | -webkit-appearance: none; 305 | } 306 | 307 | /** 308 | * 1. Correct the inability to style clickable types in iOS and Safari. 309 | * 2. Change font properties to `inherit` in Safari. 310 | */ 311 | 312 | ::-webkit-file-upload-button { 313 | -webkit-appearance: button; /* 1 */ 314 | font: inherit; /* 2 */ 315 | } 316 | 317 | /* Interactive 318 | ========================================================================== */ 319 | 320 | /* 321 | * Add the correct display in Edge, IE 10+, and Firefox. 322 | */ 323 | 324 | details { 325 | display: block; 326 | } 327 | 328 | /* 329 | * Add the correct display in all browsers. 330 | */ 331 | 332 | summary { 333 | display: list-item; 334 | } 335 | 336 | /* Misc 337 | ========================================================================== */ 338 | 339 | /** 340 | * Add the correct display in IE 10+. 341 | */ 342 | 343 | template { 344 | display: none; 345 | } 346 | 347 | /** 348 | * Add the correct display in IE 10. 349 | */ 350 | 351 | [hidden] { 352 | display: none; 353 | } 354 | -------------------------------------------------------------------------------- /src/views/401.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 81 | 82 | 297 | -------------------------------------------------------------------------------- /src/views/404.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 81 | 82 | 297 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 简体中文 | [English](./README.en.md) 4 | 5 |
6 |

vue-admin-better

7 | 8 |

众志成城,攻坚克难,愿所有美好纷沓而来!

9 |
10 | 11 | [![stars](https://img.shields.io/github/stars/chuzhixin/vue-admin-beautiful?style=flat-square&logo=GitHub)](https://github.com/chuzhixin/vue-admin-beautiful) 12 | [![star](https://gitee.com/chu1204505056/vue-admin-better/badge/star.svg?theme=gray)](https://gitee.com/chu1204505056/vue-admin-better) 13 | [![license](https://img.shields.io/github/license/chuzhixin/vue-admin-beautiful?style=flat-square)](https://en.wikipedia.org/wiki/MIT_License) 14 | 15 | --- 16 | 17 | ## 🎉 特性 18 | 19 | - 💪 40+高质量单页 20 | - 💅 RBAC 模型 + JWT 权限控制 21 | - 🌍 10 万+ 项目实际应用 22 | - 👏 良好的类型定义 23 | - 🥳 开源版本支持免费商用 24 | - 🚀 跨平台 PC、手机端、平板 25 | - 📦️ 后端路由动态渲染 26 | 27 | ## 🌐 地址 28 | 29 | - [🎉 vue2.x + element-ui(免费商用,支持 PC、平板、手机)](https://vue-admin-beautiful.com/vue-admin-beautiful-element/) 30 | 31 | - [⚡️ vue3.x + element-plus(alpha 版本,免费商用,支持 PC、平板、手机)](https://vue-admin-beautiful.com/vue-admin-beautiful-element-plus/) 32 | 33 | - [⚡️ vue3.x + ant-design-vue(beta 版本,免费商用,支持 PC、平板、手机)](https://vue-admin-beautiful.com/vue-admin-beautiful-antdv/) 34 | 35 | - [⚡️ vue3.x + vite + arco](https://vue-admin-beautiful.com/vue-admin-arco/) 36 | 37 | - [🚀 admin pro 演示地址(vue2.x 付费版本,支持 PC、平板、手机)](https://vue-admin-beautiful.com/admin-pro/) 38 | 39 | - [🚀 admin plus 演示地址(vue3.x 付费版本,支持 PC、平板、手机)](https://vue-admin-beautiful.com/admin-plus/) 40 | 41 | - [📌 pro 及 plus 购买地址 authorization](https://vue-admin-beautiful.com/authorization/) 42 | 43 | - [🚀 Vue Shop Vite 商城(付费版本)](https://vue-admin-beautiful.com/shop-vite/) 44 | 45 | - [🌐 github 仓库地址](https://github.com/chuzhixin/vue-admin-beautiful?utm_source=gold_browser_extension) 46 | 47 | - [🌐 码云仓库地址](https://gitee.com/chu1204505056/vue-admin-better?_from=gitee_search) 48 | 49 | ## 🌐 备份地址 50 | 51 | - [🚀 admin pro 演示地址(付费版本,支持 PC、平板、手机)](https://chu1204505056.gitee.io/admin-pro/) 52 | 53 | - [🚀 admin plus 演示地址(vue3.x 付费版本,支持 PC、平板、手机)](https://chu1204505056.gitee.io/admin-plus/) 54 | 55 | ## 🍻 前端讨论 QQ 群 56 | 57 | - 请我们喝杯咖啡,打赏后联系 QQ 783963206 邀请您进入讨论群(由于用户数较多,如果您打赏后未通过好友请求,请联系商家),不管您请还是不请,您都可以享受到开源的代码,感谢您的支持和信任,群内提供 vue-admin-better 基础版本、开发工具自动配置教程及项目开发文档。 58 | 59 | 60 | 63 | 66 | 69 | 70 |
61 | 62 | 64 | 65 | 67 | 68 |
71 | 72 | ## 📦️ 桌面应用程序 73 | 74 | - [Admin Pro](https://gitee.com/chu1204505056/microsoft-store/raw/master/AdminPlus.zip) 75 | - [Admin Plus](https://gitee.com/chu1204505056/microsoft-store/raw/master/AdminPlus.zip) 76 | 77 | ## 🌱 vue3.x vue3.0-antdv 分支(ant-design-vue)[点击切换分支](https://github.com/chuzhixin/vue-admin-better/tree/vue3.0-antdv) 78 | 79 | ```bash 80 | # 克隆项目 81 | git clone -b vue3.0-antdv https://github.com/chuzhixin/vue-admin-better.git 82 | # 安装依赖 83 | npm i --registry=http://mirrors.cloud.tencent.com/npm/ 84 | # 本地开发 启动项目 85 | npm run serve 86 | ``` 87 | 88 | ## 🌱 vue3.x arco-design [点击切换仓库](https://github.com/chuzhixin/vue-admin-arco) 89 | 90 | ```bash 91 | # 克隆项目 92 | git clone https://github.com/chuzhixin/vue-admin-arco.git 93 | # 安装依赖 94 | npm i --registry=http://mirrors.cloud.tencent.com/npm/ 95 | # 本地开发 启动项目 96 | npm run dev 97 | ``` 98 | 99 | ## 🌱vue2.x master 分支(element-ui)[点击切换分支](https://github.com/chuzhixin/vue-admin-better/tree/master) 100 | 101 | ```bash 102 | # 克隆项目 103 | git clone -b master https://github.com/chuzhixin/vue-admin-better.git 104 | # 安装依赖 105 | npm i --registry=http://mirrors.cloud.tencent.com/npm/ 106 | # 本地开发 启动项目 107 | npm run serve 108 | ``` 109 | 110 | ## 🔊 友情链接 111 | 112 | - [OPSLI 基于 vue-admin-better 开源版的最佳实践](https://github.com/hiparker/opsli-boot) 113 | 114 | - [uView uni-app 生态最优秀的 UI 框架](https://github.com/YanxinNet/uView/) 115 | 116 | - [form-generator Element 表单设计代码生成器](https://github.com/JakHuang/form-generator/) 117 | 118 | - [wangEditor 国产最强开源富文本编辑](https://github.com/wangeditor-team/wangEditor) 119 | 120 | ## 🙈 我们承诺将定期赞助的开源项目(感谢巨人) 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | ## 🎨 鸣谢 133 | 134 | | Project | 135 | | ---------------------------------------------------------------- | 136 | | [vue](https://github.com/vuejs/vue) | 137 | | [element-ui](https://github.com/ElemeFE/element) | 138 | | [element-plus](https://github.com/element-plus/element-plus) | 139 | | [ant-design-vue](https://github.com/vueComponent/ant-design-vue) | 140 | | [mock](https://github.com/nuysoft/Mock) | 141 | | [axios](https://github.com/axios/axios) | 142 | | [wangEditor](https://github.com/wangeditor-team/wangEditor) | 143 | 144 | ## 👷 框架杰出贡献者(排名不分先后) 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | ## 📌 优势及注意事项 163 | 164 | ``` 165 | 对比其他开源 admin 框架有如下优势: 166 | 1. 支持前端控制路由权限 intelligence、后端控制路由权限 all 模式 167 | 2. 已知开源 vue admin 框架中首家支持 mock 自动生成自动导出功能 168 | 3. 提供 50 余项全局精细化配置 169 | 4. 支持 scss 自动排序,eslint 自动修复 170 | 5. axios 精细化封装,支持多数据源、多成功 code 数组,支持 application/json;charset=UTF-8、application/x-www-form-urlencoded;charset=UTF-8 多种传参方式 171 | 6. 支持登录RSA加密 172 | 7. 支持打包自动生成7Z压缩包 173 | 8. 支持errorlog错误拦截 174 | 9. 支持多主题、多布局切换 175 | 176 | 使用注意事项: 177 | 1. 项目默认使用lf换行符而非crlf换行符,新建文件时请注意选择文件换行符 178 | 2. 项目默认使用的最严格的eslint校验规范(plugin:vue/recommended),使用之前建议配置开发工具实现自动修复(建议使用vscode开发) 179 | 3. 项目使用的是要求最宽泛的MIT开源协议,保留MIT开源协议即可免费商用 180 | 181 | ``` 182 | 183 | ## 💚 适合人群 184 | 185 | - 正在以及想使用 element-ui/element-plus 开发,前端开发经验 1 年+。 186 | - 熟悉 Vue.js 技术栈,使用它开发过几个实际项目。 187 | - 对原理技术感兴趣,想进阶和提升的同学。 188 | 189 | ## 🎉 功能地图 190 | 191 | ![img](https://fastly.jsdelivr.net/gh/chuzhixin/image/vip/flow.drawio.png) 192 | 193 | ## 🗃️ 效果图 194 | 195 | 以下是截取的是 pro 版的效果图展示: 196 | 197 | 198 | 199 | 202 | 205 | 206 | 207 | 210 | 213 | 214 | 215 | 218 | 221 | 222 |
200 | 201 | 203 | 204 |
208 | 209 | 211 | 212 |
216 | 217 | 219 | 220 |
223 | 224 | ## 📄 商用注意事项 225 | 226 | 此项目可免费用于商业用途,请遵守 MIT 协议并保留作者技术支持声明。 227 | 228 |
229 | -------------------------------------------------------------------------------- /src/layouts/components/VabThemeBar/index.vue: -------------------------------------------------------------------------------- 1 | 67 | 68 | 184 | 185 | 247 | 262 | --------------------------------------------------------------------------------