├── .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 |
2 |
3 |
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 |
2 |
3 |
4 |
5 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
26 |
--------------------------------------------------------------------------------
/layouts/VabQueryForm/VabQueryFormRightPanel.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
19 |
20 |
64 |
--------------------------------------------------------------------------------
/layouts/VabFullScreenBar/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
12 |
13 |
54 |
--------------------------------------------------------------------------------
/src/layouts/components/VabAd/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
17 |
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 |
2 |
8 |
11 |
12 |
13 |
48 |
49 |
66 |
--------------------------------------------------------------------------------
/layouts/VabSideBar/components/VabSubmenu.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
18 | {{ item.meta.title }}
19 |
20 |
21 |
22 |
23 |
24 |
61 |
--------------------------------------------------------------------------------
/src/layouts/components/VabBreadcrumb/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ item.meta.title }}
5 |
6 |
7 |
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 |
2 |
8 |
11 |
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 |
2 |
3 |
4 |
5 |
6 |
11 | {{ title }}
12 |
13 |
14 |
15 |
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 |
2 |
8 |
28 |
29 |
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 |
22 |
--------------------------------------------------------------------------------
/layouts/VabSideBar/components/VabMenuItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 | {{ routeChildren.meta.title }}
9 |
14 | {{ routeChildren.meta.badge }}
15 |
16 |
17 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
14 |
15 |
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 |
2 |
9 |
10 |
16 |
17 |
18 |
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 |
2 |
3 |
4 |
5 |
6 |
7 | {{ username }}
8 |
9 |
10 |
11 |
12 |
13 | github地址
14 | 码云地址
15 | pro付费版地址
16 | plus付费版地址
17 |
18 | shop-vite付费版地址
19 |
20 | 退出登录
21 |
22 |
23 |
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 |
2 |
3 |
4 |
5 |
6 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
26 |
27 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
73 |
74 |
137 |
--------------------------------------------------------------------------------
/layouts/VabSideBar/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
15 |
16 |
21 |
22 |
23 |
24 |
25 |
61 |
145 |
--------------------------------------------------------------------------------
/layouts/VabErrorLog/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
65 |
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 |
2 |
3 |
4 |
5 |
6 |
7 | 开源版本
8 |
9 | 永久免费 个人/商业使用
10 |
11 |
12 |
13 |
14 | - 永久开源免费,支持横纵布局切换
15 | -
16 | 保留浏览器控制台打印即可免费商用,页面中的作者信息可全部去除,无需保留
17 |
18 | -
19 | 开源地址
20 |
24 | 如果有幸帮到了你,麻烦给个star
25 |
26 |
27 | - 提供讨论群专属文档,QQ群 972435319
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | VIP群(自愿原则)
36 |
37 | ¥100(不再提供)
38 |
39 |
40 |
41 |
42 | - 为防止不必要的误解,开源版不再提供vip群
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | 开源版不保留MIT协议,变更作者(自愿原则)
51 |
52 | ¥299
53 |
54 |
55 |
56 |
57 | - 支持以上所有特权,不包含VIP群
58 | - 包含开源基础版授权与开源集成版授权
59 | - 永久更新
60 | - 提供低价外包合作机会
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | PRO版
69 |
70 | ¥699
71 |
72 |
73 |
86 |
87 |
88 |
89 |
90 |
91 | Plus版
92 |
93 | ¥799
94 |
95 |
96 |
109 |
110 |
111 |
112 |
113 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
17 |
18 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
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 |
2 |
3 |
4 |
5 |
6 |
28 |
29 |
30 |
31 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
81 |
82 |
297 |
--------------------------------------------------------------------------------
/src/views/404.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
28 |
29 |
30 |
31 |
39 |
40 |
41 |
42 |
43 |
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 | [](https://github.com/chuzhixin/vue-admin-beautiful)
12 | [](https://gitee.com/chu1204505056/vue-admin-better)
13 | [](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 |
61 |
62 | |
63 |
64 |
65 | |
66 |
67 |
68 | |
69 |
70 |
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 | 
192 |
193 | ## 🗃️ 效果图
194 |
195 | 以下是截取的是 pro 版的效果图展示:
196 |
197 |
198 |
199 |
200 |
201 | |
202 |
203 |
204 | |
205 |
206 |
207 |
208 |
209 | |
210 |
211 |
212 | |
213 |
214 |
215 |
216 |
217 | |
218 |
219 |
220 | |
221 |
222 |
223 |
224 | ## 📄 商用注意事项
225 |
226 | 此项目可免费用于商业用途,请遵守 MIT 协议并保留作者技术支持声明。
227 |
228 |
229 |
--------------------------------------------------------------------------------
/src/layouts/components/VabThemeBar/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
18 |
19 |
26 |
27 |
28 |
29 |
30 |
31 | 默认
32 | 绿荫草场
33 | 荣耀典藏
34 |
36 |
37 |
38 |
39 |
40 | 纵向布局
41 | 横向布局
42 |
43 |
44 |
45 |
46 | 固定头部
47 | 不固定头部
48 |
49 |
50 |
51 |
52 | 开启
53 | 不开启
54 |
55 |
56 |
57 |
58 | 保存
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
184 |
185 |
247 |
262 |
--------------------------------------------------------------------------------