├── .browserslistrc ├── babel.config.js ├── public ├── favicon.ico ├── static │ └── css │ │ └── loading.css └── index.html ├── src ├── assets │ ├── logo.png │ ├── error_images │ │ ├── 403.png │ │ ├── 404.png │ │ └── cloud.png │ └── login_images │ │ ├── login_form.png │ │ └── login_background.png ├── vab │ ├── index.js │ ├── styles │ │ ├── vab.less │ │ └── normalize.less │ └── plugins │ │ └── permissions.js ├── views │ ├── index │ │ └── index.vue │ ├── test │ │ └── index.vue │ ├── vab │ │ ├── table │ │ │ └── index.vue │ │ └── icon │ │ │ └── index.vue │ ├── sys │ │ ├── classify │ │ │ └── index.vue │ │ ├── business │ │ │ └── index.vue │ │ ├── word │ │ │ └── index.vue │ │ ├── ad │ │ │ └── index.vue │ │ ├── advice │ │ │ └── index.vue │ │ ├── user │ │ │ └── index.vue │ │ ├── customer │ │ │ └── index.vue │ │ └── report │ │ │ └── index.vue │ ├── 404.vue │ ├── 403.vue │ └── login │ │ └── index.vue ├── config │ ├── config.js │ ├── default │ │ ├── index.js │ │ ├── net.config.js │ │ ├── theme.config.js │ │ └── setting.config.js │ └── index.js ├── api │ ├── icon.js │ ├── router.js │ ├── business.js │ ├── classify.js │ ├── word.js │ ├── ad.js │ ├── report.js │ ├── advice.js │ ├── userlist.js │ └── user.js ├── App.vue ├── utils │ ├── pageTitle.js │ ├── clipboard.js │ ├── static.js │ ├── hasRole.js │ ├── accessToken.js │ ├── routes.js │ ├── request.js │ ├── index.js │ └── validate.js ├── layout │ ├── vab-icon │ │ └── index.vue │ ├── vab-menu │ │ ├── components │ │ │ ├── Submenu.vue │ │ │ └── MenuItem.vue │ │ └── index.vue │ ├── vab-logo │ │ └── index.vue │ ├── vab-content │ │ └── index.vue │ ├── vab-avatar │ │ └── index.vue │ ├── index.vue │ └── vab-tabs │ │ └── index.vue ├── store │ ├── index.js │ └── modules │ │ ├── acl.js │ │ ├── routes.js │ │ ├── user.js │ │ ├── tagsBar.js │ │ └── settings.js ├── main.js ├── components │ ├── broadcast.vue │ └── Detail.vue └── router │ └── index.js ├── .stylelintrc.js ├── README.md ├── .gitattributes ├── .gitignore ├── mock ├── index.js ├── controller │ ├── router.js │ ├── table.js │ └── user.js ├── utils │ └── index.js └── mockServer.js ├── .eslintrc.js ├── prettier.config.js ├── deploy.sh ├── package.json └── vue.config.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@vue/cli-plugin-babel/preset'], 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shengbid/genuine-vue3/master/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shengbid/genuine-vue3/master/src/assets/logo.png -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['stylelint-config-recess-order', 'stylelint-config-prettier'], 3 | } 4 | -------------------------------------------------------------------------------- /src/assets/error_images/403.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shengbid/genuine-vue3/master/src/assets/error_images/403.png -------------------------------------------------------------------------------- /src/assets/error_images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shengbid/genuine-vue3/master/src/assets/error_images/404.png -------------------------------------------------------------------------------- /src/assets/error_images/cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shengbid/genuine-vue3/master/src/assets/error_images/cloud.png -------------------------------------------------------------------------------- /src/assets/login_images/login_form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shengbid/genuine-vue3/master/src/assets/login_images/login_form.png -------------------------------------------------------------------------------- /src/assets/login_images/login_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shengbid/genuine-vue3/master/src/assets/login_images/login_background.png -------------------------------------------------------------------------------- /src/vab/index.js: -------------------------------------------------------------------------------- 1 | // 加载插件 2 | const requirePlugin = require.context('./plugins', true, /\.js$/) 3 | requirePlugin.keys().forEach((fileName) => { 4 | requirePlugin(fileName) 5 | }) 6 | -------------------------------------------------------------------------------- /src/views/index/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/config/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 导出自定义配置 3 | **/ 4 | const config = { 5 | layout: 'vertical', 6 | donation: false, 7 | templateFolder: 'project', 8 | } 9 | module.exports = config 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 🌱商汇后台管理项目 2 | 3 | ```bash 4 | # 克隆项目 5 | git clone https://gitee.com/shengbide/genuine-admin-vue.git 6 | # 进入项目目录 7 | cd genuine-admin-vue 8 | # 安装依赖 9 | yarn 10 | # 本地开发 启动项目 11 | npm run serve -------------------------------------------------------------------------------- /src/api/icon.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function getIconList(params) { 4 | return request({ 5 | url: '/icon/getList', 6 | method: 'get', 7 | params, 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /src/api/router.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function getRouterList(params) { 4 | return request({ 5 | url: '/menu/navigate', 6 | method: 'get', 7 | params, 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /src/views/test/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.html text eol=lf 2 | *.css text eol=lf 3 | *.js text eol=lf linguist-language=vue 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/config/default/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 导出默认配置(通用配置|主题配置|网络配置) 3 | **/ 4 | const setting = require('./setting.config') 5 | const theme = require('./theme.config') 6 | const network = require('./net.config') 7 | 8 | module.exports = { setting, theme, network } 9 | -------------------------------------------------------------------------------- /src/api/business.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | import { handlePage } from '@/utils' 3 | 4 | export function getList(params) { 5 | return request({ 6 | url: '/gsh/listByTRequirement', 7 | method: 'get', 8 | params: handlePage(params), 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /src/api/classify.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | import { handlePage } from '@/utils' 3 | 4 | export function getList(params) { 5 | return request({ 6 | url: '/gsh/listByTIndustry', 7 | method: 'get', 8 | params: handlePage(params), 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /src/api/word.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | import { handlePage } from '@/utils' 3 | 4 | export function getList(params) { 5 | return request({ 6 | url: '/gsh/listByTAdvertisementDynamic', 7 | method: 'get', 8 | params: handlePage(params), 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /src/config/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 3个子配置,通用配置|主题配置|网络配置,建议在当前目录下修改config.js修改配置,会覆盖默认配置,也可以直接修改默认配置 3 | */ 4 | //默认配置 5 | const { setting, theme, network } = require('./default') 6 | //自定义配置 7 | const config = require('./config') 8 | //导出配置(以自定义配置为主) 9 | module.exports = Object.assign({}, setting, theme, network, config) 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | package-lock.json 5 | yarn.lock 6 | *.zip 7 | 8 | # local env files 9 | .env.local 10 | .env.*.local 11 | 12 | # Log files 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | pnpm-debug.log* 17 | 18 | # Editor directories and files 19 | .idea 20 | .vscode 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/api/ad.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | import { handlePage } from '@/utils' 3 | 4 | export function getList(params) { 5 | return request({ 6 | url: '/gsh/listByTAdvertisement', 7 | method: 'get', 8 | params: handlePage(params), 9 | }) 10 | } 11 | 12 | export async function deleteAd(id) { 13 | return request(`/gsh/setReportStatus/${id}`, { 14 | method: 'put', 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /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/api/report.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | import { handlePage } from '@/utils' 3 | 4 | export function getList(params) { 5 | return request({ 6 | url: '/gsh/listByTReport', 7 | method: 'get', 8 | params: handlePage(params), 9 | }) 10 | } 11 | 12 | export async function setSugestionStatus(id) { 13 | return request(`/gsh/setReportStatus/${id}`, { 14 | method: 'put', 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: ['plugin:vue/vue3-essential', 'eslint:recommended', '@vue/prettier'], 7 | parserOptions: { 8 | parser: 'babel-eslint', 9 | }, 10 | rules: { 11 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 12 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /src/api/advice.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | import { handlePage } from '@/utils' 3 | 4 | export function getList(params) { 5 | return request({ 6 | url: '/gsh/listByTSugestion', 7 | method: 'get', 8 | params: handlePage(params), 9 | }) 10 | } 11 | 12 | export async function setSugestionStatus(id) { 13 | return request(`/gsh/setSugestionStatus/${id}`, { 14 | method: 'put', 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /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/config/default/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: 10000, 11 | //操作正常code,支持String、Array、int多种类型 12 | successCode: [200, 0], 13 | } 14 | module.exports = network 15 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 20 | 23 | -------------------------------------------------------------------------------- /src/utils/pageTitle.js: -------------------------------------------------------------------------------- 1 | import { title, titleReverse, titleSeparator } from '@/config' 2 | 3 | /** 4 | * @author chuzhixin 1204505056@qq.com 5 | * @description 设置标题 6 | * @param pageTitle 7 | * @returns {string} 8 | */ 9 | export default function getPageTitle(pageTitle) { 10 | let newTitles = [] 11 | if (pageTitle) newTitles.push(pageTitle) 12 | if (title) newTitles.push(title) 13 | if (titleReverse) newTitles = newTitles.reverse() 14 | return newTitles.join(titleSeparator) 15 | } 16 | -------------------------------------------------------------------------------- /src/layout/vab-icon/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 18 | 19 | 25 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author chuzhixin 1204505056@qq.com 3 | * @description 导入所有 vuex 模块,自动加入namespaced:true,用于解决vuex命名冲突,请勿修改。 4 | */ 5 | import { createStore } from 'vuex' 6 | 7 | const files = require.context('./modules', false, /\.js$/) 8 | const modules = {} 9 | files.keys().forEach((key) => { 10 | modules[key.replace(/(\.\/|\.js)/g, '')] = files(key).default 11 | }) 12 | Object.keys(modules).forEach((key) => { 13 | modules[key]['namespaced'] = true 14 | }) 15 | export default createStore({ 16 | modules, 17 | }) 18 | -------------------------------------------------------------------------------- /src/vab/styles/vab.less: -------------------------------------------------------------------------------- 1 | @import "./normalize.less"; 2 | 3 | html { 4 | body { 5 | 6 | * { 7 | box-sizing: border-box; 8 | } 9 | 10 | /* ant-input-search搜索框 */ 11 | .ant-input-search { 12 | max-width: 250px; 13 | } 14 | 15 | /* ant-pagination分页 */ 16 | .ant-pagination { 17 | margin-top: @vab-margin; 18 | text-align: center; 19 | 20 | &.ant-table-pagination { 21 | float: none !important; 22 | margin-top: @vab-margin; 23 | } 24 | 25 | } 26 | 27 | 28 | } 29 | } -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import Antd from 'ant-design-vue' 3 | import App from './App' 4 | import router from './router' 5 | import store from './store' 6 | import 'ant-design-vue/dist/antd.css' 7 | import '@/vab' 8 | /** 9 | * @author chuzhixin 1204505056@qq.com 10 | * @description 正式环境默认使用mock,正式项目记得注释后再打包 11 | */ 12 | // if (process.env.NODE_ENV === 'production') { 13 | // const { mockXHR } = require('@/utils/static') 14 | // mockXHR() 15 | // } 16 | 17 | createApp(App).use(store).use(router).use(Antd).mount('#app') 18 | -------------------------------------------------------------------------------- /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-beautiful-mini.git" master:gh-pages 10 | git push -f "https://${access_token}@gitee.com/chu1204505056/vue-admin-beautiful-antdv.git" master:gh-pages 11 | start "https://gitee.com/chu1204505056/vue-admin-beautiful-mini/pages" 12 | start "https://gitee.com/chu1204505056/vue-admin-beautiful-antdv/pages" 13 | cd - 14 | exec /bin/bash 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/config/default/theme.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 导出默认主题配置 3 | */ 4 | const theme = { 5 | //布局种类 horizontal vertical gallery comprehensive common 6 | layout: 'horizontal', 7 | //主题名称 default ocean green glory white 8 | themeName: 'default', 9 | //是否固定头部 10 | fixedHeader: true, 11 | //是否显示顶部进度条 12 | showProgressBar: true, 13 | //是否显示多标签页 14 | showTabsBar: true, 15 | //是否显示语言选择组件 16 | showLanguage: true, 17 | //是否显示刷新组件 18 | showRefresh: true, 19 | //是否显示搜索组件 20 | showSearch: true, 21 | //是否显示主题组件 22 | showTheme: true, 23 | //是否显示通知组件 24 | showNotice: true, 25 | //是否显示全屏组件 26 | showFullScreen: true, 27 | } 28 | module.exports = theme 29 | -------------------------------------------------------------------------------- /src/utils/clipboard.js: -------------------------------------------------------------------------------- 1 | import Clipboard from 'clipboard' 2 | import { message } from 'ant-design-vue' 3 | 4 | function clipboardSuccess(text) { 5 | message.success(`复制${text}成功`) 6 | } 7 | 8 | function clipboardError(text) { 9 | message.error(`复制${text}失败`) 10 | } 11 | 12 | /** 13 | * @description 复制数据 14 | * @param text 15 | * @param event 16 | */ 17 | export default function handleClipboard(text, event) { 18 | const clipboard = new Clipboard(event.target, { 19 | text: () => text, 20 | }) 21 | clipboard.on('success', () => { 22 | clipboardSuccess(text) 23 | clipboard.destroy() 24 | }) 25 | clipboard.on('error', () => { 26 | clipboardError(text) 27 | clipboard.destroy() 28 | }) 29 | clipboard.onClick(event) 30 | } 31 | -------------------------------------------------------------------------------- /src/api/userlist.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | import { handlePage } from '@/utils' 3 | 4 | export function getList(params, type) { 5 | return request({ 6 | url: '/gsh/listByTGshUser', 7 | method: 'get', 8 | params: { 9 | ...handlePage(params), 10 | type, 11 | }, 12 | }) 13 | } 14 | 15 | export function toFreeze(id, type) { 16 | return request({ 17 | url: `/gsh/freeze/${id}/${type}`, 18 | method: 'delete', 19 | }) 20 | } 21 | 22 | export function toBatchMassage(data) { 23 | return request({ 24 | url: '/gsh/batchMassage', 25 | method: 'post', 26 | data, 27 | }) 28 | } 29 | 30 | // 获取用户详情 31 | export async function getUserInfo(id) { 32 | return request(`/gsh/gshUserDetail/${id}`) 33 | } 34 | -------------------------------------------------------------------------------- /mock/controller/router.js: -------------------------------------------------------------------------------- 1 | const data = [ 2 | { 3 | path: '/', 4 | component: 'Layout', 5 | redirect: '/index', 6 | meta: { 7 | title: '首页', 8 | icon: 'home-4-line', 9 | affix: true, 10 | }, 11 | children: [ 12 | { 13 | path: 'index', 14 | name: 'Index', 15 | component: '@/views/index', 16 | meta: { 17 | title: '首页', 18 | icon: 'home-4-line', 19 | affix: true, 20 | }, 21 | }, 22 | ], 23 | }, 24 | ] 25 | module.exports = [ 26 | { 27 | url: '/menu/navigate', 28 | type: 'get', 29 | response() { 30 | return { 31 | code: 200, 32 | msg: 'success', 33 | data, 34 | } 35 | }, 36 | }, 37 | ] 38 | -------------------------------------------------------------------------------- /src/layout/vab-menu/components/Submenu.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 34 | -------------------------------------------------------------------------------- /src/store/modules/acl.js: -------------------------------------------------------------------------------- 1 | const state = () => ({ 2 | admin: false, 3 | role: [], 4 | ability: [], 5 | }) 6 | const getters = { 7 | admin: (state) => state.admin, 8 | role: (state) => state.role, 9 | ability: (state) => state.ability, 10 | } 11 | const mutations = { 12 | setFull(state, admin) { 13 | state.admin = admin 14 | }, 15 | setRole(state, role) { 16 | state.role = role 17 | }, 18 | setAbility(state, ability) { 19 | state.ability = ability 20 | }, 21 | } 22 | const actions = { 23 | setFull({ commit }, admin) { 24 | commit('setFull', admin) 25 | }, 26 | setRole({ commit }, role) { 27 | commit('setRole', role) 28 | }, 29 | setAbility({ commit }, ability) { 30 | commit('setAbility', ability) 31 | }, 32 | } 33 | export default { state, getters, mutations, actions } 34 | -------------------------------------------------------------------------------- /src/layout/vab-logo/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 26 | 46 | -------------------------------------------------------------------------------- /src/api/user.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | // import { tokenName } from '@/config' 3 | 4 | // 登陆 5 | export async function login(data) { 6 | return request({ 7 | url: '/gsh/login', 8 | method: 'post', 9 | data, 10 | }) 11 | } 12 | 13 | // 注册 14 | export async function register(data) { 15 | return request({ 16 | url: '/gsh/saveTGshUser', 17 | method: 'post', 18 | data, 19 | }) 20 | } 21 | // 获取登陆用户信息 22 | export function getUserInfo(accessToken) { 23 | //此处为了兼容mock.js使用data传递accessToken,如果使用mock可以走headers 24 | // return request({ 25 | // url: '/userInfo', 26 | // method: 'post', 27 | // data: { 28 | // [tokenName]: accessToken, 29 | // }, 30 | // }) 31 | return new Promise((reslove) => { 32 | reslove({ 33 | data: { 34 | username: localStorage.getItem('username') || '管理员', 35 | avatar: '', 36 | }, 37 | }) 38 | }) 39 | } 40 | 41 | // 退出登录 42 | export function logout(data) { 43 | return request({ 44 | url: '/gsh/logOut', 45 | method: 'post', 46 | data, 47 | }) 48 | } 49 | // 获取验证码 50 | export async function getFakeCaptcha(params) { 51 | return request({ 52 | url: '/email/sendSimpleEmail', 53 | params, 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /src/vab/plugins/permissions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author chuzhixin 1204505056@qq.com 3 | * @description 路由守卫,目前两种模式:all模式与intelligence模式 4 | */ 5 | import router from '@/router' 6 | import store from '@/store' 7 | import getPageTitle from '@/utils/pageTitle' 8 | import { loginInterception, recordRoute, routesWhiteList } from '@/config' 9 | 10 | router.beforeEach(async (to, from, next) => { 11 | let hasToken = store.getters['user/accessToken'] 12 | 13 | if (!loginInterception) hasToken = true 14 | 15 | if (hasToken) { 16 | if (to.path === '/login') { 17 | next({ path: '/' }) 18 | } else { 19 | const hasPermission = store.getters['user/username'] 20 | if (hasPermission) { 21 | next() 22 | } else { 23 | try { 24 | await store.dispatch('user/getUserInfo') 25 | next() 26 | } catch (error) { 27 | await store.dispatch('user/resetAll') 28 | } 29 | } 30 | } 31 | } else { 32 | if (routesWhiteList.indexOf(to.path) !== -1) { 33 | next() 34 | } else { 35 | if (recordRoute) 36 | next({ path: '/login', query: { redirect: to.path }, replace: true }) 37 | else next({ path: '/login', replace: true }) 38 | } 39 | } 40 | }) 41 | router.afterEach((to) => { 42 | document.title = getPageTitle(to.meta.title) 43 | }) 44 | -------------------------------------------------------------------------------- /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 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 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/layout/vab-menu/components/MenuItem.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 43 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/layout/vab-content/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 26 | 27 | 59 | -------------------------------------------------------------------------------- /src/utils/hasRole.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | 3 | export function hasRole(value) { 4 | if (store.getters['acl/admin']) return true 5 | if (value instanceof Array && value.length > 0) 6 | return can(store.getters['acl/role'], { 7 | role: value, 8 | mode: 'oneOf', 9 | }) 10 | let mode = 'oneOf' 11 | if (Object.prototype.hasOwnProperty.call(value, 'mode')) mode = value['mode'] 12 | let result = true 13 | if (Object.prototype.hasOwnProperty.call(value, 'role')) 14 | result = 15 | result && can(store.getters['acl/role'], { role: value['role'], mode }) 16 | if (result && Object.prototype.hasOwnProperty.call(value, 'ability')) 17 | result = 18 | result && 19 | can(store.getters['acl/ability'], { 20 | role: value['ability'], 21 | mode, 22 | }) 23 | return result 24 | } 25 | 26 | export function can(roleOrAbility, value) { 27 | let hasRole = false 28 | if ( 29 | value instanceof Object && 30 | Object.prototype.hasOwnProperty.call(value, 'role') && 31 | Object.prototype.hasOwnProperty.call(value, 'mode') 32 | ) { 33 | const { role, mode } = value 34 | if (mode === 'allOf') { 35 | hasRole = role.every((item) => { 36 | return roleOrAbility.includes(item) 37 | }) 38 | } 39 | if (mode === 'oneOf') { 40 | hasRole = role.some((item) => { 41 | return roleOrAbility.includes(item) 42 | }) 43 | } 44 | if (mode === 'except') { 45 | hasRole = !role.some((item) => { 46 | return roleOrAbility.includes(item) 47 | }) 48 | } 49 | } 50 | return hasRole 51 | } 52 | -------------------------------------------------------------------------------- /src/layout/vab-menu/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 56 | 61 | -------------------------------------------------------------------------------- /mock/controller/table.js: -------------------------------------------------------------------------------- 1 | const { mock } = require('mockjs') 2 | const { handleRandomImage } = require('../utils') 3 | const List = [] 4 | const count = 50 5 | for (let i = 0; i < count; i++) { 6 | List.push( 7 | mock({ 8 | uuid: '@uuid', 9 | id: '@id', 10 | title: '@title(1, 2)', 11 | description: '@csentence', 12 | 'status|1': ['published', 'draft', 'deleted'], 13 | author: '@cname', 14 | datetime: '@datetime', 15 | pageViews: '@integer(300, 5000)', 16 | img: handleRandomImage(228, 228), 17 | switch: '@boolean', 18 | percent: '@integer(80,99)', 19 | 'rate|1': [1, 2, 3, 4, 5], 20 | }) 21 | ) 22 | } 23 | 24 | module.exports = [ 25 | { 26 | url: '/table/getList', 27 | type: 'get', 28 | response(config) { 29 | const { title, current = 1, pageSize = 10 } = config.query 30 | let mockList = List.filter((item) => { 31 | return !(title && item.title.indexOf(title) < 0) 32 | }) 33 | const pageList = mockList.filter( 34 | (item, index) => 35 | index < pageSize * current && index >= pageSize * (current - 1) 36 | ) 37 | return { 38 | code: 200, 39 | msg: 'success', 40 | total: mockList.length, 41 | data: pageList, 42 | } 43 | }, 44 | }, 45 | { 46 | url: '/table/doEdit', 47 | type: 'post', 48 | response() { 49 | return { 50 | code: 200, 51 | msg: '模拟保存成功', 52 | } 53 | }, 54 | }, 55 | { 56 | url: '/table/doDelete', 57 | type: 'post', 58 | response() { 59 | return { 60 | code: 200, 61 | msg: '模拟删除成功', 62 | } 63 | }, 64 | }, 65 | ] 66 | -------------------------------------------------------------------------------- /src/layout/vab-avatar/index.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 55 | 64 | -------------------------------------------------------------------------------- /src/components/broadcast.vue: -------------------------------------------------------------------------------- 1 | 26 | 66 | -------------------------------------------------------------------------------- /src/views/vab/table/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 72 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "genuine-admin-antdv", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint", 9 | "clear": "rimraf node_modules&&npm install --registry=https://registry.npm.taobao.org", 10 | "use:npm": "nrm use npm", 11 | "use:taobao": "nrm use taobao", 12 | "update": "ncu -u --target greatest&&npm install --registry=https://registry.npm.taobao.org", 13 | "deploy": "start ./deploy.sh" 14 | }, 15 | "dependencies": { 16 | "ant-design-vue": "3.2.10", 17 | "axios": "^0.21.1", 18 | "clipboard": "^2.0.8", 19 | "core-js": "^3.15.2", 20 | "dayjs": "^1.10.6", 21 | "js-cookie": "^3.0.0-rc.3", 22 | "mockjs": "^1.1.0", 23 | "remixicon": "^2.5.0", 24 | "vue": "^3.1.4", 25 | "vue-router": "^4.0.10", 26 | "vuex": "^4.0.2" 27 | }, 28 | "devDependencies": { 29 | "@vue/cli-plugin-babel": "^4.5.9", 30 | "@vue/cli-plugin-eslint": "^4.5.9", 31 | "@vue/cli-service": "^4.5.9", 32 | "@vue/compiler-sfc": "^3.1.4", 33 | "@vue/eslint-config-prettier": "^6.0.0", 34 | "babel-eslint": "^11.0.0-beta.2", 35 | "body-parser": "^1.19.0", 36 | "chalk": "^4.1.1", 37 | "chokidar": "^3.5.2", 38 | "eslint": "^7.30.0", 39 | "eslint-plugin-prettier": "^3.4.0", 40 | "eslint-plugin-vue": "^7.13.0", 41 | "filemanager-webpack-plugin": "^6.1.4", 42 | "image-webpack-loader": "^7.0.1", 43 | "less": "^4.1.1", 44 | "less-loader": "^7.3.0", 45 | "prettier": "^2.3.2", 46 | "stylelint": "^13.13.1", 47 | "stylelint-config-prettier": "^8.0.2", 48 | "stylelint-config-recess-order": "^2.4.0", 49 | "svg-sprite-loader": "^6.0.9", 50 | "vab-config": "0.0.8", 51 | "webpackbar": "^5.0.0-3" 52 | }, 53 | "lint-staged": { 54 | "*.{js,jsx,vue}": [ 55 | "vue-cli-service lint", 56 | "git add" 57 | ] 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /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: 24px; 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 | -------------------------------------------------------------------------------- /src/views/sys/classify/index.vue: -------------------------------------------------------------------------------- 1 | 21 | 81 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 13 | 17 | 18 | 19 | 28 | 29 | 30 | 36 |
37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |

<%= VUE_APP_TITLE %>

47 |
48 |
49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/store/modules/routes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author chuzhixin 1204505056@qq.com 3 | * @description 路由拦截状态管理,目前两种模式:all模式与intelligence模式,其中partialRoutes是菜单暂未使用 4 | */ 5 | import { asyncRoutes, constantRoutes } from '@/router' 6 | import { getRouterList } from '@/api/router' 7 | import { convertRouter, filterRoutes } from '@/utils/routes' 8 | 9 | const state = () => ({ 10 | routes: filterRoutes([...constantRoutes, ...asyncRoutes]), 11 | partialRoutes: [], 12 | }) 13 | const getters = { 14 | routes: (state) => state.routes, 15 | partialRoutes: (state) => state.partialRoutes, 16 | } 17 | const mutations = { 18 | setRoutes(state, routes) { 19 | state.routes = routes 20 | }, 21 | setPartialRoutes(state, routes) { 22 | state.partialRoutes = routes 23 | }, 24 | } 25 | const actions = { 26 | /** 27 | * @author chuzhixin 1204505056@qq.com 28 | * @description intelligence模式设置路由 29 | * @param {*} { commit } 30 | * @returns 31 | */ 32 | async setRoutes({ commit }) { 33 | const finallyRoutes = filterRoutes([...constantRoutes, ...asyncRoutes]) 34 | commit('setRoutes', finallyRoutes) 35 | return [...asyncRoutes] 36 | }, 37 | /** 38 | * @author chuzhixin 1204505056@qq.com 39 | * @description all模式设置路由 40 | * @param {*} { commit } 41 | * @returns 42 | */ 43 | async setAllRoutes({ commit }) { 44 | let { data } = await getRouterList() 45 | if (data[data.length - 1].path !== '*') 46 | data.push({ path: '*', redirect: '/404', hidden: true }) 47 | const asyncRoutes = convertRouter(data) 48 | const finallyRoutes = filterRoutes([...constantRoutes, ...asyncRoutes]) 49 | commit('setRoutes', finallyRoutes) 50 | return [...asyncRoutes] 51 | }, 52 | /** 53 | * @author chuzhixin 1204505056@qq.com 54 | * @description 画廊布局、综合布局设置路由 55 | * @param {*} { commit } 56 | * @param accessedRoutes 画廊布局、综合布局设置路由 57 | */ 58 | setPartialRoutes({ commit }, accessedRoutes) { 59 | commit('setPartialRoutes', accessedRoutes) 60 | }, 61 | } 62 | export default { state, getters, mutations, actions } 63 | -------------------------------------------------------------------------------- /src/views/sys/business/index.vue: -------------------------------------------------------------------------------- 1 | 21 | 89 | -------------------------------------------------------------------------------- /src/views/sys/word/index.vue: -------------------------------------------------------------------------------- 1 | 24 | 86 | -------------------------------------------------------------------------------- /src/utils/accessToken.js: -------------------------------------------------------------------------------- 1 | import { storage, tokenTableName } from '@/config' 2 | import cookie from 'js-cookie' 3 | 4 | /** 5 | * @author chuzhixin 1204505056@qq.com 6 | * @description 获取accessToken 7 | * @returns {string|ActiveX.IXMLDOMNode|Promise|any|IDBRequest|MediaKeyStatus|FormDataEntryValue|Function|Promise} 8 | */ 9 | export function getAccessToken() { 10 | if (storage) { 11 | if ('localStorage' === storage) { 12 | return localStorage.getItem(tokenTableName) 13 | } else if ('sessionStorage' === storage) { 14 | return sessionStorage.getItem(tokenTableName) 15 | } else if ('cookie' === storage) { 16 | return cookie.get(tokenTableName) 17 | } else { 18 | return localStorage.getItem(tokenTableName) 19 | } 20 | } else { 21 | return localStorage.getItem(tokenTableName) 22 | } 23 | } 24 | 25 | /** 26 | * @author chuzhixin 1204505056@qq.com 27 | * @description 存储accessToken 28 | * @param accessToken 29 | * @returns {void|*} 30 | */ 31 | export function setAccessToken(accessToken) { 32 | if (storage) { 33 | if ('localStorage' === storage) { 34 | return localStorage.setItem(tokenTableName, accessToken) 35 | } else if ('sessionStorage' === storage) { 36 | return sessionStorage.setItem(tokenTableName, accessToken) 37 | } else if ('cookie' === storage) { 38 | return cookie.set(tokenTableName, accessToken) 39 | } else { 40 | return localStorage.setItem(tokenTableName, accessToken) 41 | } 42 | } else { 43 | return localStorage.setItem(tokenTableName, accessToken) 44 | } 45 | } 46 | 47 | /** 48 | * @author chuzhixin 1204505056@qq.com 49 | * @description 移除accessToken 50 | * @returns {void|Promise} 51 | */ 52 | export function removeAccessToken() { 53 | if (storage) { 54 | if ('localStorage' === storage) { 55 | return localStorage.removeItem(tokenTableName) 56 | } else if ('sessionStorage' === storage) { 57 | return sessionStorage.clear() 58 | } else if ('cookie' === storage) { 59 | return cookie.remove(tokenTableName) 60 | } else { 61 | return localStorage.removeItem(tokenTableName) 62 | } 63 | } else { 64 | return localStorage.removeItem(tokenTableName) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/config/default/setting.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 导出默认通用配置 3 | */ 4 | const setting = { 5 | //开发以及部署时的URL,hash模式时在不确定二级目录名称的情况下建议使用""代表相对路径或者"/二级目录/",history模式默认使用"/"或者"/二级目录/" 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 | '/api', 18 | //标题 (包括初次加载雪花屏的标题 页面的标题 浏览器的标题) 19 | title: 'geninue-admin-vue', 20 | //标题分隔符 21 | titleSeparator: ' - ', 22 | //标题是否反转 如果为false:"page - title",如果为ture:"title - page" 23 | titleReverse: false, 24 | //简写 25 | abbreviation: 'vab-pro', 26 | //开发环境端口号 27 | devPort: '9999', 28 | //版本号 29 | version: process.env.VUE_APP_VERSION, 30 | //pro版本copyright可随意修改 31 | copyright: 'chuzhixin 1204505056@qq.com', 32 | //缓存路由的最大数量 33 | keepAliveMaxNum: 99, 34 | //路由模式,可选值为 history 或 hash 35 | routerMode: 'hash', 36 | //不经过token校验的路由 37 | routesWhiteList: ['/login', '/register', '/callback', '/404', '/403'], 38 | //加载时显示文字 39 | loadingText: '正在加载中...', 40 | //token名称 41 | tokenName: 'token', 42 | //token在localStorage、sessionStorage、cookie存储的key的名称 43 | tokenTableName: 'token', 44 | //token存储位置localStorage sessionStorage cookie 45 | storage: 'localStorage', 46 | //token失效回退到登录页时是否记录本次的路由 47 | recordRoute: true, 48 | //是否显示logo,不显示时设置false,显示时请填写remixIcon图标名称,暂时只支持设置remixIcon 49 | logo: 'vuejs-fill', 50 | //语言类型zh、en 51 | i18n: 'zh', 52 | //在哪些环境下显示高亮错误 53 | errorLog: ['development', 'production'], 54 | //是否开启登录拦截 55 | loginInterception: true, 56 | //是否开启登录RSA加密 57 | loginRSA: false, 58 | //intelligence(前端导出路由)和all(后端导出路由)两种方式 59 | authentication: 'intelligence', 60 | //是否开启roles字段进行角色权限控制(如果是all模式后端完全处理角色并进行json组装,可设置false不处理路由中的roles字段) 61 | rolesControl: true, 62 | //vertical gallery comprehensive common布局时是否只保持一个子菜单的展开 63 | uniqueOpened: false, 64 | //vertical布局时默认展开的菜单path,使用逗号隔开建议只展开一个 65 | defaultOpeneds: ['/vab'], 66 | //需要加loading层的请求,防止重复提交 67 | debounce: ['doEdit'], 68 | //需要自动注入并加载的模块 69 | providePlugin: {}, 70 | //npm run build时是否自动生成7z压缩包 71 | build7z: false, 72 | //代码生成机生成在view下的文件夹名称 73 | templateFolder: 'project', 74 | //是否显示终端donation打印 75 | donation: false, 76 | //画廊布局和综合布局时,是否点击一级菜单默认开启第一个二级菜单 77 | openFirstMenu: true, 78 | } 79 | module.exports = setting 80 | -------------------------------------------------------------------------------- /src/utils/routes.js: -------------------------------------------------------------------------------- 1 | import router from '@/router' 2 | import path from 'path' 3 | import { rolesControl } from '@/config' 4 | import { isExternal } from '@/utils/validate' 5 | import { hasRole } from '@/utils/hasRole' 6 | 7 | /** 8 | * @author chuzhixin 1204505056@qq.com 9 | * @description all模式渲染后端返回路由 10 | * @param constantRoutes 11 | * @returns {*} 12 | */ 13 | export function convertRouter(constantRoutes) { 14 | return constantRoutes.map((route) => { 15 | if (route.component) { 16 | if (route.component === 'Layout') { 17 | const path = 'layouts' 18 | route.component = (resolve) => require([`@/${path}`], resolve) 19 | } else { 20 | let path = 'views/' + route.component 21 | if ( 22 | new RegExp('^/views/.*$').test(route.component) || 23 | new RegExp('^views/.*$').test(route.component) 24 | ) { 25 | path = route.component 26 | } else if (new RegExp('^/.*$').test(route.component)) { 27 | path = 'views' + route.component 28 | } else if (new RegExp('^@views/.*$').test(route.component)) { 29 | path = route.component.slice(1) 30 | } else { 31 | path = 'views/' + route.component 32 | } 33 | route.component = (resolve) => require([`@/${path}`], resolve) 34 | } 35 | } 36 | if (route.children && route.children.length) 37 | route.children = convertRouter(route.children) 38 | 39 | if (route.children && route.children.length === 0) delete route.children 40 | 41 | return route 42 | }) 43 | } 44 | 45 | /** 46 | * @author chuzhixin 1204505056@qq.com 47 | * @description 根据roles数组拦截路由 48 | * @param routes 49 | * @param baseUrl 50 | * @returns {[]} 51 | */ 52 | export function filterRoutes(routes, baseUrl = '/') { 53 | return routes 54 | .filter((route) => { 55 | if (route.meta && route.meta.roles) 56 | return !rolesControl || hasRole(route.meta.roles) 57 | else return true 58 | }) 59 | .map((route) => { 60 | if (route.path !== '*' && !isExternal(route.path)) 61 | route.path = path.resolve(baseUrl, route.path) 62 | route.fullPath = route.path 63 | if (route.children) 64 | route.children = filterRoutes(route.children, route.fullPath) 65 | return route 66 | }) 67 | } 68 | 69 | /** 70 | * 根据当前页面firstMenu 71 | * @returns {string} 72 | */ 73 | export function handleFirstMenu() { 74 | const firstMenu = router.currentRoute.matched[0].path 75 | if (firstMenu === '') return '/' 76 | return firstMenu 77 | } 78 | -------------------------------------------------------------------------------- /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: '/login', 10 | type: 'post', 11 | response(config) { 12 | const { username } = config.body 13 | const accessToken = accessTokens[username] 14 | if (!accessToken) { 15 | return { 16 | code: 500, 17 | msg: '帐户或密码不正确。', 18 | } 19 | } 20 | return { 21 | code: 200, 22 | msg: 'success', 23 | data: { accessToken }, 24 | } 25 | }, 26 | }, 27 | { 28 | url: '/socialLogin', 29 | type: 'post', 30 | response(config) { 31 | const { code } = config.body 32 | if (!code) { 33 | return { 34 | code: 500, 35 | msg: '未成功获取Token。', 36 | } 37 | } 38 | return { 39 | code: 200, 40 | msg: 'success', 41 | data: { accessToken: accessTokens['admin'] }, 42 | } 43 | }, 44 | }, 45 | { 46 | url: '/register', 47 | type: 'post', 48 | response() { 49 | return { 50 | code: 200, 51 | msg: '模拟注册成功', 52 | } 53 | }, 54 | }, 55 | { 56 | url: '/userInfo', 57 | type: 'post', 58 | response(config) { 59 | const { accessToken } = config.body 60 | let roles = ['admin'] 61 | let ability = ['READ'] 62 | let username = 'admin' 63 | if ('admin-accessToken' === accessToken) { 64 | roles = ['admin'] 65 | ability = ['READ', 'WRITE', 'DELETE'] 66 | username = 'admin' 67 | } 68 | if ('editor-accessToken' === accessToken) { 69 | roles = ['editor'] 70 | ability = ['READ', 'WRITE'] 71 | username = 'editor' 72 | } 73 | if ('test-accessToken' === accessToken) { 74 | roles = ['admin', 'editor'] 75 | ability = ['READ'] 76 | username = 'test' 77 | } 78 | return { 79 | code: 200, 80 | msg: 'success', 81 | data: { 82 | roles, 83 | ability, 84 | username, 85 | 'avatar|1': [ 86 | 'https://i.gtimg.cn/club/item/face/img/2/15922_100.gif', 87 | 'https://i.gtimg.cn/club/item/face/img/8/15918_100.gif', 88 | ], 89 | }, 90 | } 91 | }, 92 | }, 93 | { 94 | url: '/logout', 95 | type: 'post', 96 | response() { 97 | return { 98 | code: 200, 99 | msg: 'success', 100 | } 101 | }, 102 | }, 103 | ] 104 | -------------------------------------------------------------------------------- /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 | ignored: /mock-server/, 74 | ignoreInitial: true, 75 | }) 76 | .on('all', (event) => { 77 | if (event === 'change' || event === 'add') { 78 | try { 79 | app._router.stack.splice(mockStartIndex, mockRoutesLength) 80 | 81 | Object.keys(require.cache).forEach((item) => { 82 | if (item.includes(mockDir)) { 83 | delete require.cache[require.resolve(item)] 84 | } 85 | }) 86 | const mockRoutes = registerRoutes(app) 87 | mockRoutesLength = mockRoutes.mockRoutesLength 88 | mockStartIndex = mockRoutes.mockStartIndex 89 | } catch (error) { 90 | console.log(chalk.red(error)) 91 | } 92 | } 93 | }) 94 | } 95 | -------------------------------------------------------------------------------- /src/views/sys/ad/index.vue: -------------------------------------------------------------------------------- 1 | 32 | 109 | -------------------------------------------------------------------------------- /src/components/Detail.vue: -------------------------------------------------------------------------------- 1 | 60 | 110 | -------------------------------------------------------------------------------- /src/views/vab/icon/index.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 93 | 94 | 128 | -------------------------------------------------------------------------------- /src/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { 3 | baseURL, 4 | contentType, 5 | debounce, 6 | requestTimeout, 7 | successCode, 8 | tokenName, 9 | } from '@/config' 10 | import store from '@/store' 11 | import qs from 'qs' 12 | import router from '@/router' 13 | import { isArray } from '@/utils/validate' 14 | import { message } from 'ant-design-vue' 15 | 16 | let loadingInstance 17 | 18 | /** 19 | * @author chuzhixin 1204505056@qq.com 20 | * @description 处理code异常 21 | * @param {*} code 22 | * @param {*} msg 23 | */ 24 | const handleCode = (code, msg) => { 25 | switch (code) { 26 | case 401: 27 | message.error(msg || '登录失效') 28 | store.dispatch('user/resetAll').catch(() => {}) 29 | break 30 | case 403: 31 | router.push({ path: '/401' }).catch(() => {}) 32 | break 33 | default: 34 | message.error(msg || `后端接口${code}异常`) 35 | break 36 | } 37 | } 38 | 39 | /** 40 | * @author chuzhixin 1204505056@qq.com 41 | * @description axios初始化 42 | */ 43 | const instance = axios.create({ 44 | baseURL, 45 | timeout: requestTimeout, 46 | headers: { 47 | 'Content-Type': contentType, 48 | }, 49 | }) 50 | 51 | /** 52 | * @author chuzhixin 1204505056@qq.com 53 | * @description axios请求拦截器 54 | */ 55 | instance.interceptors.request.use( 56 | (config) => { 57 | if (store.getters['user/accessToken']) 58 | config.headers[tokenName] = store.getters['user/accessToken'] 59 | if ( 60 | config.data && 61 | config.headers['Content-Type'] === 62 | 'application/x-www-form-urlencoded;charset=UTF-8' 63 | ) 64 | config.data = qs.stringify(config.data) 65 | if (debounce.some((item) => config.url.includes(item))) { 66 | //这里写加载动画 67 | } 68 | return config 69 | }, 70 | (error) => { 71 | return Promise.reject(error) 72 | } 73 | ) 74 | 75 | /** 76 | * @author chuzhixin 1204505056@qq.com 77 | * @description axios响应拦截器 78 | */ 79 | instance.interceptors.response.use( 80 | (response) => { 81 | if (loadingInstance) loadingInstance.close() 82 | 83 | const { data, config } = response 84 | const { msg } = data 85 | const code = Number(data.code) 86 | // 操作正常Code数组 87 | const codeVerificationArray = isArray(successCode) 88 | ? [...successCode] 89 | : [...[successCode]] 90 | // 是否操作正常 91 | if (codeVerificationArray.includes(code)) { 92 | return data 93 | } else { 94 | handleCode(code, msg) 95 | return Promise.reject( 96 | '请求异常拦截:' + 97 | JSON.stringify({ url: config.url, code, msg }) || 'Error' 98 | ) 99 | } 100 | }, 101 | (error) => { 102 | if (loadingInstance) loadingInstance.close() 103 | const { response, message } = error 104 | if (error.response && error.response.data) { 105 | const { status, data } = response 106 | handleCode(status, data.msg || message) 107 | return Promise.reject(error) 108 | } else { 109 | let { message } = error 110 | if (message === 'Network Error') { 111 | message = '后端接口连接异常' 112 | } 113 | if (message.includes('timeout')) { 114 | message = '后端接口请求超时' 115 | } 116 | if (message.includes('Request failed with status code')) { 117 | const code = message.substr(message.length - 3) 118 | message = '后端接口' + code + '异常' 119 | } 120 | message.error(message || `后端接口未知异常`) 121 | return Promise.reject(error) 122 | } 123 | } 124 | ) 125 | 126 | export default instance 127 | -------------------------------------------------------------------------------- /src/views/sys/advice/index.vue: -------------------------------------------------------------------------------- 1 | 45 | 141 | -------------------------------------------------------------------------------- /src/views/sys/user/index.vue: -------------------------------------------------------------------------------- 1 | 45 | 154 | -------------------------------------------------------------------------------- /src/views/sys/customer/index.vue: -------------------------------------------------------------------------------- 1 | 45 | 154 | -------------------------------------------------------------------------------- /src/views/sys/report/index.vue: -------------------------------------------------------------------------------- 1 | 40 | 157 | -------------------------------------------------------------------------------- /src/store/modules/user.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author chuzhixin 1204505056@qq.com 3 | * @description 登录、获取用户信息、退出登录、清除accessToken逻辑,不建议修改 4 | */ 5 | import { getUserInfo, login, logout } from '@/api/user' 6 | import { 7 | getAccessToken, 8 | removeAccessToken, 9 | setAccessToken, 10 | } from '@/utils/accessToken' 11 | import { title, tokenName } from '@/config' 12 | import { message, notification } from 'ant-design-vue' 13 | 14 | const state = () => ({ 15 | accessToken: getAccessToken(), 16 | username: '', 17 | avatar: '', 18 | }) 19 | const getters = { 20 | accessToken: (state) => state.accessToken, 21 | username: (state) => state.username, 22 | avatar: (state) => state.avatar, 23 | } 24 | const mutations = { 25 | /** 26 | * @author chuzhixin 1204505056@qq.com 27 | * @description 设置accessToken 28 | * @param {*} state 29 | * @param {*} accessToken 30 | */ 31 | setAccessToken(state, accessToken) { 32 | state.accessToken = accessToken 33 | setAccessToken(accessToken) 34 | }, 35 | /** 36 | * @author chuzhixin 1204505056@qq.com 37 | * @description 设置用户名 38 | * @param {*} state 39 | * @param {*} username 40 | */ 41 | setUsername(state, username) { 42 | state.username = username 43 | }, 44 | /** 45 | * @author chuzhixin 1204505056@qq.com 46 | * @description 设置头像 47 | * @param {*} state 48 | * @param {*} avatar 49 | */ 50 | setAvatar(state, avatar) { 51 | state.avatar = avatar 52 | }, 53 | } 54 | const actions = { 55 | /** 56 | * @author chuzhixin 1204505056@qq.com 57 | * @description 登录拦截放行时,设置虚拟角色 58 | * @param {*} { commit, dispatch } 59 | */ 60 | setVirtualRoles({ commit, dispatch }) { 61 | dispatch('acl/setFull', true, { root: true }) 62 | commit('setAvatar', 'https://i.gtimg.cn/club/item/face/img/2/15922_100.gif') 63 | commit('setUsername', 'admin(未开启登录拦截)') 64 | }, 65 | /** 66 | * @author chuzhixin 1204505056@qq.com 67 | * @description 登录 68 | * @param {*} { commit } 69 | * @param {*} userInfo 70 | */ 71 | async login({ commit }, userInfo) { 72 | const { data } = await login(userInfo) 73 | const accessToken = data[tokenName] 74 | localStorage.setItem('username', data.name) 75 | if (accessToken) { 76 | commit('setAccessToken', accessToken) 77 | const hour = new Date().getHours() 78 | const thisTime = 79 | hour < 8 80 | ? '早上好' 81 | : hour <= 11 82 | ? '上午好' 83 | : hour <= 13 84 | ? '中午好' 85 | : hour < 18 86 | ? '下午好' 87 | : '晚上好' 88 | notification.open({ 89 | message: `欢迎登录${title}`, 90 | description: `${thisTime}!`, 91 | }) 92 | } else { 93 | message.error(`登录接口异常,未正确返回${tokenName}...`) 94 | } 95 | }, 96 | /** 97 | * @author chuzhixin 1204505056@qq.com 98 | * @description 获取用户信息接口 这个接口非常非常重要,如果没有明确底层前逻辑禁止修改此方法,错误的修改可能造成整个框架无法正常使用 99 | * @param {*} { commit, dispatch, state } 100 | * @returns 101 | */ 102 | async getUserInfo({ commit, state }) { 103 | const { data } = await getUserInfo(state.accessToken) 104 | if (!data) { 105 | message.error(`验证失败,请重新登录...`) 106 | return false 107 | } 108 | let { username, avatar } = data 109 | if (username) { 110 | commit('setUsername', username) 111 | commit('setAvatar', avatar) 112 | } else { 113 | message.error('用户信息接口异常') 114 | } 115 | }, 116 | 117 | /** 118 | * @author chuzhixin 1204505056@qq.com 119 | * @description 退出登录 120 | * @param {*} { dispatch } 121 | */ 122 | async logout({ dispatch }) { 123 | await logout(state.accessToken) 124 | await dispatch('resetAll') 125 | }, 126 | /** 127 | * @author chuzhixin 1204505056@qq.com 128 | * @description 重置accessToken、roles、ability、router等 129 | * @param {*} { commit, dispatch } 130 | */ 131 | async resetAll({ dispatch }) { 132 | await dispatch('setAccessToken', '') 133 | // await dispatch('acl/setFull', false, { root: true }) 134 | // await dispatch('acl/setRole', [], { root: true }) 135 | // await dispatch('acl/setAbility', [], { root: true }) 136 | removeAccessToken() 137 | }, 138 | /** 139 | * @author chuzhixin 1204505056@qq.com 140 | * @description 设置token 141 | */ 142 | setAccessToken({ commit }, accessToken) { 143 | commit('setAccessToken', accessToken) 144 | }, 145 | } 146 | export default { state, getters, mutations, actions } 147 | -------------------------------------------------------------------------------- /src/store/modules/tagsBar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author chuzhixin 1204505056@qq.com 3 | * @description tagsBar多标签页逻辑,前期借鉴了很多开源项目发现都有个共同的特点很繁琐并不符合框架设计的初衷,后来在github用户cyea的启发下完成了重构,请勿修改 4 | */ 5 | 6 | const state = () => ({ 7 | visitedRoutes: [], 8 | }) 9 | const getters = { 10 | visitedRoutes: (state) => state.visitedRoutes, 11 | } 12 | const mutations = { 13 | /** 14 | * @author chuzhixin 1204505056@qq.com 15 | * @description 添加标签页 16 | * @param {*} state 17 | * @param {*} route 18 | * @returns 19 | */ 20 | addVisitedRoute(state, route) { 21 | let target = state.visitedRoutes.find((item) => item.path === route.path) 22 | if (target) { 23 | if (route.fullPath !== target.fullPath) Object.assign(target, route) 24 | return 25 | } 26 | state.visitedRoutes.push(Object.assign({}, route)) 27 | }, 28 | /** 29 | * @author chuzhixin 1204505056@qq.com 30 | * @description 删除当前标签页 31 | * @param {*} state 32 | * @param {*} route 33 | * @returns 34 | */ 35 | delVisitedRoute(state, route) { 36 | state.visitedRoutes.forEach((item, index) => { 37 | if (item.path === route.path) state.visitedRoutes.splice(index, 1) 38 | }) 39 | }, 40 | /** 41 | * @author chuzhixin 1204505056@qq.com 42 | * @description 删除当前标签页以外其它全部多标签页 43 | * @param {*} state 44 | * @param {*} route 45 | * @returns 46 | */ 47 | delOthersVisitedRoutes(state, route) { 48 | state.visitedRoutes = state.visitedRoutes.filter( 49 | (item) => item.meta.affix || item.path === route.path 50 | ) 51 | }, 52 | /** 53 | * @author chuzhixin 1204505056@qq.com 54 | * @description 删除当前标签页左边全部多标签页 55 | * @param {*} state 56 | * @param {*} route 57 | * @returns 58 | */ 59 | delLeftVisitedRoutes(state, route) { 60 | let index = state.visitedRoutes.length 61 | state.visitedRoutes = state.visitedRoutes.filter((item) => { 62 | if (item.name === route.name) index = state.visitedRoutes.indexOf(item) 63 | return item.meta.affix || index <= state.visitedRoutes.indexOf(item) 64 | }) 65 | }, 66 | /** 67 | * @author chuzhixin 1204505056@qq.com 68 | * @description 删除当前标签页右边全部多标签页 69 | * @param {*} state 70 | * @param {*} route 71 | * @returns 72 | */ 73 | delRightVisitedRoutes(state, route) { 74 | let index = state.visitedRoutes.length 75 | state.visitedRoutes = state.visitedRoutes.filter((item) => { 76 | if (item.name === route.name) index = state.visitedRoutes.indexOf(item) 77 | return item.meta.affix || index >= state.visitedRoutes.indexOf(item) 78 | }) 79 | }, 80 | /** 81 | * @author chuzhixin 1204505056@qq.com 82 | * @description 删除全部多标签页 83 | * @param {*} state 84 | * @param {*} route 85 | * @returns 86 | */ 87 | delAllVisitedRoutes(state) { 88 | state.visitedRoutes = state.visitedRoutes.filter((item) => item.meta.affix) 89 | }, 90 | } 91 | const actions = { 92 | /** 93 | * @author chuzhixin 1204505056@qq.com 94 | * @description 添加标签页 95 | * @param {*} { commit } 96 | * @param {*} route 97 | */ 98 | addVisitedRoute({ commit }, route) { 99 | commit('addVisitedRoute', route) 100 | }, 101 | /** 102 | * @author chuzhixin 1204505056@qq.com 103 | * @description 删除当前标签页 104 | * @param {*} { commit } 105 | * @param {*} route 106 | */ 107 | delVisitedRoute({ commit }, route) { 108 | commit('delVisitedRoute', route) 109 | }, 110 | /** 111 | * @author chuzhixin 1204505056@qq.com 112 | * @description 删除当前标签页以外其它全部多标签页 113 | * @param {*} { commit } 114 | * @param {*} route 115 | */ 116 | delOthersVisitedRoutes({ commit }, route) { 117 | commit('delOthersVisitedRoutes', route) 118 | }, 119 | /** 120 | * @author chuzhixin 1204505056@qq.com 121 | * @description 删除当前标签页左边全部多标签页 122 | * @param {*} { commit } 123 | * @param {*} route 124 | */ 125 | delLeftVisitedRoutes({ commit }, route) { 126 | commit('delLeftVisitedRoutes', route) 127 | }, 128 | /** 129 | * @author chuzhixin 1204505056@qq.com 130 | * @description 删除当前标签页右边全部多标签页 131 | * @param {*} { commit } 132 | * @param {*} route 133 | */ 134 | delRightVisitedRoutes({ commit }, route) { 135 | commit('delRightVisitedRoutes', route) 136 | }, 137 | /** 138 | * @author chuzhixin 1204505056@qq.com 139 | * @description 删除全部多标签页 140 | * @param {*} { commit } 141 | */ 142 | delAllVisitedRoutes({ commit }) { 143 | commit('delAllVisitedRoutes') 144 | }, 145 | } 146 | export default { state, getters, mutations, actions } 147 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author chuzhixin 1204505056@qq.com 3 | * @description vue.config.js全局配置 4 | */ 5 | const path = require('path') 6 | const { 7 | /* baseURL, */ 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('vab-config') 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 | process.env.VUE_APP_TITLE = title || 'genine-admin' 31 | process.env.VUE_APP_AUTHOR = author || 'chuzhixin' 32 | process.env.VUE_APP_UPDATE_TIME = time 33 | process.env.VUE_APP_VERSION = version 34 | 35 | const resolve = (dir) => { 36 | return path.join(__dirname, dir) 37 | } 38 | 39 | // const mockServer = () => { 40 | // if (process.env.NODE_ENV === 'development') { 41 | // return require('./mock/mockServer.js') 42 | // } else { 43 | // return '' 44 | // } 45 | // } 46 | 47 | module.exports = { 48 | publicPath, 49 | assetsDir, 50 | outputDir, 51 | lintOnSave, 52 | transpileDependencies, 53 | devServer: { 54 | hot: true, 55 | port: devPort, 56 | // open: true, 57 | noInfo: false, 58 | overlay: { 59 | warnings: false, 60 | errors: true, 61 | }, 62 | // 注释掉的地方是前端配置代理访问后端的示例 63 | proxy: { 64 | '/api': { 65 | target: `http://39.108.168.143:6179/`, 66 | ws: true, 67 | changeOrigin: true, 68 | pathRewrite: { 69 | ["^/" + "api"]: "", 70 | }, 71 | }, 72 | }, 73 | // after: mockServer(), 74 | }, 75 | configureWebpack() { 76 | return { 77 | resolve: { 78 | alias: { 79 | '@': resolve('src'), 80 | '*': resolve(''), 81 | }, 82 | }, 83 | plugins: [ 84 | new Webpack.ProvidePlugin(providePlugin), 85 | new WebpackBar({ 86 | name: webpackBarName, 87 | }), 88 | ], 89 | } 90 | }, 91 | chainWebpack(config) { 92 | config.resolve.symlinks(true) 93 | config.module.rule('svg').exclude.add(resolve('src/icon/remixIcon')).end() 94 | 95 | config.module 96 | .rule('remixIcon') 97 | .test(/\.svg$/) 98 | .include.add(resolve('src/icon/remixIcon')) 99 | .end() 100 | .use('svg-sprite-loader') 101 | .loader('svg-sprite-loader') 102 | .options({ symbolId: 'remix-icon-[name]' }) 103 | .end() 104 | 105 | config.when(process.env.NODE_ENV === 'development', (config) => { 106 | config.devtool('source-map') 107 | }) 108 | 109 | config.when(process.env.NODE_ENV !== 'development', (config) => { 110 | config.performance.set('hints', false) 111 | config.devtool('none') 112 | config.optimization.splitChunks({ 113 | chunks: 'all', 114 | cacheGroups: { 115 | libs: { 116 | name: 'vue-admin', 117 | test: /[\\/]node_modules[\\/]/, 118 | priority: 10, 119 | chunks: 'initial', 120 | }, 121 | }, 122 | }) 123 | config 124 | .plugin('banner') 125 | .use(Webpack.BannerPlugin, [`${webpackBanner}${time}`]) 126 | .end() 127 | config.module 128 | .rule('images') 129 | .use('image-webpack-loader') 130 | .loader('image-webpack-loader') 131 | .options({ 132 | bypassOnDebug: true, 133 | }) 134 | .end() 135 | }) 136 | 137 | if (build7z) { 138 | config.when(process.env.NODE_ENV === 'production', (config) => { 139 | config 140 | .plugin('fileManager') 141 | .use(FileManagerPlugin, [ 142 | { 143 | onEnd: { 144 | delete: [`./${outputDir}/video`, `./${outputDir}/data`], 145 | archive: [ 146 | { 147 | source: `./${outputDir}`, 148 | destination: `./${outputDir}/${abbreviation}_${outputDir}_${date}.7z`, 149 | }, 150 | ], 151 | }, 152 | }, 153 | ]) 154 | .end() 155 | }) 156 | } 157 | }, 158 | runtimeCompiler: true, 159 | productionSourceMap: false, 160 | css: { 161 | requireModuleExtension: true, 162 | sourceMap: true, 163 | loaderOptions: { 164 | less: { 165 | lessOptions: { 166 | javascriptEnabled: true, 167 | modifyVars: { 168 | 'vab-color-blue': '#1890ff', 169 | 'vab-margin': '20px', 170 | 'vab-padding': '20px', 171 | 'vab-header-height': '65px', 172 | }, 173 | }, 174 | }, 175 | }, 176 | }, 177 | } 178 | -------------------------------------------------------------------------------- /src/store/modules/settings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author chuzhixin 1204505056@qq.com 3 | * @description 所有全局配置的状态管理,如无必要请勿修改 4 | */ 5 | import defaultSettings from '@/config' 6 | import { isJson } from '@/utils/validate' 7 | 8 | const { 9 | logo, 10 | title, 11 | layout, 12 | header, 13 | themeName, 14 | i18n, 15 | showLanguage, 16 | showProgressBar, 17 | showRefresh, 18 | showSearch, 19 | showTheme, 20 | showTagsBar, 21 | showNotice, 22 | showFullScreen, 23 | } = defaultSettings 24 | 25 | const getLocalStorage = (key) => { 26 | const value = localStorage.getItem(key) 27 | if (isJson(value)) { 28 | return JSON.parse(value) 29 | } else { 30 | return false 31 | } 32 | } 33 | 34 | const theme = getLocalStorage('vue-admin-beautiful-pro-theme') 35 | const { collapse } = getLocalStorage('vue-admin-beautiful-pro-collapse') 36 | const { language } = getLocalStorage('vue-admin-beautiful-pro-language') 37 | const toggleBoolean = (key) => { 38 | return typeof theme[key] !== 'undefined' ? theme[key] : key 39 | } 40 | 41 | const state = () => ({ 42 | logo, 43 | title, 44 | collapse, 45 | themeName: theme.themeName || themeName, 46 | layout: theme.layout || layout, 47 | header: theme.header || header, 48 | device: 'desktop', 49 | language: language || i18n, 50 | showLanguage: toggleBoolean(showLanguage), 51 | showProgressBar: toggleBoolean(showProgressBar), 52 | showRefresh: toggleBoolean(showRefresh), 53 | showSearch: toggleBoolean(showSearch), 54 | showTheme: toggleBoolean(showTheme), 55 | showTagsBar: toggleBoolean(showTagsBar), 56 | showNotice: toggleBoolean(showNotice), 57 | showFullScreen: toggleBoolean(showFullScreen), 58 | }) 59 | const getters = { 60 | collapse: (state) => state.collapse, 61 | device: (state) => state.device, 62 | header: (state) => state.header, 63 | language: (state) => state.language, 64 | layout: (state) => state.layout, 65 | logo: (state) => state.logo, 66 | title: (state) => state.title, 67 | showLanguage: (state) => state.showLanguage, 68 | showProgressBar: (state) => state.showProgressBar, 69 | showRefresh: (state) => state.showRefresh, 70 | showSearch: (state) => state.showSearch, 71 | showTheme: (state) => state.showTheme, 72 | showTagsBar: (state) => state.showTagsBar, 73 | showNotice: (state) => state.showNotice, 74 | showFullScreen: (state) => state.showFullScreen, 75 | themeName: (state) => state.themeName, 76 | } 77 | const mutations = { 78 | toggleCollapse(state) { 79 | state.collapse = !state.collapse 80 | localStorage.setItem( 81 | 'vue-admin-beautiful-pro-collapse', 82 | `{"collapse":${state.collapse}}` 83 | ) 84 | }, 85 | toggleDevice(state, device) { 86 | state.device = device 87 | }, 88 | changeHeader(state, header) { 89 | state.header = header 90 | }, 91 | changeLayout(state, layout) { 92 | state.layout = layout 93 | }, 94 | handleShowLanguage(state, showLanguage) { 95 | state.showLanguage = showLanguage 96 | }, 97 | handleShowProgressBar(state, showProgressBar) { 98 | state.showProgressBar = showProgressBar 99 | }, 100 | handleShowRefresh(state, showRefresh) { 101 | state.showRefresh = showRefresh 102 | }, 103 | handleShowSearch(state, showSearch) { 104 | state.showSearch = showSearch 105 | }, 106 | handleShowTheme(state, showTheme) { 107 | state.showTheme = showTheme 108 | }, 109 | handleShowTagsBar(state, showTagsBar) { 110 | state.showTagsBar = showTagsBar 111 | }, 112 | handleShowNotice(state, showNotice) { 113 | state.showNotice = showNotice 114 | }, 115 | handleShowFullScreen(state, showFullScreen) { 116 | state.showFullScreen = showFullScreen 117 | }, 118 | openSideBar(state) { 119 | state.collapse = false 120 | }, 121 | foldSideBar(state) { 122 | state.collapse = true 123 | }, 124 | changeLanguage(state, language) { 125 | localStorage.setItem( 126 | 'vue-admin-beautiful-pro-language', 127 | `{"language":"${language}"}` 128 | ) 129 | state.language = language 130 | }, 131 | } 132 | const actions = { 133 | toggleCollapse({ commit }) { 134 | commit('toggleCollapse') 135 | }, 136 | toggleDevice({ commit }, device) { 137 | commit('toggleDevice', device) 138 | }, 139 | changeHeader({ commit }, header) { 140 | commit('changeHeader', header) 141 | }, 142 | changeLayout({ commit }, layout) { 143 | commit('changeLayout', layout) 144 | }, 145 | handleShowLanguage: ({ commit }, showLanguage) => { 146 | commit('handleShowLanguage', showLanguage) 147 | }, 148 | handleShowProgressBar: ({ commit }, showProgressBar) => { 149 | commit('handleShowProgressBar', showProgressBar) 150 | }, 151 | handleShowRefresh: ({ commit }, showRefresh) => { 152 | commit('handleShowRefresh', showRefresh) 153 | }, 154 | handleShowSearch: ({ commit }, showSearch) => { 155 | commit('handleShowSearch', showSearch) 156 | }, 157 | handleShowTheme: ({ commit }, showTheme) => { 158 | commit('handleShowTheme', showTheme) 159 | }, 160 | handleShowTagsBar({ commit }, showTagsBar) { 161 | commit('handleShowTagsBar', showTagsBar) 162 | }, 163 | handleShowNotice: ({ commit }, showNotice) => { 164 | commit('handleShowNotice', showNotice) 165 | }, 166 | handleShowFullScreen: ({ commit }, showFullScreen) => { 167 | commit('handleShowFullScreen', showFullScreen) 168 | }, 169 | openSideBar({ commit }) { 170 | commit('openSideBar') 171 | }, 172 | foldSideBar({ commit }) { 173 | commit('foldSideBar') 174 | }, 175 | changeLanguage: ({ commit }, language) => { 176 | commit('changeLanguage', language) 177 | }, 178 | } 179 | export default { state, getters, mutations, actions } 180 | -------------------------------------------------------------------------------- /src/views/404.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 69 | 70 | 212 | -------------------------------------------------------------------------------- /src/views/403.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 70 | 71 | 213 | -------------------------------------------------------------------------------- /src/layout/index.vue: -------------------------------------------------------------------------------- 1 | 56 | 136 | 228 | -------------------------------------------------------------------------------- /src/layout/vab-tabs/index.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 178 | 220 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from 'vue-router' 2 | import Layout from '@/layout' 3 | 4 | export const constantRoutes = [ 5 | { 6 | path: '/login', 7 | component: () => import('@/views/login'), 8 | hidden: true, 9 | }, 10 | { 11 | path: '/403', 12 | name: '403', 13 | component: () => import('@/views/403'), 14 | hidden: true, 15 | }, 16 | { 17 | path: '/404', 18 | name: '404', 19 | component: () => import('@/views/404'), 20 | hidden: true, 21 | }, 22 | ] 23 | export const asyncRoutes = [ 24 | { 25 | path: '/', 26 | component: Layout, 27 | redirect: '/index', 28 | meta: { 29 | title: '首页', 30 | icon: 'home-4-line', 31 | affix: true, 32 | }, 33 | children: [ 34 | { 35 | path: 'index', 36 | name: 'Index', 37 | component: () => import('@/views/index'), 38 | meta: { 39 | title: '首页', 40 | icon: 'home-4-line', 41 | affix: true, 42 | }, 43 | }, 44 | ], 45 | }, 46 | { 47 | path: '/', 48 | component: Layout, 49 | redirect: '/user', 50 | meta: { 51 | title: '用户管理', 52 | icon: 'user-line', 53 | affix: true, 54 | }, 55 | children: [ 56 | { 57 | path: 'user', 58 | name: 'User', 59 | component: () => import('@/views/sys/user'), 60 | meta: { 61 | title: '用户管理', 62 | icon: 'user-line', 63 | affix: true, 64 | }, 65 | }, 66 | ], 67 | }, 68 | { 69 | path: '/', 70 | component: Layout, 71 | redirect: '/customer', 72 | meta: { 73 | title: '商户管理', 74 | icon: 'group-line', 75 | affix: true, 76 | }, 77 | children: [ 78 | { 79 | path: 'customer', 80 | name: 'Customer', 81 | component: () => import('@/views/sys/customer'), 82 | meta: { 83 | title: '商户管理', 84 | icon: 'group-line', 85 | affix: true, 86 | }, 87 | }, 88 | ], 89 | }, 90 | { 91 | path: '/', 92 | component: Layout, 93 | redirect: '/advice', 94 | meta: { 95 | title: '建议管理', 96 | icon: 'file-text-line', 97 | affix: true, 98 | }, 99 | children: [ 100 | { 101 | path: 'advice', 102 | name: 'Advice', 103 | component: () => import('@/views/sys/advice'), 104 | meta: { 105 | title: '建议管理', 106 | icon: 'file-text-line', 107 | affix: true, 108 | }, 109 | }, 110 | ], 111 | }, 112 | { 113 | path: '/', 114 | component: Layout, 115 | redirect: '/report', 116 | meta: { 117 | title: '举报管理', 118 | icon: 'phone-line', 119 | affix: true, 120 | }, 121 | children: [ 122 | { 123 | path: 'report', 124 | name: 'Report', 125 | component: () => import('@/views/sys/report'), 126 | meta: { 127 | title: '举报管理', 128 | icon: 'phone-line', 129 | affix: true, 130 | }, 131 | }, 132 | ], 133 | }, 134 | { 135 | path: '/', 136 | component: Layout, 137 | redirect: '/advertisement', 138 | meta: { 139 | title: '广告管理', 140 | icon: 'advertisement-line', 141 | affix: true, 142 | }, 143 | children: [ 144 | { 145 | path: 'advertisement', 146 | name: 'Advertisement', 147 | component: () => import('@/views/sys/ad'), 148 | meta: { 149 | title: '广告管理', 150 | icon: 'advertisement-line', 151 | affix: true, 152 | }, 153 | }, 154 | ], 155 | }, 156 | { 157 | path: '/', 158 | component: Layout, 159 | redirect: '/word', 160 | meta: { 161 | title: '文字管理', 162 | icon: 'text', 163 | affix: true, 164 | }, 165 | children: [ 166 | { 167 | path: 'word', 168 | name: 'Word', 169 | component: () => import('@/views/sys/word'), 170 | meta: { 171 | title: '文字管理', 172 | icon: 'text', 173 | affix: true, 174 | }, 175 | }, 176 | ], 177 | }, 178 | { 179 | path: '/', 180 | component: Layout, 181 | redirect: '/business', 182 | meta: { 183 | title: '业务需求管理', 184 | icon: 'attachment-line', 185 | affix: true, 186 | }, 187 | children: [ 188 | { 189 | path: 'business', 190 | name: 'Business', 191 | component: () => import('@/views/sys/business'), 192 | meta: { 193 | title: '业务需求管理', 194 | icon: 'attachment-line', 195 | affix: true, 196 | }, 197 | }, 198 | ], 199 | }, 200 | { 201 | path: '/', 202 | component: Layout, 203 | redirect: '/classify', 204 | meta: { 205 | title: '需求类别管理', 206 | icon: 'stack-line', 207 | affix: true, 208 | }, 209 | children: [ 210 | { 211 | path: 'classify', 212 | name: 'Classify', 213 | component: () => import('@/views/sys/classify'), 214 | meta: { 215 | title: '需求类别管理', 216 | icon: 'stack-line', 217 | affix: true, 218 | }, 219 | }, 220 | ], 221 | }, 222 | // { 223 | // path: '/vab', 224 | // component: Layout, 225 | // redirect: '/vab/table', 226 | // alwaysShow: true, 227 | // meta: { 228 | // title: '组件', 229 | // icon: 'apps-line', 230 | // }, 231 | // children: [ 232 | // { 233 | // path: 'table', 234 | // name: 'Table', 235 | // component: () => import('@/views/vab/table'), 236 | // meta: { 237 | // title: '表格', 238 | // icon: 'table-2', 239 | // }, 240 | // }, 241 | // { 242 | // path: 'icon', 243 | // name: 'Icon', 244 | // component: () => import('@/views/vab/icon'), 245 | // meta: { 246 | // title: '图标', 247 | // icon: 'remixicon-line', 248 | // }, 249 | // }, 250 | // ], 251 | // }, 252 | // { 253 | // path: '/test', 254 | // component: Layout, 255 | // redirect: '/test/test', 256 | // meta: { 257 | // title: '动态路由测试', 258 | // icon: 'test-tube-line', 259 | // }, 260 | // children: [ 261 | // { 262 | // path: 'test', 263 | // name: 'Test', 264 | // component: () => import('@/views/test'), 265 | // meta: { 266 | // title: '动态路由测试', 267 | // icon: 'test-tube-line', 268 | // }, 269 | // }, 270 | // ], 271 | // }, 272 | // { 273 | // path: '/error', 274 | // name: 'Error', 275 | // component: Layout, 276 | // redirect: '/error/403', 277 | // meta: { 278 | // title: '错误页', 279 | // icon: 'error-warning-line', 280 | // }, 281 | // children: [ 282 | // { 283 | // path: '403', 284 | // name: 'Error403', 285 | // component: () => import('@/views/403'), 286 | // meta: { 287 | // title: '403', 288 | // icon: 'error-warning-line', 289 | // }, 290 | // }, 291 | // { 292 | // path: '404', 293 | // name: 'Error404', 294 | // component: () => import('@/views/404'), 295 | // meta: { 296 | // title: '404', 297 | // icon: 'error-warning-line', 298 | // }, 299 | // }, 300 | // ], 301 | // }, 302 | { 303 | path: '/*', 304 | redirect: '/404', 305 | hidden: true, 306 | }, 307 | ] 308 | const router = createRouter({ 309 | history: createWebHashHistory(), 310 | routes: [...constantRoutes, ...asyncRoutes], 311 | }) 312 | 313 | export default router 314 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | import { devDependencies } from '../../package.json' 2 | if (!devDependencies['vab-config']) document.body.innerHTML = '' 3 | /** 4 | * @author chuzhixin 1204505056@qq.com 5 | * @description 格式化时间 6 | * @param time 7 | * @param cFormat 8 | * @returns {string|null} 9 | */ 10 | export function parseTime(time, cFormat) { 11 | if (arguments.length === 0) { 12 | return null 13 | } 14 | const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}' 15 | let date 16 | if (typeof time === 'object') { 17 | date = time 18 | } else { 19 | if (typeof time === 'string' && /^[0-9]+$/.test(time)) { 20 | time = parseInt(time) 21 | } 22 | if (typeof time === 'number' && time.toString().length === 10) { 23 | time = time * 1000 24 | } 25 | date = new Date(time) 26 | } 27 | const formatObj = { 28 | y: date.getFullYear(), 29 | m: date.getMonth() + 1, 30 | d: date.getDate(), 31 | h: date.getHours(), 32 | i: date.getMinutes(), 33 | s: date.getSeconds(), 34 | a: date.getDay(), 35 | } 36 | return format.replace(/{([ymdhisa])+}/g, (result, key) => { 37 | let value = formatObj[key] 38 | if (key === 'a') { 39 | return ['日', '一', '二', '三', '四', '五', '六'][value] 40 | } 41 | if (result.length > 0 && value < 10) { 42 | value = '0' + value 43 | } 44 | return value || 0 45 | }) 46 | } 47 | 48 | /** 49 | * @author chuzhixin 1204505056@qq.com 50 | * @description 格式化时间 51 | * @param time 52 | * @param option 53 | * @returns {string} 54 | */ 55 | export function formatTime(time, option) { 56 | if (('' + time).length === 10) { 57 | time = parseInt(time) * 1000 58 | } else { 59 | time = +time 60 | } 61 | const d = new Date(time) 62 | const now = Date.now() 63 | 64 | const diff = (now - d) / 1000 65 | 66 | if (diff < 30) { 67 | return '刚刚' 68 | } else if (diff < 3600) { 69 | // less 1 hour 70 | return Math.ceil(diff / 60) + '分钟前' 71 | } else if (diff < 3600 * 24) { 72 | return Math.ceil(diff / 3600) + '小时前' 73 | } else if (diff < 3600 * 24 * 2) { 74 | return '1天前' 75 | } 76 | if (option) { 77 | return parseTime(time, option) 78 | } else { 79 | return ( 80 | d.getMonth() + 81 | 1 + 82 | '月' + 83 | d.getDate() + 84 | '日' + 85 | d.getHours() + 86 | '时' + 87 | d.getMinutes() + 88 | '分' 89 | ) 90 | } 91 | } 92 | 93 | /** 94 | * @author chuzhixin 1204505056@qq.com 95 | * @description 将url请求参数转为json格式 96 | * @param url 97 | * @returns {{}|any} 98 | */ 99 | export function paramObj(url) { 100 | const search = url.split('?')[1] 101 | if (!search) { 102 | return {} 103 | } 104 | return JSON.parse( 105 | '{"' + 106 | decodeURIComponent(search) 107 | .replace(/"/g, '\\"') 108 | .replace(/&/g, '","') 109 | .replace(/=/g, '":"') 110 | .replace(/\+/g, ' ') + 111 | '"}' 112 | ) 113 | } 114 | 115 | /** 116 | * @author chuzhixin 1204505056@qq.com 117 | * @description 父子关系的数组转换成树形结构数据 118 | * @param data 119 | * @returns {*} 120 | */ 121 | export function translateDataToTree(data) { 122 | const parent = data.filter( 123 | (value) => value.parentId === 'undefined' || value.parentId == null 124 | ) 125 | const children = data.filter( 126 | (value) => value.parentId !== 'undefined' && value.parentId != null 127 | ) 128 | const translator = (parent, children) => { 129 | parent.forEach((parent) => { 130 | children.forEach((current, index) => { 131 | if (current.parentId === parent.id) { 132 | const temp = JSON.parse(JSON.stringify(children)) 133 | temp.splice(index, 1) 134 | translator([current], temp) 135 | typeof parent.children !== 'undefined' 136 | ? parent.children.push(current) 137 | : (parent.children = [current]) 138 | } 139 | }) 140 | }) 141 | } 142 | translator(parent, children) 143 | return parent 144 | } 145 | 146 | /** 147 | * @author chuzhixin 1204505056@qq.com 148 | * @description 树形结构数据转换成父子关系的数组 149 | * @param data 150 | * @returns {[]} 151 | */ 152 | export function translateTreeToData(data) { 153 | const result = [] 154 | data.forEach((item) => { 155 | const loop = (data) => { 156 | result.push({ 157 | id: data.id, 158 | name: data.name, 159 | parentId: data.parentId, 160 | }) 161 | const child = data.children 162 | if (child) { 163 | for (let i = 0; i < child.length; i++) { 164 | loop(child[i]) 165 | } 166 | } 167 | } 168 | loop(item) 169 | }) 170 | return result 171 | } 172 | 173 | /** 174 | * @author chuzhixin 1204505056@qq.com 175 | * @description 10位时间戳转换 176 | * @param time 177 | * @returns {string} 178 | */ 179 | export function tenBitTimestamp(time) { 180 | const date = new Date(time * 1000) 181 | const y = date.getFullYear() 182 | let m = date.getMonth() + 1 183 | m = m < 10 ? '' + m : m 184 | let d = date.getDate() 185 | d = d < 10 ? '' + d : d 186 | let h = date.getHours() 187 | h = h < 10 ? '0' + h : h 188 | let minute = date.getMinutes() 189 | let second = date.getSeconds() 190 | minute = minute < 10 ? '0' + minute : minute 191 | second = second < 10 ? '0' + second : second 192 | return y + '年' + m + '月' + d + '日 ' + h + ':' + minute + ':' + second //组合 193 | } 194 | 195 | /** 196 | * @author chuzhixin 1204505056@qq.com 197 | * @description 13位时间戳转换 198 | * @param time 199 | * @returns {string} 200 | */ 201 | export function thirteenBitTimestamp(time) { 202 | const date = new Date(time / 1) 203 | const y = date.getFullYear() 204 | let m = date.getMonth() + 1 205 | m = m < 10 ? '' + m : m 206 | let d = date.getDate() 207 | d = d < 10 ? '' + d : d 208 | let h = date.getHours() 209 | h = h < 10 ? '0' + h : h 210 | let minute = date.getMinutes() 211 | let second = date.getSeconds() 212 | minute = minute < 10 ? '0' + minute : minute 213 | second = second < 10 ? '0' + second : second 214 | return y + '年' + m + '月' + d + '日 ' + h + ':' + minute + ':' + second //组合 215 | } 216 | 217 | /** 218 | * @author chuzhixin 1204505056@qq.com 219 | * @description 获取随机id 220 | * @param length 221 | * @returns {string} 222 | */ 223 | export function uuid(length = 32) { 224 | const num = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890' 225 | let str = '' 226 | for (let i = 0; i < length; i++) { 227 | str += num.charAt(Math.floor(Math.random() * num.length)) 228 | } 229 | return str 230 | } 231 | 232 | /** 233 | * @author chuzhixin 1204505056@qq.com 234 | * @description m到n的随机数 235 | * @param m 236 | * @param n 237 | * @returns {number} 238 | */ 239 | export function random(m, n) { 240 | return Math.floor(Math.random() * (m - n) + n) 241 | } 242 | 243 | /** 244 | * @author chuzhixin 1204505056@qq.com 245 | * @description addEventListener 246 | * @type {function(...[*]=)} 247 | */ 248 | export const on = (function () { 249 | return function (element, event, handler, useCapture = false) { 250 | if (element && event && handler) { 251 | element.addEventListener(event, handler, useCapture) 252 | } 253 | } 254 | })() 255 | 256 | /** 257 | * @author chuzhixin 1204505056@qq.com 258 | * @description removeEventListener 259 | * @type {function(...[*]=)} 260 | */ 261 | export const off = (function () { 262 | return function (element, event, handler, useCapture = false) { 263 | if (element && event) { 264 | element.removeEventListener(event, handler, useCapture) 265 | } 266 | } 267 | })() 268 | 269 | 270 | // 处理page参数 271 | export const handlePage = (data) => { 272 | return { 273 | ...data, 274 | pageNo: data.current, 275 | pageSize: data.pageSize, 276 | } 277 | } -------------------------------------------------------------------------------- /src/utils/validate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author chuzhixin 1204505056@qq.com 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 13 | * @description 校验密码是否小于6位 14 | * @param value 15 | * @returns {boolean} 16 | */ 17 | export function isPassword(value) { 18 | return value.length >= 6 19 | } 20 | 21 | /** 22 | * @author chuzhixin 1204505056@qq.com 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 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 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 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 69 | * @description 判断是否是小写字母 70 | * @param value 71 | * @returns {boolean} 72 | */ 73 | export function isLowerCase(value) { 74 | const reg = /^[a-z]+$/ 75 | return reg.test(value) 76 | } 77 | 78 | /** 79 | * @author chuzhixin 1204505056@qq.com 80 | * @description 判断是否是大写字母 81 | * @param value 82 | * @returns {boolean} 83 | */ 84 | export function isUpperCase(value) { 85 | const reg = /^[A-Z]+$/ 86 | return reg.test(value) 87 | } 88 | 89 | /** 90 | * @author chuzhixin 1204505056@qq.com 91 | * @description 判断是否是大写字母开头 92 | * @param value 93 | * @returns {boolean} 94 | */ 95 | export function isAlphabets(value) { 96 | const reg = /^[A-Za-z]+$/ 97 | return reg.test(value) 98 | } 99 | 100 | /** 101 | * @author chuzhixin 1204505056@qq.com 102 | * @description 判断是否是字符串 103 | * @param value 104 | * @returns {boolean} 105 | */ 106 | export function isString(value) { 107 | return typeof value === 'string' || value instanceof String 108 | } 109 | 110 | /** 111 | * @author chuzhixin 1204505056@qq.com 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 125 | * @description 判断是否是端口号 126 | * @param value 127 | * @returns {boolean} 128 | */ 129 | export function isPort(value) { 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(value) 133 | } 134 | 135 | /** 136 | * @author chuzhixin 1204505056@qq.com 137 | * @description 判断是否是手机号 138 | * @param value 139 | * @returns {boolean} 140 | */ 141 | export function isPhone(value) { 142 | const reg = /^1\d{10}$/ 143 | return reg.test(value) 144 | } 145 | 146 | /** 147 | * @author chuzhixin 1204505056@qq.com 148 | * @description 判断是否是身份证号(第二代) 149 | * @param value 150 | * @returns {boolean} 151 | */ 152 | export function isIdCard(value) { 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(value) 156 | } 157 | 158 | /** 159 | * @author chuzhixin 1204505056@qq.com 160 | * @description 判断是否是邮箱 161 | * @param value 162 | * @returns {boolean} 163 | */ 164 | export function isEmail(value) { 165 | const reg = /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/ 166 | return reg.test(value) 167 | } 168 | 169 | /** 170 | * @author chuzhixin 1204505056@qq.com 171 | * @description 判断是否中文 172 | * @param value 173 | * @returns {boolean} 174 | */ 175 | export function isChina(value) { 176 | const reg = /^[\u4E00-\u9FA5]{2,4}$/ 177 | return reg.test(value) 178 | } 179 | 180 | /** 181 | * @author chuzhixin 1204505056@qq.com 182 | * @description 判断是否为空 183 | * @param value 184 | * @returns {boolean} 185 | */ 186 | export function isBlank(value) { 187 | return ( 188 | value == null || 189 | false || 190 | value === '' || 191 | value.trim() === '' || 192 | value.toLocaleLowerCase().trim() === 'null' 193 | ) 194 | } 195 | 196 | /** 197 | * @author chuzhixin 1204505056@qq.com 198 | * @description 判断是否为固话 199 | * @param value 200 | * @returns {boolean} 201 | */ 202 | export function isTel(value) { 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(value) 206 | } 207 | 208 | /** 209 | * @author chuzhixin 1204505056@qq.com 210 | * @description 判断是否为数字且最多两位小数 211 | * @param value 212 | * @returns {boolean} 213 | */ 214 | export function isNum(value) { 215 | const reg = /^\d+(\.\d{1,2})?$/ 216 | return reg.test(value) 217 | } 218 | 219 | /** 220 | * @author chuzhixin 1204505056@qq.com 221 | * @description 判断经度 -180.0~+180.0(整数部分为0~180,必须输入1到5位小数) 222 | * @param value 223 | * @returns {boolean} 224 | */ 225 | export function isLongitude(value) { 226 | const reg = /^[-|+]?(0?\d{1,2}\.\d{1,5}|1[0-7]?\d{1}\.\d{1,5}|180\.0{1,5})$/ 227 | return reg.test(value) 228 | } 229 | 230 | /** 231 | * @author chuzhixin 1204505056@qq.com 232 | * @description 判断纬度 -90.0~+90.0(整数部分为0~90,必须输入1到5位小数) 233 | * @param value 234 | * @returns {boolean} 235 | */ 236 | export function isLatitude(value) { 237 | const reg = /^[-|+]?([0-8]?\d{1}\.\d{1,5}|90\.0{1,5})$/ 238 | return reg.test(value) 239 | } 240 | 241 | /** 242 | * @author chuzhixin 1204505056@qq.com 243 | * @description rtsp校验,只要有rtsp:// 244 | * @param value 245 | * @returns {boolean} 246 | */ 247 | export function isRTSP(value) { 248 | const reg = 249 | /^rtsp:\/\/([a-z]{0,10}:.{0,10}@)?(\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])$/ 250 | const reg1 = 251 | /^rtsp:\/\/([a-z]{0,10}:.{0,10}@)?(\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]):[0-9]{1,5}/ 252 | const reg2 = 253 | /^rtsp:\/\/([a-z]{0,10}:.{0,10}@)?(\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])\// 254 | return reg.test(value) || reg1.test(value) || reg2.test(value) 255 | } 256 | 257 | /** 258 | * @author chuzhixin 1204505056@qq.com 259 | * @description 判断是否为json 260 | * @param value 261 | * @returns {boolean} 262 | */ 263 | export function isJson(value) { 264 | if (typeof value == 'string') { 265 | try { 266 | var obj = JSON.parse(value) 267 | if (typeof obj == 'object' && obj) { 268 | return true 269 | } else { 270 | return false 271 | } 272 | } catch (e) { 273 | return false 274 | } 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /src/views/login/index.vue: -------------------------------------------------------------------------------- 1 | 99 | 187 | 243 | -------------------------------------------------------------------------------- /src/vab/styles/normalize.less: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in iOS. 9 | */ 10 | 11 | html { 12 | line-height: 1.15; 13 | /* 1 */ 14 | -webkit-text-size-adjust: 100%; 15 | /* 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; 57 | /* 1 */ 58 | height: 0; 59 | /* 1 */ 60 | overflow: visible; 61 | /* 2 */ 62 | } 63 | 64 | /** 65 | * 1. Correct the inheritance and scaling of font size in all browsers. 66 | * 2. Correct the odd `em` font sizing in all browsers. 67 | */ 68 | 69 | pre { 70 | font-family: monospace; 71 | /* 1 */ 72 | font-size: 1em; 73 | /* 2 */ 74 | } 75 | 76 | /* Text-level semantics 77 | ========================================================================== */ 78 | 79 | /** 80 | * Remove the gray background on active links in IE 10. 81 | */ 82 | 83 | a { 84 | background-color: transparent; 85 | } 86 | 87 | /** 88 | * 1. Remove the bottom border in Chrome 57- 89 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 90 | */ 91 | 92 | abbr[title] { 93 | text-decoration: underline; 94 | /* 2 */ 95 | text-decoration: underline dotted; 96 | /* 2 */ 97 | border-bottom: none; 98 | /* 1 */ 99 | } 100 | 101 | /** 102 | * Add the correct font weight in Chrome, Edge, and Safari. 103 | */ 104 | 105 | b, 106 | strong { 107 | font-weight: bolder; 108 | } 109 | 110 | /** 111 | * 1. Correct the inheritance and scaling of font size in all browsers. 112 | * 2. Correct the odd `em` font sizing in all browsers. 113 | */ 114 | 115 | code, 116 | kbd, 117 | samp { 118 | font-family: monospace; 119 | /* 1 */ 120 | font-size: 1em; 121 | /* 2 */ 122 | } 123 | 124 | /** 125 | * Add the correct font size in all browsers. 126 | */ 127 | 128 | small { 129 | font-size: 80%; 130 | } 131 | 132 | /** 133 | * Prevent `sub` and `sup` elements from affecting the line height in 134 | * all browsers. 135 | */ 136 | 137 | sub, 138 | sup { 139 | position: relative; 140 | font-size: 75%; 141 | line-height: 0; 142 | vertical-align: baseline; 143 | } 144 | 145 | sub { 146 | bottom: -0.25em; 147 | } 148 | 149 | sup { 150 | top: -0.5em; 151 | } 152 | 153 | /* Embedded content 154 | ========================================================================== */ 155 | 156 | /** 157 | * Remove the border on images inside links in IE 10. 158 | */ 159 | 160 | img { 161 | border-style: none; 162 | } 163 | 164 | /* Forms 165 | ========================================================================== */ 166 | 167 | /** 168 | * 1. Change the font styles in all browsers. 169 | * 2. Remove the margin in Firefox and Safari. 170 | */ 171 | 172 | button, 173 | input, 174 | optgroup, 175 | select, 176 | textarea { 177 | margin: 0; 178 | /* 2 */ 179 | font-family: inherit; 180 | /* 1 */ 181 | font-size: 100%; 182 | /* 1 */ 183 | line-height: 1.15; 184 | /* 1 */ 185 | } 186 | 187 | /** 188 | * Show the overflow in IE. 189 | * 1. Show the overflow in Edge. 190 | */ 191 | 192 | button, 193 | input { 194 | /* 1 */ 195 | overflow: visible; 196 | } 197 | 198 | /** 199 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 200 | * 1. Remove the inheritance of text transform in Firefox. 201 | */ 202 | 203 | button, 204 | select { 205 | /* 1 */ 206 | text-transform: none; 207 | } 208 | 209 | /** 210 | * Correct the inability to style clickable types in iOS and Safari. 211 | */ 212 | 213 | button, 214 | [type="button"], 215 | [type="reset"], 216 | [type="submit"] { 217 | -webkit-appearance: button; 218 | } 219 | 220 | /** 221 | * Remove the inner border and padding in Firefox. 222 | */ 223 | 224 | button::-moz-focus-inner, 225 | [type="button"]::-moz-focus-inner, 226 | [type="reset"]::-moz-focus-inner, 227 | [type="submit"]::-moz-focus-inner { 228 | padding: 0; 229 | border-style: none; 230 | } 231 | 232 | /** 233 | * Restore the focus styles unset by the previous rule. 234 | */ 235 | 236 | button:-moz-focusring, 237 | [type="button"]:-moz-focusring, 238 | [type="reset"]:-moz-focusring, 239 | [type="submit"]:-moz-focusring { 240 | outline: 1px dotted ButtonText; 241 | } 242 | 243 | /** 244 | * Correct the padding in Firefox. 245 | */ 246 | 247 | fieldset { 248 | padding: 0.35em 0.75em 0.625em; 249 | } 250 | 251 | /** 252 | * 1. Correct the text wrapping in Edge and IE. 253 | * 2. Correct the color inheritance from `fieldset` elements in IE. 254 | * 3. Remove the padding so developers are not caught out when they zero out 255 | * `fieldset` elements in all browsers. 256 | */ 257 | 258 | legend { 259 | box-sizing: border-box; 260 | /* 1 */ 261 | display: table; 262 | /* 1 */ 263 | max-width: 100%; 264 | /* 1 */ 265 | padding: 0; 266 | /* 3 */ 267 | color: inherit; 268 | /* 2 */ 269 | white-space: normal; 270 | /* 1 */ 271 | } 272 | 273 | /** 274 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 275 | */ 276 | 277 | progress { 278 | vertical-align: baseline; 279 | } 280 | 281 | /** 282 | * Remove the default vertical scrollbar in IE 10+. 283 | */ 284 | 285 | textarea { 286 | overflow: auto; 287 | } 288 | 289 | /** 290 | * 1. Add the correct box sizing in IE 10. 291 | * 2. Remove the padding in IE 10. 292 | */ 293 | 294 | [type="checkbox"], 295 | [type="radio"] { 296 | box-sizing: border-box; 297 | /* 1 */ 298 | padding: 0; 299 | /* 2 */ 300 | } 301 | 302 | /** 303 | * Correct the cursor style of increment and decrement buttons in Chrome. 304 | */ 305 | 306 | [type="number"]::-webkit-inner-spin-button, 307 | [type="number"]::-webkit-outer-spin-button { 308 | height: auto; 309 | } 310 | 311 | /** 312 | * 1. Correct the odd appearance in Chrome and Safari. 313 | * 2. Correct the outline style in Safari. 314 | */ 315 | 316 | [type="search"] { 317 | -webkit-appearance: textfield; 318 | /* 1 */ 319 | outline-offset: -2px; 320 | /* 2 */ 321 | } 322 | 323 | /** 324 | * Remove the inner padding in Chrome and Safari on macOS. 325 | */ 326 | 327 | [type="search"]::-webkit-search-decoration { 328 | -webkit-appearance: none; 329 | } 330 | 331 | /** 332 | * 1. Correct the inability to style clickable types in iOS and Safari. 333 | * 2. Change font properties to `inherit` in Safari. 334 | */ 335 | 336 | ::-webkit-file-upload-button { 337 | -webkit-appearance: button; 338 | /* 1 */ 339 | font: inherit; 340 | /* 2 */ 341 | } 342 | 343 | /* Interactive 344 | ========================================================================== */ 345 | 346 | /* 347 | * Add the correct display in Edge, IE 10+, and Firefox. 348 | */ 349 | 350 | details { 351 | display: block; 352 | } 353 | 354 | /* 355 | * Add the correct display in all browsers. 356 | */ 357 | 358 | summary { 359 | display: list-item; 360 | } 361 | 362 | /* Misc 363 | ========================================================================== */ 364 | 365 | /** 366 | * Add the correct display in IE 10+. 367 | */ 368 | 369 | template { 370 | display: none; 371 | } 372 | 373 | /** 374 | * Add the correct display in IE 10. 375 | */ 376 | 377 | [hidden] { 378 | display: none; 379 | } 380 | --------------------------------------------------------------------------------