基于antdv vue3.0的版本首次提交 感谢vue作者尤雨溪 感谢antdv作者唐金州

This commit is contained in:
chuzhixin 2020-10-03 20:23:46 +08:00
parent d5e576485f
commit 841b4bc494
61 changed files with 4160 additions and 0 deletions

3
.browserslistrc Normal file
View File

@ -0,0 +1,3 @@
> 1%
last 2 versions
not dead

14
.eslintrc.js Normal file
View File

@ -0,0 +1,14 @@
module.exports = {
root: true,
env: {
node: true,
},
extends: ["plugin:vue/vue3-essential", "eslint:recommended", "@vue/prettier"],
parserOptions: {
parser: "babel-eslint",
},
rules: {
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
},
};

10
.gitattributes vendored Normal file
View File

@ -0,0 +1,10 @@
*.html text eol=lf
*.css text eol=lf
*.js text eol=lf
*.scss text eol=lf
*.vue text eol=lf
*.hbs text eol=lf
*.sh text eol=lf
*.md text eol=lf
*.json text eol=lf
*.yml text eol=lf

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
.DS_Store
node_modules
/dist
/package-lock.json
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
.stylelintrc.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = {
extends: ["stylelint-config-recess-order", "stylelint-config-prettier"],
};

BIN
README.md

Binary file not shown.

3
babel.config.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = {
presets: ["@vue/cli-plugin-babel/preset"],
};

16
deploy.sh Normal file
View File

@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -e
npm run build
cd dist
touch .nojekyll
git init
git add -A
git commit -m 'deploy'
git push -f "https://${access_token}@gitee.com/chu1204505056/vue-admin-beautiful-mini.git" master:gh-pages
start "https://gitee.com/chu1204505056/vue-admin-beautiful-mini/pages"
cd -
exec /bin/bash

59
mock/controller/router.js Normal file
View File

@ -0,0 +1,59 @@
/**
* @author chuzhixin 1204505056@qq.com
* @description router全局配置如有必要可分文件抽离其中asyncRoutes只有在intelligence模式下才会用到pro版只支持remixIcon图标
* hidden:true 是否显示在菜单中显示路由默认值false
* alwaysShow:true 当只有一级子路由时是否显示父路由是否显示在菜单中显示路由默认值false
* name:"Demo" 首字母大写一定要与vue文件的name对应起来用于noKeepAlive缓存控制该项特别重要
* meta:{
title:"title" 菜单面包屑多标签页显示的名称
roles:["admin","..."] 当config/settings.js中rolesControl配置开启时用于控制角色
roles: {
role: ["admin"],
ability: ["READ","WRITE","DELETE"], ability: ["READ","WRITE"],
mode: "allOf" allOf: 数组内所有角色都拥有返回True oneOf: 数组内拥有任一角色返回True(等价第1种数据) except: 不拥有数组内任一角色返回True(取反)
}
icon:"" remix图标
isCustomSvgIcon:false, 是否是自定义svg图标默认值false如果设置true那么需要把你的svg拷贝到icon/remixIcon下然后remixIcon字段配置上你的图标名
noKeepAlive:true 当前路由是否不缓存默认值false
affix:true 当前路由是否固定多标签页
badge:"New" badge小标签只支持子级
tagHidden:true 当前路由是否不显示多标签页
}
*/
const data = [
{
path: "/",
component: "Layout",
redirect: "/index",
meta: {
title: "首页",
icon: "home-4-line",
affix: true,
},
children: [
{
path: "index",
name: "Index",
component: "@/views/index",
meta: {
title: "首页",
icon: "home-4-line",
affix: true,
},
},
],
},
];
module.exports = [
{
url: "/menu/navigate",
type: "get",
response() {
return {
code: 200,
msg: "success",
data,
};
},
},
];

103
mock/controller/user.js Normal file
View File

@ -0,0 +1,103 @@
const accessTokens = {
admin: "admin-accessToken",
editor: "editor-accessToken",
test: "test-accessToken",
};
module.exports = [
{
url: "/login",
type: "post",
response(config) {
const { username } = config.body;
const accessToken = accessTokens[username];
if (!accessToken) {
return {
code: 500,
msg: "帐户或密码不正确。",
};
}
return {
code: 200,
msg: "success",
data: { accessToken },
};
},
},
{
url: "/socialLogin",
type: "post",
response(config) {
const { code } = config.body;
if (!code) {
return {
code: 500,
msg: "未成功获取Token。",
};
}
return {
code: 200,
msg: "success",
data: { accessToken: accessTokens["admin"] },
};
},
},
{
url: "/register",
type: "post",
response() {
return {
code: 200,
msg: "模拟注册成功",
};
},
},
{
url: "/userInfo",
type: "post",
response(config) {
const { accessToken } = config.body;
let roles = ["admin"];
let ability = ["READ"];
let username = "admin";
if ("admin-accessToken" === accessToken) {
roles = ["admin"];
ability = ["READ", "WRITE", "DELETE"];
username = "admin";
}
if ("editor-accessToken" === accessToken) {
roles = ["editor"];
ability = ["READ", "WRITE"];
username = "editor";
}
if ("test-accessToken" === accessToken) {
roles = ["admin", "editor"];
ability = ["READ"];
username = "test";
}
return {
code: 200,
msg: "success",
data: {
roles,
ability,
username,
"avatar|1": [
"https://i.gtimg.cn/club/item/face/img/2/15922_100.gif",
"https://i.gtimg.cn/club/item/face/img/8/15918_100.gif",
],
},
};
},
},
{
url: "/logout",
type: "post",
response() {
return {
code: 200,
msg: "success",
};
},
},
];

16
mock/index.js Normal file
View File

@ -0,0 +1,16 @@
/**
* @author chuzhixin 1204505056@qq.com
* @description 导入所有 controller 模块npm run serve时在node环境中自动输出controller文件夹下Mock接口请勿修改
*/
const { handleMockArray } = require("./utils");
const mocks = [];
const mockArray = handleMockArray();
mockArray.forEach((item) => {
const obj = require(item);
mocks.push(...obj);
});
module.exports = {
mocks,
};

94
mock/mockServer.js Normal file
View File

@ -0,0 +1,94 @@
const chokidar = require("chokidar");
const bodyParser = require("body-parser");
const chalk = require("chalk");
const path = require("path");
const Mock = require("mockjs");
const { baseURL } = require("../src/config/settings");
const mockDir = path.join(process.cwd(), "mock");
/**
*
* @param app
* @returns {{mockStartIndex: number, mockRoutesLength: number}}
*/
const registerRoutes = (app) => {
let mockLastIndex;
const { mocks } = require("./index.js");
const mocksForServer = mocks.map((route) => {
return responseFake(route.url, route.type, route.response);
});
for (const mock of mocksForServer) {
app[mock.type](mock.url, mock.response);
mockLastIndex = app._router.stack.length;
}
const mockRoutesLength = Object.keys(mocksForServer).length;
return {
mockRoutesLength: mockRoutesLength,
mockStartIndex: mockLastIndex - mockRoutesLength,
};
};
/**
*
* @param url
* @param type
* @param respond
* @returns {{response(*=, *=): void, type: (*|string), url: RegExp}}
*/
const responseFake = (url, type, respond) => {
return {
url: new RegExp(`${baseURL}${url}`),
type: type || "get",
response(req, res) {
res.status(200);
if (JSON.stringify(req.body) !== "{}") {
console.log(chalk.green(`> 请求地址:${req.path}`));
console.log(chalk.green(`> 请求参数:${JSON.stringify(req.body)}\n`));
} else {
console.log(chalk.green(`> 请求地址:${req.path}\n`));
}
res.json(
Mock.mock(respond instanceof Function ? respond(req, res) : respond)
);
},
};
};
/**
*
* @param app
*/
module.exports = (app) => {
app.use(bodyParser.json());
app.use(
bodyParser.urlencoded({
extended: true,
})
);
const mockRoutes = registerRoutes(app);
let mockRoutesLength = mockRoutes.mockRoutesLength;
let mockStartIndex = mockRoutes.mockStartIndex;
chokidar
.watch(mockDir, {
ignored: /mock-server/,
ignoreInitial: true,
})
.on("all", (event) => {
if (event === "change" || event === "add") {
try {
app._router.stack.splice(mockStartIndex, mockRoutesLength);
Object.keys(require.cache).forEach((item) => {
if (item.includes(mockDir)) {
delete require.cache[require.resolve(item)];
}
});
const mockRoutes = registerRoutes(app);
mockRoutesLength = mockRoutes.mockRoutesLength;
mockStartIndex = mockRoutes.mockStartIndex;
} catch (error) {
console.log(chalk.red(error));
}
}
});
};

43
mock/utils/index.js Normal file
View File

@ -0,0 +1,43 @@
const { Random } = require("mockjs");
const { join } = require("path");
const fs = require("fs");
/**
* @author chuzhixin 1204505056@qq.com
* @description 随机生成图片url
* @param width
* @param height
* @returns {string}
*/
function handleRandomImage(width = 50, height = 50) {
return `https://picsum.photos/${width}/${height}?random=${Random.guid()}`;
}
/**
* @author chuzhixin 1204505056@qq.com
* @description 处理所有 controller 模块npm run serve时在node环境中自动输出controller文件夹下Mock接口请勿修改
* @returns {[]}
*/
function handleMockArray() {
const mockArray = [];
const getFiles = (jsonPath) => {
const jsonFiles = [];
const findJsonFile = (path) => {
const files = fs.readdirSync(path);
files.forEach((item) => {
const fPath = join(path, item);
const stat = fs.statSync(fPath);
if (stat.isDirectory() === true) findJsonFile(item);
if (stat.isFile() === true) jsonFiles.push(item);
});
};
findJsonFile(jsonPath);
jsonFiles.forEach((item) => mockArray.push(`./controller/${item}`));
};
getFiles("mock/controller");
return mockArray;
}
module.exports = {
handleRandomImage,
handleMockArray,
};

62
package.json Normal file
View File

@ -0,0 +1,62 @@
{
"name": "vue-admin-beautiful-mini",
"version": "1.0.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"clear": "rimraf node_modules&&npm install --registry=https://registry.npm.taobao.org",
"use:npm": "nrm use npm",
"use:taobao": "nrm use taobao",
"update": "ncu -u --target greatest&&npm install --registry=https://registry.npm.taobao.org",
"deploy": "start ./deploy.sh"
},
"dependencies": {
"ant-design-vue": "^2.0.0-beta.10",
"axios": "^0.20.0",
"core-js": "^3.6.5",
"dayjs": "^1.8.36",
"js-cookie": "^3.0.0-rc.1",
"mockjs": "^1.1.0",
"remixicon": "^2.5.0",
"vue": "^3.0.0",
"vue-router": "^4.0.0-beta.11",
"vuex": "^4.0.0-beta.4",
"zx-layouts": "^0.6.13"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.5.6",
"@vue/cli-plugin-eslint": "^4.5.6",
"@vue/cli-service": "^4.5.6",
"@vue/compiler-sfc": "^3.0.0",
"@vue/eslint-config-prettier": "^6.0.0",
"babel-eslint": "^11.0.0-beta.2",
"body-parser": "^1.19.0",
"chalk": "^4.1.0",
"chokidar": "^3.4.2",
"compression-webpack-plugin": "^6.0.2",
"eslint": "^7.9.0",
"eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-vue": "^7.0.0-beta.4",
"filemanager-webpack-plugin": "^2.0.5",
"less": "^3.12.2",
"less-loader": "^7.0.1",
"lint-staged": "^10.4.0",
"prettier": "^2.1.2",
"stylelint": "^13.7.1",
"stylelint-config-prettier": "^8.0.2",
"stylelint-config-recess-order": "^2.1.0",
"svg-sprite-loader": "^5.0.0",
"webpackbar": "^4.0.0"
},
"gitHooks": {
"pre-commit": "lint-staged"
},
"lint-staged": {
"*.{js,jsx,vue}": [
"vue-cli-service lint",
"git add"
]
}
}

16
prettier.config.js Normal file
View File

@ -0,0 +1,16 @@
module.exports = {
printWidth: 80,
tabWidth: 2,
useTabs: false,
semi: true,
singleQuote: false,
quoteProps: "as-needed",
jsxSingleQuote: false,
trailingComma: "es5",
bracketSpacing: true,
jsxBracketSameLine: false,
arrowParens: "always",
htmlWhitespaceSensitivity: "ignore",
vueIndentScriptAndStyle: true,
endOfLine: "lf",
};

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

17
public/index.html Normal file
View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

6
src/App.vue Normal file
View File

@ -0,0 +1,6 @@
<template>
<router-view />
</template>
<style lang="less">
@import "~@/vab/styles/vab.less";
</style>

9
src/api/router.js Normal file
View File

@ -0,0 +1,9 @@
import request from "@/utils/request";
export function getRouterList(params) {
return request({
url: "/menu/navigate",
method: "get",
params,
});
}

43
src/api/user.js Normal file
View File

@ -0,0 +1,43 @@
import request from "@/utils/request";
import { tokenName } from "@/config/settings";
export async function login(data) {
return request({
url: "/login",
method: "post",
data,
});
}
export async function socialLogin(data) {
return request({
url: "/socialLogin",
method: "post",
data,
});
}
export function getUserInfo(accessToken) {
//此处为了兼容mock.js使用data传递accessToken如果使用mock可以走headers
return request({
url: "/userInfo",
method: "post",
data: {
[tokenName]: accessToken,
},
});
}
export function logout() {
return request({
url: "/logout",
method: "post",
});
}
export function register() {
return request({
url: "/register",
method: "post",
});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

73
src/config/settings.js Normal file
View File

@ -0,0 +1,73 @@
/**
* @author chuzhixin 1204505056@qq.com
* @description 全局变量配置
*/
module.exports = {
//开发以及部署时的URLhash模式时在不确定二级目录名称的情况下建议使用""代表相对路径或者"/二级目录/"history模式默认使用"/"或者"/二级目录/"
publicPath: "",
//生产环境构建文件的目录名
outputDir: "dist",
//放置生成的静态资源 (js、css、img、fonts) 的 (相对于 outputDir 的) 目录。
assetsDir: "static",
//开发环境每次保存时是否输出为eslint编译警告
lintOnSave: true,
//进行编译的依赖
transpileDependencies: [],
//默认的接口地址 如果是开发环境和生产环境走vab-mock-server当然你也可以选择自己配置成需要的接口地址
baseURL:
process.env.NODE_ENV === "development"
? "vab-mock-server"
: "vab-mock-server",
//是否显示logo不显示时设置false显示时请填写remixIcon图标名称暂时只支持设置remixIcon
logo: "vuejs-fill",
//标题 (包括初次加载雪花屏的标题 页面的标题 浏览器的标题)
title: "vue-admin-beautiful",
//标题分隔符
titleSeparator: " - ",
//标题是否反转 如果为false:"page - title"如果为ture:"title - page"
titleReverse: false,
//简写
abbreviation: "vab-pro",
//开发环境端口号
devPort: "9999",
//版本号
version: process.env.VUE_APP_VERSION,
//pro版本copyright可随意修改
copyright: "chuzhixin 1204505056@qq.com",
//不经过token校验的路由
routesWhiteList: ["/login", "/register", "/callback", "/404", "/401"],
//token名称
tokenName: "accessToken",
//token在localStorage、sessionStorage、cookie存储的key的名称
tokenTableName: "vue-admin-beautiful-accessToken",
//token存储位置localStorage sessionStorage cookie
storage: "localStorage",
//token失效回退到登录页时是否记录本次的路由
recordRoute: true,
//配后端数据的接收方式application/json;charset=UTF-8或者application/x-www-form-urlencoded;charset=UTF-8
contentType: "application/json;charset=UTF-8",
//消息框消失时间
messageDuration: 3000,
//最长请求时间
requestTimeout: 10000,
//操作正常code支持String、Array、int多种类型
successCode: [200, 0],
//登录失效code
invalidCode: 402,
//无角色code
noRoleCode: 401,
//是否开启登录拦截
loginInterception: true,
//intelligence前端导出路由和all后端导出路由两种方式
authentication: "intelligence",
//是否开启roles字段进行角色权限控制如果是all模式后端完全处理角色并进行json组装可设置false不处理路由中的roles字段
rolesControl: true,
//需要加loading层的请求防止重复提交
debounce: ["doEdit"],
//需要自动注入并加载的模块
providePlugin: {},
//npm run build时是否自动生成7z压缩包
build7z: false,
//是否显示终端donation打印
donation: false,
};

201
src/layout/index.vue Normal file
View File

@ -0,0 +1,201 @@
<template>
<a-layout class="vab-layout-wrap">
<div
v-if="device === 'mobile' && !collapse"
class="vab-mask"
@click="handleFoldSideBar"
></div>
<a-layout-sider
v-model:collapsed="collapse"
:trigger="null"
collapsible
:class="classObj"
>
<vab-logo />
<a-menu
class="menu"
theme="dark"
mode="inline"
v-model:selectedKeys="selectedKeys"
>
<vab-menu v-for="route in routes" :key="route.path" :item="route" />
</a-menu>
</a-layout-sider>
<a-layout>
<a-layout-header class="header">
<a-row>
<a-col :xs="12" :sm="12" :md="12" :lg="12" :xl="12">
<menu-unfold-outlined
v-if="collapse"
class="trigger"
@click="toggleCollapse"
/>
<menu-fold-outlined
v-else
class="trigger"
@click="toggleCollapse"
/>
</a-col>
<a-col :xs="12" :sm="12" :md="12" :lg="12" :xl="12">
<vab-avatar />
</a-col>
</a-row>
</a-layout-header>
<vab-tabs />
<vab-content />
</a-layout>
</a-layout>
</template>
<script>
import VabLogo from "./vab-logo";
import VabAvatar from "./vab-avatar";
import VabMenu from "./vab-menu";
import VabTabs from "./vab-tabs";
import VabContent from "./vab-content";
import { mapActions, mapGetters } from "vuex";
import { MenuUnfoldOutlined, MenuFoldOutlined } from "@ant-design/icons-vue";
export default {
components: {
VabLogo,
VabAvatar,
VabMenu,
VabTabs,
VabContent,
MenuUnfoldOutlined,
MenuFoldOutlined,
},
data() {
return {
selectedKeys: ["/index"],
};
},
computed: {
...mapGetters({
collapse: "settings/collapse",
routes: "routes/routes",
device: "settings/device",
}),
classObj() {
return {
"vab-mobile": this.device === "mobile",
"vab-collapse": this.collapse,
};
},
},
watch: {
$route: {
handler({ fullPath }) {
if (fullPath === "/index") fullPath = "/";
this.selectedKeys = [fullPath];
},
immediate: true,
},
},
beforeMount() {
window.addEventListener("resize", this.handleLayouts);
},
beforeUnmount() {
window.removeEventListener("resize", this.handleLayouts);
},
mounted() {
this.handleLayouts();
},
methods: {
...mapActions({
toggleDevice: "settings/toggleDevice",
handleFoldSideBar: "settings/foldSideBar",
toggleCollapse: "settings/toggleCollapse",
}),
handleLayouts() {
const width = document.body.getBoundingClientRect().width;
if (this.width !== width) {
const isMobile = width - 1 < 992;
this.toggleDevice(isMobile ? "mobile" : "desktop");
this.width = width;
}
},
},
};
</script>
<style lang="less">
.ant-layout-content {
min-height: calc(
100vh - @vab-header-height - @vab-padding - @vab-padding - @vab-padding -
@vab-padding
) !important;
}
.ant-layout-sider-collapsed {
.vab-logo .anticon + span {
display: inline-block;
max-width: 0;
opacity: 0;
transition: opacity 0.3s cubic-bezier(0.645, 0.045, 0.355, 1),
width 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
}
}
.header {
.ant-col + .ant-col {
display: flex;
justify-content: flex-end;
padding: 0 @vab-padding;
}
}
.vab-mask {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 998;
width: 100%;
height: 100vh;
overflow: hidden;
background: #000;
opacity: 0.5;
}
.vab-mobile {
position: fixed !important;
z-index: 999;
}
.vab-mobile.vab-collapse {
width: 0 !important;
min-width: 0 !important;
max-width: 0 !important;
* {
display: none !important;
width: 0 !important;
min-width: 0 !important;
max-width: 0 !important;
}
.ant-menu-item,
.ant-menu-submenu {
display: none !important;
width: 0 !important;
min-width: 0 !important;
max-width: 0 !important;
}
}
</style>
<style lang="less" scpoed>
.vab-layout-wrap {
.menu {
height: calc(100vh - @vab-header-height);
}
.header {
padding: 0;
background: #fff;
.trigger {
height: @vab-header-height;
padding: 0 @vab-padding;
font-size: 18px;
line-height: @vab-header-height;
cursor: pointer;
transition: color 0.3s;
&:hover {
color: #1890ff;
}
}
}
}
</style>

View File

@ -0,0 +1,53 @@
<template>
<div class="vab-avatar">
<a-dropdown>
<span class="ant-dropdown-link">
<a-avatar :src="avatar" />
{{ username }}
<DownOutlined />
</span>
<template v-slot:overlay>
<a-menu>
<a-menu-item @click="logout">退出登录</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
</template>
<script>
import { recordRoute } from "@/config/settings";
import { DownOutlined } from "@ant-design/icons-vue";
import { mapGetters } from "vuex";
export default {
name: "VabAvatar",
components: { DownOutlined },
computed: {
...mapGetters({
avatar: "user/avatar",
username: "user/username",
}),
},
methods: {
async logout() {
await this.$store.dispatch("user/logout");
if (recordRoute) {
const fullPath = this.$route.fullPath;
this.$router.push(`/login?redirect=${fullPath}`);
} else {
this.$router.push("/login");
}
},
},
};
</script>
<style lang="less">
.vab-avatar {
.ant-dropdown-link {
display: block;
min-height: 64px;
cursor: pointer;
}
}
</style>

View File

@ -0,0 +1,51 @@
<template>
<a-layout-content class="content">
<router-view v-slot="{ Component }">
<transition mode="out-in" name="fade-transform">
<component :is="Component" />
</transition>
</router-view>
</a-layout-content>
</template>
<script>
export default {
name: "VabContent",
watch: {
$route: {
handler() {
if ("mobile" === this.device) {
this.$store.dispatch("settings/foldSideBar");
}
},
immediate: true,
},
},
};
</script>
<style lang="less">
.fade-transform-leave-active,
.fade-transform-enter-active {
transition: all 0.2s;
}
.fade-transform-enter {
opacity: 0;
transform: translateX(-30px);
}
.fade-transform-leave-to {
opacity: 0;
transform: translateX(30px);
}
.content {
padding: @vab-padding;
margin: @vab-margin;
background: #fff;
.error-container {
min-height: 100% !important;
}
}
</style>

View File

@ -0,0 +1,24 @@
<template>
<i :class="'ri-' + icon" aria-hidden="true"></i>
</template>
<script>
import "remixicon/fonts/remixicon.css";
export default {
name: "VabIcon",
props: {
icon: {
type: String,
required: true,
},
},
};
</script>
<style lang="less" scoped>
[class*="ri"] {
font-size: 16px;
vertical-align: -1px;
}
</style>

View File

@ -0,0 +1,37 @@
<template>
<div class="vab-logo">
<vab-icon v-if="logo" :icon="logo"></vab-icon>
<span class="anticon"></span>
<span>{{ title }}</span>
</div>
</template>
<script>
import VabIcon from "@/layout/vab-icon";
import { mapGetters } from "vuex";
export default {
name: "VabLogo",
components: { VabIcon },
computed: {
...mapGetters({
logo: "settings/logo",
title: "settings/title",
}),
},
};
</script>
<style lang="less" scoped>
.vab-logo {
display: flex;
align-items: center;
justify-content: center;
height: 32px;
margin: 16px 5px;
overflow: hidden;
overflow: hidden;
font-size: 15px;
color: #fff;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>

View File

@ -0,0 +1,42 @@
<template>
<a-menu-item :key="routeChildren.fullPath" @click="handleLink">
<span class="anticon">
<vab-icon :icon="routeChildren.meta.icon"></vab-icon>
</span>
<span>{{ routeChildren.meta.title }}</span>
</a-menu-item>
</template>
<script>
import { isExternal } from "@/utils/validate";
import VabIcon from "@/layout/vab-icon";
export default {
name: "MenuItem",
components: { VabIcon },
props: {
item: {
type: Object,
default() {
return null;
},
},
routeChildren: {
type: Object,
default: () => null,
},
},
methods: {
handleLink() {
const routePath = this.routeChildren.fullPath;
const target = this.routeChildren.meta.target;
if (target === "_blank") {
if (isExternal(routePath)) window.open(routePath);
else if (this.$route.path !== routePath) window.open(routePath.href);
} else {
if (isExternal(routePath)) window.location.href = routePath;
else if (this.$route.path !== routePath) this.$router.push(routePath);
}
},
},
};
</script>

View File

@ -0,0 +1,34 @@
<template>
<a-sub-menu :key="item.fullPath">
<template v-slot:title>
<span class="anticon">
<vab-icon :icon="item.meta.icon"></vab-icon>
</span>
<span>{{ item.meta.title }}</span>
</template>
<slot></slot>
</a-sub-menu>
</template>
<script>
import VabIcon from "@/layout/vab-icon";
export default {
name: "Submenu",
components: { VabIcon },
props: {
item: {
type: Object,
default() {
return null;
},
},
routeChildren: {
type: Object,
default() {
return null;
},
},
},
methods: {},
};
</script>

View File

@ -0,0 +1,60 @@
<template>
<component
:is="menuComponent"
v-if="!item.hidden"
:item="item"
:route-children="routeChildren"
>
<template v-if="item.children && item.children.length">
<vab-menu
v-for="route in item.children"
:key="route.fullPath"
:item="route"
></vab-menu>
</template>
</component>
</template>
<script>
import MenuItem from "./components/MenuItem";
import Submenu from "./components/Submenu";
export default {
name: "VabMenu",
components: { MenuItem, Submenu },
props: {
item: {
type: Object,
required: true,
},
},
data() {
return {
routeChildren: {},
menuComponent: "",
};
},
created() {
const showChildren = this.handleChildren(this.item.children);
if (showChildren.length === 0) {
this.menuComponent = "MenuItem";
this.routeChildren = this.item;
} else if (showChildren.length === 1 && this.item.alwaysShow !== true) {
this.menuComponent = "MenuItem";
this.routeChildren = showChildren[0];
} else {
this.menuComponent = "Submenu";
}
},
methods: {
handleChildren(children = []) {
if (children === null) return [];
return children.filter((item) => item.hidden !== true);
},
},
};
</script>
<style lang="less">
.anticon {
margin-right: 3px !important;
}
</style>

View File

@ -0,0 +1,159 @@
<template>
<div class="vab-tabs">
<a-tabs
@tab-click="handleTabClick"
@edit="handleTabRemove"
v-model:activeKey="tabActive"
hide-add
type="editable-card"
>
<a-tab-pane
v-for="item in visitedRoutes"
:key="item.fullPath"
:closable="!isAffix(item)"
:tab="item.meta.title"
></a-tab-pane>
</a-tabs>
</div>
</template>
<script>
import { mapActions, mapGetters } from "vuex";
export default {
name: "VabTabs",
data() {
return {
affixTags: [],
tabActive: null,
created: false,
};
},
computed: {
...mapGetters({
visitedRoutes: "tagsBar/visitedRoutes",
routes: "routes/routes",
}),
},
watch: {
$route: {
handler(route) {
this.addTags(route);
},
},
},
created() {
this.initAffixTags(this.routes);
this.addTags(this.$route);
},
methods: {
...mapActions({
addVisitedRoute: "tagsBar/addVisitedRoute",
delVisitedRoute: "tagsBar/delVisitedRoute",
delOthersVisitedRoutes: "tagsBar/delOthersVisitedRoutes",
delLeftVisitedRoutes: "tagsBar/delLeftVisitedRoutes",
delRightVisitedRoutes: "tagsBar/delRightVisitedRoutes",
delAllVisitedRoutes: "tagsBar/delAllVisitedRoutes",
}),
initAffixTags(routes) {
routes.forEach((route) => {
if (route.meta && route.meta.affix) this.addTags(route);
if (route.children) this.initAffixTags(route.children);
});
},
async addTags(tag) {
if (tag.name && tag.meta && tag.meta.tagHidden !== true) {
let matched = [tag.name];
if (tag.matched) matched = tag.matched.map((item) => item.name);
await this.addVisitedRoute({
path: tag.path,
fullPath: tag.fullPath,
query: tag.query,
name: tag.name,
matched: matched,
meta: { ...tag.meta },
});
this.tabActive = tag.fullPath;
}
},
isActive(route) {
return route.path === this.$route.path;
},
isAffix(tag) {
return tag.meta && tag.meta.affix;
},
handleTabClick(tab) {
const route = this.visitedRoutes.filter((item) => item.path === tab)[0];
if (this.$route.fullPath !== route.fullPath) this.$router.push(route);
},
async handleTabRemove(fullPath) {
const view = this.visitedRoutes.find((item) => {
return fullPath === item.fullPath;
});
await this.delVisitedRoute(view);
if (this.isActive(view)) this.toLastTag();
},
handleCommand(command) {
switch (command) {
case "closeOthersTags":
this.closeOthersTags();
break;
case "closeLeftTags":
this.closeLeftTags();
break;
case "closeRightTags":
this.closeRightTags();
break;
case "closeAllTags":
this.closeAllTags();
break;
}
},
async closeSelectedTag(view) {
await this.delVisitedRoute(view);
if (this.isActive(view)) {
this.toLastTag();
}
},
async closeOthersTags() {
const view = this.toThisTag();
await this.delOthersVisitedRoutes(view);
},
async closeLeftTags() {
const view = this.toThisTag();
await this.delLeftVisitedRoutes(view);
},
async closeRightTags() {
const view = this.toThisTag();
await this.delRightVisitedRoutes(view);
},
async closeAllTags() {
const view = this.toThisTag();
await this.delAllVisitedRoutes();
if (this.affixTags.some((tag) => tag.path === view.path)) return;
this.toLastTag();
},
toLastTag() {
const latestView = this.visitedRoutes.slice(-1)[0];
if (latestView) {
this.$router.push(latestView);
} else {
this.$router.push("/");
}
},
toThisTag() {
const view = this.visitedRoutes.find((item) => {
return item.fullPath === this.$route.fullPath;
});
if (this.$route.path !== view.path) this.$router.push(view);
return view;
},
},
};
</script>
<style lang="less">
.vab-tabs {
.ant-tabs-bar {
margin: 0 !important;
}
}
</style>

17
src/main.js Normal file
View File

@ -0,0 +1,17 @@
import { createApp } from "vue";
import Antd from "ant-design-vue";
import App from "./App";
import router from "./router";
import store from "./store";
import "ant-design-vue/dist/antd.css";
import "@/vab";
/**
* @author chuzhixin 1204505056@qq.com
* @description 正式环境默认使用mock正式项目记得注释后再打包
*/
if (process.env.NODE_ENV === "production") {
const { mockXHR } = require("@/utils/static");
mockXHR();
}
createApp(App).use(store).use(router).use(Antd).mount("#app");

87
src/router/index.js Normal file
View File

@ -0,0 +1,87 @@
import { createRouter, createWebHashHistory } from "vue-router";
import Layout from "@/layout";
export const constantRoutes = [
{
path: "/login",
component: () => import("@/views/login"),
hidden: true,
},
{
path: "/401",
name: "401",
component: () => import("@/views/401"),
hidden: true,
},
{
path: "/404",
name: "404",
component: () => import("@/views/404"),
hidden: true,
},
];
export const asyncRoutes = [
{
path: "/",
component: Layout,
redirect: "/index",
meta: {
title: "首页",
icon: "home-4-line",
affix: true,
},
children: [
{
path: "index",
name: "Index",
component: () => import("@/views/index"),
meta: {
title: "首页",
icon: "home-4-line",
affix: true,
},
},
],
},
{
path: "/error",
name: "Error",
component: Layout,
redirect: "/error/401",
meta: {
title: "错误页",
icon: "error-warning-line",
},
children: [
{
path: "401",
name: "Error401",
component: () => import("@/views/401"),
meta: {
title: "401",
icon: "error-warning-line",
},
},
{
path: "404",
name: "Error404",
component: () => import("@/views/404"),
meta: {
title: "404",
icon: "error-warning-line",
},
},
],
},
{
path: "/*",
redirect: "/404",
hidden: true,
},
];
const router = createRouter({
history: createWebHashHistory(),
routes: constantRoutes,
});
export default router;

17
src/store/index.js Normal file
View File

@ -0,0 +1,17 @@
/**
* @author chuzhixin 1204505056@qq.com
* @description 导入所有 vuex 模块自动加入namespaced:true用于解决vuex命名冲突请勿修改
*/
import { createStore } from "vuex";
const files = require.context("./modules", false, /\.js$/);
const modules = {};
files.keys().forEach((key) => {
modules[key.replace(/(\.\/|\.js)/g, "")] = files(key).default;
});
Object.keys(modules).forEach((key) => {
modules[key]["namespaced"] = true;
});
export default createStore({
modules,
});

33
src/store/modules/acl.js Normal file
View File

@ -0,0 +1,33 @@
const state = {
admin: false,
role: [],
ability: [],
};
const getters = {
admin: (state) => state.admin,
role: (state) => state.role,
ability: (state) => state.ability,
};
const mutations = {
setFull(state, admin) {
state.admin = admin;
},
setRole(state, role) {
state.role = role;
},
setAbility(state, ability) {
state.ability = ability;
},
};
const actions = {
setFull({ commit }, admin) {
commit("setFull", admin);
},
setRole({ commit }, role) {
commit("setRole", role);
},
setAbility({ commit }, ability) {
commit("setAbility", ability);
},
};
export default { state, getters, mutations, actions };

View File

@ -0,0 +1,60 @@
/**
* @author chuzhixin 1204505056@qq.com
* @description 路由拦截状态管理目前两种模式all模式与intelligence模式其中partialRoutes是菜单暂未使用
*/
import { asyncRoutes, constantRoutes } from "@/router";
import { getRouterList } from "@/api/router";
import { convertRouter, filterRoutes } from "@/utils/routes";
const state = { routes: [], partialRoutes: [] };
const getters = {
routes: (state) => state.routes,
partialRoutes: (state) => state.partialRoutes,
};
const mutations = {
setRoutes(state, routes) {
state.routes = routes;
},
setPartialRoutes(state, routes) {
state.partialRoutes = routes;
},
};
const actions = {
/**
* @author chuzhixin 1204505056@qq.com
* @description intelligence模式设置路由
* @param {*} { commit }
* @returns
*/
async setRoutes({ commit }) {
const finallyRoutes = filterRoutes([...constantRoutes, ...asyncRoutes]);
commit("setRoutes", finallyRoutes);
console.log(asyncRoutes);
return [...asyncRoutes];
},
/**
* @author chuzhixin 1204505056@qq.com
* @description all模式设置路由
* @param {*} { commit }
* @returns
*/
async setAllRoutes({ commit }) {
let { data } = await getRouterList();
if (data[data.length - 1].path !== "*")
data.push({ path: "*", redirect: "/404", hidden: true });
const asyncRoutes = convertRouter(data);
const finallyRoutes = filterRoutes([...constantRoutes, ...asyncRoutes]);
commit("setRoutes", finallyRoutes);
return [...asyncRoutes];
},
/**
* @author chuzhixin 1204505056@qq.com
* @description 画廊布局综合布局设置路由
* @param {*} { commit }
* @param accessedRoutes 画廊布局综合布局设置路由
*/
setPartialRoutes({ commit }, accessedRoutes) {
commit("setPartialRoutes", accessedRoutes);
},
};
export default { state, getters, mutations, actions };

View File

@ -0,0 +1,179 @@
/**
* @author chuzhixin 1204505056@qq.com
* @description 所有全局配置的状态管理如无必要请勿修改
*/
import defaultSettings from "@/config/settings";
import { isJson } from "@/utils/validate";
const {
logo,
title,
layout,
header,
themeName,
i18n,
showLanguage,
showProgressBar,
showRefresh,
showSearch,
showTheme,
showTagsBar,
showNotice,
showFullScreen,
} = defaultSettings;
const getLocalStorage = (key) => {
const value = localStorage.getItem(key);
if (isJson(value)) {
return JSON.parse(value);
} else {
return false;
}
};
const theme = getLocalStorage("vue-admin-beautiful-pro-theme");
const { collapse } = getLocalStorage("vue-admin-beautiful-pro-collapse");
const { language } = getLocalStorage("vue-admin-beautiful-pro-language");
const toggleBoolean = (key) => {
return typeof theme[key] !== "undefined" ? theme[key] : key;
};
const state = {
logo,
title,
collapse,
themeName: theme.themeName || themeName,
layout: theme.layout || layout,
header: theme.header || header,
device: "desktop",
language: language || i18n,
showLanguage: toggleBoolean(showLanguage),
showProgressBar: toggleBoolean(showProgressBar),
showRefresh: toggleBoolean(showRefresh),
showSearch: toggleBoolean(showSearch),
showTheme: toggleBoolean(showTheme),
showTagsBar: toggleBoolean(showTagsBar),
showNotice: toggleBoolean(showNotice),
showFullScreen: toggleBoolean(showFullScreen),
};
const getters = {
collapse: (state) => state.collapse,
device: (state) => state.device,
header: (state) => state.header,
language: (state) => state.language,
layout: (state) => state.layout,
logo: (state) => state.logo,
title: (state) => state.title,
showLanguage: (state) => state.showLanguage,
showProgressBar: (state) => state.showProgressBar,
showRefresh: (state) => state.showRefresh,
showSearch: (state) => state.showSearch,
showTheme: (state) => state.showTheme,
showTagsBar: (state) => state.showTagsBar,
showNotice: (state) => state.showNotice,
showFullScreen: (state) => state.showFullScreen,
themeName: (state) => state.themeName,
};
const mutations = {
toggleCollapse(state) {
state.collapse = !state.collapse;
localStorage.setItem(
"vue-admin-beautiful-pro-collapse",
`{"collapse":${state.collapse}}`
);
},
toggleDevice(state, device) {
state.device = device;
},
changeHeader(state, header) {
state.header = header;
},
changeLayout(state, layout) {
state.layout = layout;
},
handleShowLanguage(state, showLanguage) {
state.showLanguage = showLanguage;
},
handleShowProgressBar(state, showProgressBar) {
state.showProgressBar = showProgressBar;
},
handleShowRefresh(state, showRefresh) {
state.showRefresh = showRefresh;
},
handleShowSearch(state, showSearch) {
state.showSearch = showSearch;
},
handleShowTheme(state, showTheme) {
state.showTheme = showTheme;
},
handleShowTagsBar(state, showTagsBar) {
state.showTagsBar = showTagsBar;
},
handleShowNotice(state, showNotice) {
state.showNotice = showNotice;
},
handleShowFullScreen(state, showFullScreen) {
state.showFullScreen = showFullScreen;
},
openSideBar(state) {
state.collapse = false;
},
foldSideBar(state) {
state.collapse = true;
},
changeLanguage(state, language) {
localStorage.setItem(
"vue-admin-beautiful-pro-language",
`{"language":"${language}"}`
);
state.language = language;
},
};
const actions = {
toggleCollapse({ commit }) {
commit("toggleCollapse");
},
toggleDevice({ commit }, device) {
commit("toggleDevice", device);
},
changeHeader({ commit }, header) {
commit("changeHeader", header);
},
changeLayout({ commit }, layout) {
commit("changeLayout", layout);
},
handleShowLanguage: ({ commit }, showLanguage) => {
commit("handleShowLanguage", showLanguage);
},
handleShowProgressBar: ({ commit }, showProgressBar) => {
commit("handleShowProgressBar", showProgressBar);
},
handleShowRefresh: ({ commit }, showRefresh) => {
commit("handleShowRefresh", showRefresh);
},
handleShowSearch: ({ commit }, showSearch) => {
commit("handleShowSearch", showSearch);
},
handleShowTheme: ({ commit }, showTheme) => {
commit("handleShowTheme", showTheme);
},
handleShowTagsBar({ commit }, showTagsBar) {
commit("handleShowTagsBar", showTagsBar);
},
handleShowNotice: ({ commit }, showNotice) => {
commit("handleShowNotice", showNotice);
},
handleShowFullScreen: ({ commit }, showFullScreen) => {
commit("handleShowFullScreen", showFullScreen);
},
openSideBar({ commit }) {
commit("openSideBar");
},
foldSideBar({ commit }) {
commit("foldSideBar");
},
changeLanguage: ({ commit }, language) => {
commit("changeLanguage", language);
},
};
export default { state, getters, mutations, actions };

View File

@ -0,0 +1,146 @@
/**
* @author chuzhixin 1204505056@qq.com
* @description tagsBar多标签页逻辑前期借鉴了很多开源项目发现都有个共同的特点很繁琐并不符合框架设计的初衷后来在github用户cyea的启发下完成了重构请勿修改
*/
const state = {
visitedRoutes: [],
};
const getters = {
visitedRoutes: (state) => state.visitedRoutes,
};
const mutations = {
/**
* @author chuzhixin 1204505056@qq.com
* @description 添加标签页
* @param {*} state
* @param {*} route
* @returns
*/
addVisitedRoute(state, route) {
let target = state.visitedRoutes.find((item) => item.path === route.path);
if (target) {
if (route.fullPath !== target.fullPath) Object.assign(target, route);
return;
}
state.visitedRoutes.push(Object.assign({}, route));
},
/**
* @author chuzhixin 1204505056@qq.com
* @description 删除当前标签页
* @param {*} state
* @param {*} route
* @returns
*/
delVisitedRoute(state, route) {
state.visitedRoutes.forEach((item, index) => {
if (item.path === route.path) state.visitedRoutes.splice(index, 1);
});
},
/**
* @author chuzhixin 1204505056@qq.com
* @description 删除当前标签页以外其它全部多标签页
* @param {*} state
* @param {*} route
* @returns
*/
delOthersVisitedRoutes(state, route) {
state.visitedRoutes = state.visitedRoutes.filter(
(item) => item.meta.affix || item.path === route.path
);
},
/**
* @author chuzhixin 1204505056@qq.com
* @description 删除当前标签页左边全部多标签页
* @param {*} state
* @param {*} route
* @returns
*/
delLeftVisitedRoutes(state, route) {
let index = state.visitedRoutes.length;
state.visitedRoutes = state.visitedRoutes.filter((item) => {
if (item.name === route.name) index = state.visitedRoutes.indexOf(item);
return item.meta.affix || index <= state.visitedRoutes.indexOf(item);
});
},
/**
* @author chuzhixin 1204505056@qq.com
* @description 删除当前标签页右边全部多标签页
* @param {*} state
* @param {*} route
* @returns
*/
delRightVisitedRoutes(state, route) {
let index = state.visitedRoutes.length;
state.visitedRoutes = state.visitedRoutes.filter((item) => {
if (item.name === route.name) index = state.visitedRoutes.indexOf(item);
return item.meta.affix || index >= state.visitedRoutes.indexOf(item);
});
},
/**
* @author chuzhixin 1204505056@qq.com
* @description 删除全部多标签页
* @param {*} state
* @param {*} route
* @returns
*/
delAllVisitedRoutes(state) {
state.visitedRoutes = state.visitedRoutes.filter((item) => item.meta.affix);
},
};
const actions = {
/**
* @author chuzhixin 1204505056@qq.com
* @description 添加标签页
* @param {*} { commit }
* @param {*} route
*/
addVisitedRoute({ commit }, route) {
commit("addVisitedRoute", route);
},
/**
* @author chuzhixin 1204505056@qq.com
* @description 删除当前标签页
* @param {*} { commit }
* @param {*} route
*/
delVisitedRoute({ commit }, route) {
commit("delVisitedRoute", route);
},
/**
* @author chuzhixin 1204505056@qq.com
* @description 删除当前标签页以外其它全部多标签页
* @param {*} { commit }
* @param {*} route
*/
delOthersVisitedRoutes({ commit }, route) {
commit("delOthersVisitedRoutes", route);
},
/**
* @author chuzhixin 1204505056@qq.com
* @description 删除当前标签页左边全部多标签页
* @param {*} { commit }
* @param {*} route
*/
delLeftVisitedRoutes({ commit }, route) {
commit("delLeftVisitedRoutes", route);
},
/**
* @author chuzhixin 1204505056@qq.com
* @description 删除当前标签页右边全部多标签页
* @param {*} { commit }
* @param {*} route
*/
delRightVisitedRoutes({ commit }, route) {
commit("delRightVisitedRoutes", route);
},
/**
* @author chuzhixin 1204505056@qq.com
* @description 删除全部多标签页
* @param {*} { commit }
*/
delAllVisitedRoutes({ commit }) {
commit("delAllVisitedRoutes");
},
};
export default { state, getters, mutations, actions };

138
src/store/modules/user.js Normal file
View File

@ -0,0 +1,138 @@
/**
* @author chuzhixin 1204505056@qq.com
* @description 登录获取用户信息退出登录清除accessToken逻辑不建议修改
*/
import { getUserInfo, login, logout } from "@/api/user";
import {
getAccessToken,
removeAccessToken,
setAccessToken,
} from "@/utils/accessToken";
import { tokenName } from "@/config/settings";
const state = {
accessToken: getAccessToken(),
username: "",
avatar: "",
};
const getters = {
accessToken: (state) => state.accessToken,
username: (state) => state.username,
avatar: (state) => state.avatar,
};
const mutations = {
/**
* @author chuzhixin 1204505056@qq.com
* @description 设置accessToken
* @param {*} state
* @param {*} accessToken
*/
setAccessToken(state, accessToken) {
state.accessToken = accessToken;
setAccessToken(accessToken);
},
/**
* @author chuzhixin 1204505056@qq.com
* @description 设置用户名
* @param {*} state
* @param {*} username
*/
setUsername(state, username) {
state.username = username;
},
/**
* @author chuzhixin 1204505056@qq.com
* @description 设置头像
* @param {*} state
* @param {*} avatar
*/
setAvatar(state, avatar) {
state.avatar = avatar;
},
};
const actions = {
/**
* @author chuzhixin 1204505056@qq.com
* @description 登录拦截放行时设置虚拟角色
* @param {*} { commit, dispatch }
*/
setVirtualRoles({ commit, dispatch }) {
dispatch("acl/setFull", true, { root: true });
commit(
"setAvatar",
"https://i.gtimg.cn/club/item/face/img/2/15922_100.gif"
);
commit("setUsername", "admin(未开启登录拦截)");
},
/**
* @author chuzhixin 1204505056@qq.com
* @description 登录
* @param {*} { commit }
* @param {*} userInfo
*/
async login({ commit }, userInfo) {
const { data } = await login(userInfo);
const accessToken = data[tokenName];
if (accessToken) {
commit("setAccessToken", accessToken);
} else {
/* Vue.prototype.$baseMessage(
`登录接口异常,未正确返回${tokenName}...`,
"error"
); */
}
},
/**
* @author chuzhixin 1204505056@qq.com
* @description 获取用户信息接口 这个接口非常非常重要如果没有明确底层前逻辑禁止修改此方法错误的修改可能造成整个框架无法正常使用
* @param {*} { commit, dispatch, state }
* @returns
*/
async getUserInfo({ commit, dispatch, state }) {
const { data } = await getUserInfo(state.accessToken);
if (!data) {
/* Vue.prototype.$baseMessage("验证失败,请重新登录...", "error"); */
return false;
}
let { username, avatar, roles, ability } = data;
if (username && roles && Array.isArray(roles)) {
dispatch("acl/setRole", roles, { root: true });
if (ability && ability.length > 0)
dispatch("acl/setAbility", ability, { root: true });
commit("setUsername", username);
commit("setAvatar", avatar);
} else {
console.log("用户信息接口异常");
}
},
/**
* @author chuzhixin 1204505056@qq.com
* @description 退出登录
* @param {*} { dispatch }
*/
async logout({ dispatch }) {
await logout(state.accessToken);
await dispatch("resetAll");
},
/**
* @author chuzhixin 1204505056@qq.com
* @description 重置accessTokenrolesabilityrouter等
* @param {*} { commit, dispatch }
*/
async resetAll({ dispatch }) {
await dispatch("setAccessToken", "");
await dispatch("acl/setFull", false, { root: true });
await dispatch("acl/setRole", [], { root: true });
await dispatch("acl/setAbility", [], { root: true });
removeAccessToken();
},
/**
* @author chuzhixin 1204505056@qq.com
* @description 设置token
*/
setAccessToken({ commit }, accessToken) {
commit("setAccessToken", accessToken);
},
};
export default { state, getters, mutations, actions };

66
src/utils/accessToken.js Normal file
View File

@ -0,0 +1,66 @@
import { storage, tokenTableName } from "@/config/settings";
import cookie from "js-cookie";
/**
* @author chuzhixin 1204505056@qq.com
* @description 获取accessToken
* @returns {string|ActiveX.IXMLDOMNode|Promise<any>|any|IDBRequest<any>|MediaKeyStatus|FormDataEntryValue|Function|Promise<Credential | null>}
*/
export function getAccessToken() {
if (storage) {
if ("localStorage" === storage) {
return localStorage.getItem(tokenTableName);
} else if ("sessionStorage" === storage) {
return sessionStorage.getItem(tokenTableName);
} else if ("cookie" === storage) {
return cookie.get(tokenTableName);
} else {
return localStorage.getItem(tokenTableName);
}
} else {
return localStorage.getItem(tokenTableName);
}
}
/**
* @author chuzhixin 1204505056@qq.com
* @description 存储accessToken
* @param accessToken
* @returns {void|*}
*/
export function setAccessToken(accessToken) {
if (storage) {
if ("localStorage" === storage) {
return localStorage.setItem(tokenTableName, accessToken);
} else if ("sessionStorage" === storage) {
return sessionStorage.setItem(tokenTableName, accessToken);
} else if ("cookie" === storage) {
return cookie.set(tokenTableName, accessToken);
} else {
return localStorage.setItem(tokenTableName, accessToken);
}
} else {
return localStorage.setItem(tokenTableName, accessToken);
}
}
/**
* @author chuzhixin 1204505056@qq.com
* @description 移除accessToken
* @returns {void|Promise<void>}
*/
export function removeAccessToken() {
if (storage) {
if ("localStorage" === storage) {
return localStorage.removeItem(tokenTableName);
} else if ("sessionStorage" === storage) {
return sessionStorage.clear();
} else if ("cookie" === storage) {
return cookie.remove(tokenTableName);
} else {
return localStorage.removeItem(tokenTableName);
}
} else {
return localStorage.removeItem(tokenTableName);
}
}

51
src/utils/hasRole.js Normal file
View File

@ -0,0 +1,51 @@
import store from "@/store";
export function hasRole(value) {
if (store.getters["acl/admin"]) return true;
if (value instanceof Array && value.length > 0)
return can(store.getters["acl/role"], {
role: value,
mode: "oneOf",
});
let mode = "oneOf";
if (Object.prototype.hasOwnProperty.call(value, "mode")) mode = value["mode"];
let result = true;
if (Object.prototype.hasOwnProperty.call(value, "role"))
result =
result && can(store.getters["acl/role"], { role: value["role"], mode });
if (result && Object.prototype.hasOwnProperty.call(value, "ability"))
result =
result &&
can(store.getters["acl/ability"], {
role: value["ability"],
mode,
});
return result;
}
export function can(roleOrAbility, value) {
let hasRole = false;
if (
value instanceof Object &&
Object.prototype.hasOwnProperty.call(value, "role") &&
Object.prototype.hasOwnProperty.call(value, "mode")
) {
const { role, mode } = value;
if (mode === "allOf") {
hasRole = role.every((item) => {
return roleOrAbility.includes(item);
});
}
if (mode === "oneOf") {
hasRole = role.some((item) => {
return roleOrAbility.includes(item);
});
}
if (mode === "except") {
hasRole = !role.some((item) => {
return roleOrAbility.includes(item);
});
}
}
return hasRole;
}

265
src/utils/index.js Normal file
View File

@ -0,0 +1,265 @@
/**
* @author chuzhixin 1204505056@qq.com
* @description 格式化时间
* @param time
* @param cFormat
* @returns {string|null}
*/
export function parseTime(time, cFormat) {
if (arguments.length === 0) {
return null;
}
const format = cFormat || "{y}-{m}-{d} {h}:{i}:{s}";
let date;
if (typeof time === "object") {
date = time;
} else {
if (typeof time === "string" && /^[0-9]+$/.test(time)) {
time = parseInt(time);
}
if (typeof time === "number" && time.toString().length === 10) {
time = time * 1000;
}
date = new Date(time);
}
const formatObj = {
y: date.getFullYear(),
m: date.getMonth() + 1,
d: date.getDate(),
h: date.getHours(),
i: date.getMinutes(),
s: date.getSeconds(),
a: date.getDay(),
};
return format.replace(/{([ymdhisa])+}/g, (result, key) => {
let value = formatObj[key];
if (key === "a") {
return ["日", "一", "二", "三", "四", "五", "六"][value];
}
if (result.length > 0 && value < 10) {
value = "0" + value;
}
return value || 0;
});
}
/**
* @author chuzhixin 1204505056@qq.com
* @description 格式化时间
* @param time
* @param option
* @returns {string}
*/
export function formatTime(time, option) {
if (("" + time).length === 10) {
time = parseInt(time) * 1000;
} else {
time = +time;
}
const d = new Date(time);
const now = Date.now();
const diff = (now - d) / 1000;
if (diff < 30) {
return "刚刚";
} else if (diff < 3600) {
// less 1 hour
return Math.ceil(diff / 60) + "分钟前";
} else if (diff < 3600 * 24) {
return Math.ceil(diff / 3600) + "小时前";
} else if (diff < 3600 * 24 * 2) {
return "1天前";
}
if (option) {
return parseTime(time, option);
} else {
return (
d.getMonth() +
1 +
"月" +
d.getDate() +
"日" +
d.getHours() +
"时" +
d.getMinutes() +
"分"
);
}
}
/**
* @author chuzhixin 1204505056@qq.com
* @description 将url请求参数转为json格式
* @param url
* @returns {{}|any}
*/
export function paramObj(url) {
const search = url.split("?")[1];
if (!search) {
return {};
}
return JSON.parse(
'{"' +
decodeURIComponent(search)
.replace(/"/g, '\\"')
.replace(/&/g, '","')
.replace(/=/g, '":"')
.replace(/\+/g, " ") +
'"}'
);
}
/**
* @author chuzhixin 1204505056@qq.com
* @description 父子关系的数组转换成树形结构数据
* @param data
* @returns {*}
*/
export function translateDataToTree(data) {
const parent = data.filter(
(value) => value.parentId === "undefined" || value.parentId == null
);
const children = data.filter(
(value) => value.parentId !== "undefined" && value.parentId != null
);
const translator = (parent, children) => {
parent.forEach((parent) => {
children.forEach((current, index) => {
if (current.parentId === parent.id) {
const temp = JSON.parse(JSON.stringify(children));
temp.splice(index, 1);
translator([current], temp);
typeof parent.children !== "undefined"
? parent.children.push(current)
: (parent.children = [current]);
}
});
});
};
translator(parent, children);
return parent;
}
/**
* @author chuzhixin 1204505056@qq.com
* @description 树形结构数据转换成父子关系的数组
* @param data
* @returns {[]}
*/
export function translateTreeToData(data) {
const result = [];
data.forEach((item) => {
const loop = (data) => {
result.push({
id: data.id,
name: data.name,
parentId: data.parentId,
});
const child = data.children;
if (child) {
for (let i = 0; i < child.length; i++) {
loop(child[i]);
}
}
};
loop(item);
});
return result;
}
/**
* @author chuzhixin 1204505056@qq.com
* @description 10位时间戳转换
* @param time
* @returns {string}
*/
export function tenBitTimestamp(time) {
const date = new Date(time * 1000);
const y = date.getFullYear();
let m = date.getMonth() + 1;
m = m < 10 ? "" + m : m;
let d = date.getDate();
d = d < 10 ? "" + d : d;
let h = date.getHours();
h = h < 10 ? "0" + h : h;
let minute = date.getMinutes();
let second = date.getSeconds();
minute = minute < 10 ? "0" + minute : minute;
second = second < 10 ? "0" + second : second;
return y + "年" + m + "月" + d + "日 " + h + ":" + minute + ":" + second; //组合
}
/**
* @author chuzhixin 1204505056@qq.com
* @description 13位时间戳转换
* @param time
* @returns {string}
*/
export function thirteenBitTimestamp(time) {
const date = new Date(time / 1);
const y = date.getFullYear();
let m = date.getMonth() + 1;
m = m < 10 ? "" + m : m;
let d = date.getDate();
d = d < 10 ? "" + d : d;
let h = date.getHours();
h = h < 10 ? "0" + h : h;
let minute = date.getMinutes();
let second = date.getSeconds();
minute = minute < 10 ? "0" + minute : minute;
second = second < 10 ? "0" + second : second;
return y + "年" + m + "月" + d + "日 " + h + ":" + minute + ":" + second; //组合
}
/**
* @author chuzhixin 1204505056@qq.com
* @description 获取随机id
* @param length
* @returns {string}
*/
export function uuid(length = 32) {
const num = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
let str = "";
for (let i = 0; i < length; i++) {
str += num.charAt(Math.floor(Math.random() * num.length));
}
return str;
}
/**
* @author chuzhixin 1204505056@qq.com
* @description m到n的随机数
* @param m
* @param n
* @returns {number}
*/
export function random(m, n) {
return Math.floor(Math.random() * (m - n) + n);
}
/**
* @author chuzhixin 1204505056@qq.com
* @description addEventListener
* @type {function(...[*]=)}
*/
export const on = (function () {
return function (element, event, handler, useCapture = false) {
if (element && event && handler) {
element.addEventListener(event, handler, useCapture);
}
};
})();
/**
* @author chuzhixin 1204505056@qq.com
* @description removeEventListener
* @type {function(...[*]=)}
*/
export const off = (function () {
return function (element, event, handler, useCapture = false) {
if (element && event) {
element.removeEventListener(event, handler, useCapture);
}
};
})();

15
src/utils/pageTitle.js Normal file
View File

@ -0,0 +1,15 @@
import { title, titleReverse, titleSeparator } from "@/config/settings";
/**
* @author chuzhixin 1204505056@qq.com
* @description 设置标题
* @param pageTitle
* @returns {string}
*/
export default function getPageTitle(pageTitle) {
let newTitles = [];
if (pageTitle) newTitles.push(pageTitle);
if (title) newTitles.push(title);
if (titleReverse) newTitles = newTitles.reverse();
return newTitles.join(titleSeparator);
}

126
src/utils/request.js Normal file
View File

@ -0,0 +1,126 @@
import axios from "axios";
import {
baseURL,
contentType,
debounce,
invalidCode,
noRoleCode,
requestTimeout,
successCode,
tokenName,
} from "@/config/settings";
import store from "@/store";
import qs from "qs";
import router from "@/router";
import { isArray } from "@/utils/validate";
let loadingInstance;
/**
* @author chuzhixin 1204505056@qq.com
* @description 处理code异常
* @param {*} code
* @param {*} msg
*/
const handleCode = (code, msg) => {
switch (code) {
case invalidCode:
alert(msg || `后端接口${code}异常`, "error");
store.dispatch("user/resetAll").catch(() => {});
break;
case noRoleCode:
router.push({ path: "/401" }).catch(() => {});
break;
default:
alert(msg || `后端接口${code}异常`, "error");
break;
}
};
/**
* @author chuzhixin 1204505056@qq.com
* @description axios初始化
*/
const instance = axios.create({
baseURL,
timeout: requestTimeout,
headers: {
"Content-Type": contentType,
},
});
/**
* @author chuzhixin 1204505056@qq.com
* @description axios请求拦截器
*/
instance.interceptors.request.use(
(config) => {
if (store.getters["user/accessToken"])
config.headers[tokenName] = store.getters["user/accessToken"];
if (
config.data &&
config.headers["Content-Type"] ===
"application/x-www-form-urlencoded;charset=UTF-8"
)
config.data = qs.stringify(config.data);
if (debounce.some((item) => config.url.includes(item))) {
//这里写加载动画
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
/**
* @author chuzhixin 1204505056@qq.com
* @description axios响应拦截器
*/
instance.interceptors.response.use(
(response) => {
if (loadingInstance) loadingInstance.close();
const { data, config } = response;
const { code, msg } = data;
// 操作正常Code数组
const codeVerificationArray = isArray(successCode)
? [...successCode]
: [...[successCode]];
// 是否操作正常
if (codeVerificationArray.includes(code)) {
return data;
} else {
handleCode(code, msg);
return Promise.reject(
"vue-admin-beautiful请求异常拦截:" +
JSON.stringify({ url: config.url, code, msg }) || "Error"
);
}
},
(error) => {
if (loadingInstance) loadingInstance.close();
const { response, message } = error;
if (error.response && error.response.data) {
const { status, data } = response;
handleCode(status, data.msg || message);
return Promise.reject(error);
} else {
let { message } = error;
if (message === "Network Error") {
message = "后端接口连接异常";
}
if (message.includes("timeout")) {
message = "后端接口请求超时";
}
if (message.includes("Request failed with status code")) {
const code = message.substr(message.length - 3);
message = "后端接口" + code + "异常";
}
alert(message || `后端接口未知异常`, "error");
return Promise.reject(error);
}
}
);
export default instance;

77
src/utils/routes.js Normal file
View File

@ -0,0 +1,77 @@
import router from "@/router";
import path from "path";
import { rolesControl } from "@/config/settings";
import { isExternal } from "@/utils/validate";
import { hasRole } from "@/utils/hasRole";
/**
* @author chuzhixin 1204505056@qq.com
* @description all模式渲染后端返回路由
* @param constantRoutes
* @returns {*}
*/
export function convertRouter(constantRoutes) {
return constantRoutes.map((route) => {
if (route.component) {
if (route.component === "Layout") {
const path = "layouts";
route.component = (resolve) => require([`@/${path}`], resolve);
} else {
let path = "views/" + route.component;
if (
new RegExp("^/views/.*$").test(route.component) ||
new RegExp("^views/.*$").test(route.component)
) {
path = route.component;
} else if (new RegExp("^/.*$").test(route.component)) {
path = "views" + route.component;
} else if (new RegExp("^@views/.*$").test(route.component)) {
path = route.component.slice(1);
} else {
path = "views/" + route.component;
}
route.component = (resolve) => require([`@/${path}`], resolve);
}
}
if (route.children && route.children.length)
route.children = convertRouter(route.children);
if (route.children && route.children.length === 0) delete route.children;
return route;
});
}
/**
* @author chuzhixin 1204505056@qq.com
* @description 根据roles数组拦截路由
* @param routes
* @param baseUrl
* @returns {[]}
*/
export function filterRoutes(routes, baseUrl = "/") {
return routes
.filter((route) => {
if (route.meta && route.meta.roles)
return !rolesControl || hasRole(route.meta.roles);
else return true;
})
.map((route) => {
if (route.path !== "*" && !isExternal(route.path))
route.path = path.resolve(baseUrl, route.path);
route.fullPath = route.path;
if (route.children)
route.children = filterRoutes(route.children, route.fullPath);
return route;
});
}
/**
* 根据当前页面firstMenu
* @returns {string}
*/
export function handleFirstMenu() {
const firstMenu = router.currentRoute.matched[0].path;
if (firstMenu === "") return "/";
return firstMenu;
}

52
src/utils/static.js Normal file
View File

@ -0,0 +1,52 @@
/**
* @author chuzhixin 1204505056@qq.com
* @description 导入所有 controller 模块浏览器环境中自动输出controller文件夹下Mock接口请勿修改
*/
import Mock from "mockjs";
import { paramObj } from "@/utils/index";
const mocks = [];
const files = require.context("../../mock/controller", false, /\.js$/);
files.keys().forEach((key) => {
mocks.push(...files(key));
});
export function mockXHR() {
Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send;
Mock.XHR.prototype.send = function () {
if (this.custom.xhr) {
this.custom.xhr.withCredentials = this.withCredentials || false;
if (this.responseType) {
this.custom.xhr.responseType = this.responseType;
}
}
this.proxy_send(...arguments);
};
function XHRHttpRequst(respond) {
return function (options) {
let result;
if (respond instanceof Function) {
const { body, type, url } = options;
result = respond({
method: type,
body: JSON.parse(body),
query: paramObj(url),
});
} else {
result = respond;
}
return Mock.mock(result);
};
}
mocks.forEach((item) => {
Mock.mock(
new RegExp(item.url),
item.type || "get",
XHRHttpRequst(item.response)
);
});
}

268
src/utils/validate.js Normal file
View File

@ -0,0 +1,268 @@
/**
* @author chuzhixin 1204505056@qq.com
* @description 判读是否为外链
* @param path
* @returns {boolean}
*/
export function isExternal(path) {
return /^(https?:|mailto:|tel:)/.test(path);
}
/**
* @author chuzhixin 1204505056@qq.com
* @description 校验密码是否小于6位
* @param value
* @returns {boolean}
*/
export function isPassword(value) {
return value.length >= 6;
}
/**
* @author chuzhixin 1204505056@qq.com
* @description 判断是否为数字
* @param value
* @returns {boolean}
*/
export function isNumber(value) {
const reg = /^[0-9]*$/;
return reg.test(value);
}
/**
* @author chuzhixin 1204505056@qq.com
* @description 判断是否是名称
* @param value
* @returns {boolean}
*/
export function isName(value) {
const reg = /^[\u4e00-\u9fa5a-zA-Z0-9]+$/;
return reg.test(value);
}
/**
* @author chuzhixin 1204505056@qq.com
* @description 判断是否为IP
* @param ip
* @returns {boolean}
*/
export function isIP(ip) {
const reg = /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/;
return reg.test(ip);
}
/**
* @author chuzhixin 1204505056@qq.com
* @description 判断是否是传统网站
* @param url
* @returns {boolean}
*/
export function isUrl(url) {
const reg = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/;
return reg.test(url);
}
/**
* @author chuzhixin 1204505056@qq.com
* @description 判断是否是小写字母
* @param value
* @returns {boolean}
*/
export function isLowerCase(value) {
const reg = /^[a-z]+$/;
return reg.test(value);
}
/**
* @author chuzhixin 1204505056@qq.com
* @description 判断是否是大写字母
* @param value
* @returns {boolean}
*/
export function isUpperCase(value) {
const reg = /^[A-Z]+$/;
return reg.test(value);
}
/**
* @author chuzhixin 1204505056@qq.com
* @description 判断是否是大写字母开头
* @param value
* @returns {boolean}
*/
export function isAlphabets(value) {
const reg = /^[A-Za-z]+$/;
return reg.test(value);
}
/**
* @author chuzhixin 1204505056@qq.com
* @description 判断是否是字符串
* @param value
* @returns {boolean}
*/
export function isString(value) {
return typeof value === "string" || value instanceof String;
}
/**
* @author chuzhixin 1204505056@qq.com
* @description 判断是否是数组
* @param arg
* @returns {arg is any[]|boolean}
*/
export function isArray(arg) {
if (typeof Array.isArray === "undefined") {
return Object.prototype.toString.call(arg) === "[object Array]";
}
return Array.isArray(arg);
}
/**
* @author chuzhixin 1204505056@qq.com
* @description 判断是否是端口号
* @param value
* @returns {boolean}
*/
export function isPort(value) {
const reg = /^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$/;
return reg.test(value);
}
/**
* @author chuzhixin 1204505056@qq.com
* @description 判断是否是手机号
* @param value
* @returns {boolean}
*/
export function isPhone(value) {
const reg = /^1\d{10}$/;
return reg.test(value);
}
/**
* @author chuzhixin 1204505056@qq.com
* @description 判断是否是身份证号(第二代)
* @param value
* @returns {boolean}
*/
export function isIdCard(value) {
const reg = /^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/;
return reg.test(value);
}
/**
* @author chuzhixin 1204505056@qq.com
* @description 判断是否是邮箱
* @param value
* @returns {boolean}
*/
export function isEmail(value) {
const reg = /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/;
return reg.test(value);
}
/**
* @author chuzhixin 1204505056@qq.com
* @description 判断是否中文
* @param value
* @returns {boolean}
*/
export function isChina(value) {
const reg = /^[\u4E00-\u9FA5]{2,4}$/;
return reg.test(value);
}
/**
* @author chuzhixin 1204505056@qq.com
* @description 判断是否为空
* @param value
* @returns {boolean}
*/
export function isBlank(value) {
return (
value == null ||
false ||
value === "" ||
value.trim() === "" ||
value.toLocaleLowerCase().trim() === "null"
);
}
/**
* @author chuzhixin 1204505056@qq.com
* @description 判断是否为固话
* @param value
* @returns {boolean}
*/
export function isTel(value) {
const reg = /^(400|800)([0-9\\-]{7,10})|(([0-9]{4}|[0-9]{3})([- ])?)?([0-9]{7,8})(([- 转])*([0-9]{1,4}))?$/;
return reg.test(value);
}
/**
* @author chuzhixin 1204505056@qq.com
* @description 判断是否为数字且最多两位小数
* @param value
* @returns {boolean}
*/
export function isNum(value) {
const reg = /^\d+(\.\d{1,2})?$/;
return reg.test(value);
}
/**
* @author chuzhixin 1204505056@qq.com
* @description 判断经度 -180.0+180.0整数部分为0180必须输入1到5位小数
* @param value
* @returns {boolean}
*/
export function isLongitude(value) {
const reg = /^[-|+]?(0?\d{1,2}\.\d{1,5}|1[0-7]?\d{1}\.\d{1,5}|180\.0{1,5})$/;
return reg.test(value);
}
/**
* @author chuzhixin 1204505056@qq.com
* @description 判断纬度 -90.0+90.0整数部分为090必须输入1到5位小数
* @param value
* @returns {boolean}
*/
export function isLatitude(value) {
const reg = /^[-|+]?([0-8]?\d{1}\.\d{1,5}|90\.0{1,5})$/;
return reg.test(value);
}
/**
* @author chuzhixin 1204505056@qq.com
* @description rtsp校验只要有rtsp://
* @param value
* @returns {boolean}
*/
export function isRTSP(value) {
const reg = /^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])$/;
const reg1 = /^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}/;
const reg2 = /^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])\//;
return reg.test(value) || reg1.test(value) || reg2.test(value);
}
/**
* @author chuzhixin 1204505056@qq.com
* @description 判断是否为json
* @param value
* @returns {boolean}
*/
export function isJson(value) {
if (typeof value == "string") {
try {
var obj = JSON.parse(value);
if (typeof obj == "object" && obj) {
return true;
} else {
return false;
}
} catch (e) {
return false;
}
}
}

5
src/vab/index.js Normal file
View File

@ -0,0 +1,5 @@
// 加载插件
const requirePlugin = require.context("./plugins", true, /\.js$/);
requirePlugin.keys().forEach((fileName) => {
requirePlugin(fileName);
});

View File

@ -0,0 +1,74 @@
/**
* @author chuzhixin 1204505056@qq.com
* @description 路由守卫目前两种模式all模式与intelligence模式
*/
import router from "@/router";
import store from "@/store";
import getPageTitle from "@/utils/pageTitle";
import {
authentication,
loginInterception,
recordRoute,
routesWhiteList,
} from "@/config/settings";
router.beforeEach(async (to, from, next) => {
let hasToken = store.getters["user/accessToken"];
if (!loginInterception) hasToken = true;
if (hasToken) {
if (to.path === "/login") {
next({ path: "/" });
} else {
const hasRoles =
store.getters["acl/admin"] ||
store.getters["acl/role"].length > 0 ||
store.getters["acl/ability"].length > 0;
if (hasRoles) {
next();
} else {
try {
if (loginInterception) {
await store.dispatch("user/getUserInfo");
} else {
//settings.js loginInterception为false关闭登录拦截时创建虚拟角色
await store.dispatch("user/setVirtualRoles");
}
let accessRoutes = [];
if (authentication === "intelligence") {
accessRoutes = await store.dispatch("routes/setRoutes");
} else if (authentication === "all") {
accessRoutes = await store.dispatch("routes/setAllRoutes");
}
accessRoutes.forEach((item) => {
router.addRoute(item);
});
next({ ...to, replace: true });
} catch {
await store.dispatch("user/resetAll");
if (recordRoute)
next({
path: "/login",
query: { redirect: to.path },
replace: true,
});
else next({ path: "/login", replace: true });
}
}
}
} else {
if (routesWhiteList.indexOf(to.path) !== -1) {
next();
} else {
if (recordRoute)
next({ path: "/login", query: { redirect: to.path }, replace: true });
else next({ path: "/login", replace: true });
}
}
});
router.afterEach((to) => {
document.title = getPageTitle(to.meta.title);
});

379
src/vab/styles/normalize.less vendored Normal file
View File

@ -0,0 +1,379 @@
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
/* Document
========================================================================== */
/**
* 1. Correct the line height in all browsers.
* 2. Prevent adjustments of font size after orientation changes in iOS.
*/
html {
line-height: 1.15;
/* 1 */
-webkit-text-size-adjust: 100%;
/* 2 */
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers.
*/
body {
margin: 0;
}
/**
* Render the `main` element consistently in IE.
*/
main {
display: block;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
margin: 0.67em 0;
font-size: 2em;
}
/* Grouping content
========================================================================== */
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box;
/* 1 */
height: 0;
/* 1 */
overflow: visible;
/* 2 */
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-family: monospace;
/* 1 */
font-size: 1em;
/* 2 */
}
/* Text-level semantics
========================================================================== */
/**
* Remove the gray background on active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* 1. Remove the bottom border in Chrome 57-
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
text-decoration: underline;
/* 2 */
text-decoration: underline dotted;
/* 2 */
border-bottom: none;
/* 1 */
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-family: monospace;
/* 1 */
font-size: 1em;
/* 2 */
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
position: relative;
font-size: 75%;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Remove the border on images inside links in IE 10.
*/
img {
border-style: none;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers.
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
margin: 0;
/* 2 */
font-family: inherit;
/* 1 */
font-size: 100%;
/* 1 */
line-height: 1.15;
/* 1 */
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input {
/* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select {
/* 1 */
text-transform: none;
}
/**
* Correct the inability to style clickable types in iOS and Safari.
*/
button,
[type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
padding: 0;
border-style: none;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Correct the padding in Firefox.
*/
fieldset {
padding: 0.35em 0.75em 0.625em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box;
/* 1 */
display: table;
/* 1 */
max-width: 100%;
/* 1 */
padding: 0;
/* 3 */
color: inherit;
/* 2 */
white-space: normal;
/* 1 */
}
/**
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
vertical-align: baseline;
}
/**
* Remove the default vertical scrollbar in IE 10+.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10.
* 2. Remove the padding in IE 10.
*/
[type="checkbox"],
[type="radio"] {
box-sizing: border-box;
/* 1 */
padding: 0;
/* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type="search"] {
-webkit-appearance: textfield;
/* 1 */
outline-offset: -2px;
/* 2 */
}
/**
* Remove the inner padding in Chrome and Safari on macOS.
*/
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button;
/* 1 */
font: inherit;
/* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in Edge, IE 10+, and Firefox.
*/
details {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/* Misc
========================================================================== */
/**
* Add the correct display in IE 10+.
*/
template {
display: none;
}
/**
* Add the correct display in IE 10.
*/
[hidden] {
display: none;
}

29
src/vab/styles/vab.less Normal file
View File

@ -0,0 +1,29 @@
@import "./normalize.less";
/* 滚动条样式 */
.base-scrollbar {
&::-webkit-scrollbar {
width: 8px;
height: 8px;
}
&::-webkit-scrollbar-thumb {
background-color: rgba(144, 147, 153, 0.3);
border: 3px solid transparent;
border-radius: 7px;
}
&::-webkit-scrollbar-thumb:hover {
background-color: rgba(144, 147, 153, 0.5);
}
}
html {
body {
.base-scrollbar;
* {
box-sizing: border-box;
.base-scrollbar;
}
}
}

212
src/views/401.vue Normal file
View File

@ -0,0 +1,212 @@
<template>
<div class="error-container">
<div class="error-content">
<a-row :gutter="20">
<a-col :lg="12" :md="12" :sm="24" :xl="12" :xs="24">
<div class="pic-error">
<img class="pic-error-parent" src="@/assets/error_images/401.png" />
<img
class="pic-error-child left"
src="@/assets/error_images/cloud.png"
/>
</div>
</a-col>
<a-col :lg="12" :md="12" :sm="24" :xl="12" :xs="24">
<div class="bullshit">
<div class="bullshit-oops">{{ oops }}</div>
<div class="bullshit-headline">{{ headline }}</div>
<div class="bullshit-info">{{ info }}</div>
<a class="bullshit-return-home" href="#/index">
{{ jumpTime }}s&nbsp;{{ btn }}
</a>
</div>
</a-col>
</a-row>
</div>
</div>
</template>
<script>
import { mapActions } from "vuex";
export default {
name: "Page401",
data() {
return {
jumpTime: 5,
oops: "抱歉!",
headline: "您没有操作角色...",
info: "当前帐号没有操作角色,请联系管理员。",
btn: "返回首页",
timer: 0,
};
},
mounted() {
this.timeChange();
},
beforeUnmount() {
clearInterval(this.timer);
},
methods: {
...mapActions({
delVisitedRoute: "tagsBar/delVisitedRoute",
delOthersVisitedRoutes: "tagsBar/delOthersVisitedRoutes",
}),
timeChange() {
this.timer = setInterval(() => {
if (this.jumpTime) {
this.jumpTime--;
} else {
this.$router.push({ path: "/" });
this.delOthersVisitedRoutes({ path: "/" });
clearInterval(this.timer);
}
}, 1000000);
},
},
};
</script>
<style lang="less" scoped>
.error-container {
position: relative;
min-height: 100vh;
.error-content {
position: absolute;
top: 55%;
left: 50%;
width: 40vw;
height: 400px;
transform: translate(-50%, -50%);
.pic-error {
position: relative;
float: left;
width: 100%;
overflow: hidden;
&-parent {
width: 100%;
}
&-child {
position: absolute;
&.left {
top: 17px;
left: 220px;
width: 80px;
opacity: 0;
animation-name: cloudLeft;
animation-duration: 2s;
animation-timing-function: linear;
animation-delay: 1s;
animation-fill-mode: forwards;
}
@keyframes cloudLeft {
0% {
top: 17px;
left: 220px;
opacity: 0;
}
20% {
top: 33px;
left: 188px;
opacity: 1;
}
80% {
top: 81px;
left: 92px;
opacity: 1;
}
100% {
top: 97px;
left: 60px;
opacity: 0;
}
}
}
}
.bullshit {
position: relative;
float: left;
width: 300px;
padding: 30px 0;
overflow: hidden;
&-oops {
margin-bottom: @vab-margin;
font-size: 32px;
font-weight: bold;
line-height: 40px;
opacity: 0;
animation-name: slideUp;
animation-duration: 0.5s;
animation-fill-mode: forwards;
}
&-headline {
margin-bottom: 10px;
font-size: 20px;
font-weight: bold;
line-height: 24px;
color: #222;
opacity: 0;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.1s;
animation-fill-mode: forwards;
}
&-info {
margin-bottom: 30px;
font-size: 13px;
line-height: 21px;
opacity: 0;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.2s;
animation-fill-mode: forwards;
}
&-return-home {
display: block;
float: left;
width: 110px;
height: 36px;
font-size: 14px;
line-height: 36px;
color: #fff;
text-align: center;
cursor: pointer;
background: @vab-color-blue;
border-radius: 100px;
opacity: 0;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.3s;
animation-fill-mode: forwards;
}
@keyframes slideUp {
0% {
opacity: 0;
transform: translateY(60px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
}
}
}
</style>

210
src/views/404.vue Normal file
View File

@ -0,0 +1,210 @@
<template>
<div class="error-container">
<div class="error-content">
<a-row :gutter="20">
<a-col :lg="12" :md="12" :sm="24" :xl="12" :xs="24">
<div class="pic-error">
<img class="pic-error-parent" src="@/assets/error_images/404.png" />
<img
class="pic-error-child left"
src="@/assets/error_images/cloud.png"
/>
</div>
</a-col>
<a-col :lg="12" :md="12" :sm="24" :xl="12" :xs="24">
<div class="bullshit">
<div class="bullshit-oops">{{ oops }}</div>
<div class="bullshit-headline">{{ headline }}</div>
<div class="bullshit-info">{{ info }}</div>
<a class="bullshit-return-home" href="#/index">
{{ jumpTime }}s&nbsp;{{ btn }}
</a>
</div>
</a-col>
</a-row>
</div>
</div>
</template>
<script>
import { mapActions } from "vuex";
export default {
name: "Page404",
data() {
return {
jumpTime: 5,
oops: "抱歉!",
headline: "当前页面不存在...",
info: "请检查您输入的网址是否正确,或点击下面的按钮返回首页。",
btn: "返回首页",
timer: 0,
};
},
mounted() {
this.timeChange();
},
beforeUnmount() {
clearInterval(this.timer);
},
methods: {
...mapActions({
delOthersVisitedRoutes: "tagsBar/delOthersVisitedRoutes",
}),
timeChange() {
this.timer = setInterval(() => {
if (this.jumpTime) {
this.jumpTime--;
} else {
this.$router.push({ path: "/" });
this.delOthersVisitedRoutes({ path: "/" });
clearInterval(this.timer);
}
}, 1000);
},
},
};
</script>
<style lang="less" scoped>
.error-container {
position: relative;
min-height: 100vh;
.error-content {
position: absolute;
top: 55%;
left: 50%;
width: 40vw;
height: 400px;
transform: translate(-50%, -50%);
.pic-error {
position: relative;
float: left;
width: 100%;
overflow: hidden;
&-parent {
width: 100%;
}
&-child {
position: absolute;
&.left {
top: 17px;
left: 220px;
width: 80px;
opacity: 0;
animation-name: cloudLeft;
animation-duration: 2s;
animation-timing-function: linear;
animation-delay: 1s;
animation-fill-mode: forwards;
}
@keyframes cloudLeft {
0% {
top: 17px;
left: 220px;
opacity: 0;
}
20% {
top: 33px;
left: 188px;
opacity: 1;
}
80% {
top: 81px;
left: 92px;
opacity: 1;
}
100% {
top: 97px;
left: 60px;
opacity: 0;
}
}
}
}
.bullshit {
position: relative;
float: left;
width: 300px;
padding: 30px 0;
overflow: hidden;
&-oops {
margin-bottom: @vab-margin;
font-size: 32px;
font-weight: bold;
line-height: 40px;
opacity: 0;
animation-name: slideUp;
animation-duration: 0.5s;
animation-fill-mode: forwards;
}
&-headline {
margin-bottom: 10px;
font-size: 20px;
font-weight: bold;
line-height: 24px;
color: #222;
opacity: 0;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.1s;
animation-fill-mode: forwards;
}
&-info {
margin-bottom: 30px;
font-size: 13px;
line-height: 21px;
opacity: 0;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.2s;
animation-fill-mode: forwards;
}
&-return-home {
display: block;
float: left;
width: 110px;
height: 36px;
font-size: 14px;
line-height: 36px;
color: #fff;
text-align: center;
cursor: pointer;
background: @vab-color-blue;
border-radius: 100px;
opacity: 0;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.3s;
animation-fill-mode: forwards;
}
@keyframes slideUp {
0% {
opacity: 0;
transform: translateY(60px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
}
}
}
</style>

View File

@ -0,0 +1,90 @@
<template>
<div class="index-container">
<h3>
欢迎体验全网首个基于vue3.0开发的vue
admin框架vue-admin-beautiful-minivue3.0的流畅超乎了我们的想象
</h3>
<a-card class="version-information" title="">
<template v-slot:extra>
<a href="#">部署时间:{{ updateTime }}</a>
</template>
<table class="table">
<tr>
<td>vue</td>
<td>{{ dependencies["vue"] }}</td>
<td>@vue/cli</td>
<td>{{ devDependencies["@vue/cli-service"] }}</td>
</tr>
<tr>
<td>vuex</td>
<td>{{ dependencies["vuex"] }}</td>
<td>vue-router</td>
<td>{{ dependencies["vue-router"] }}</td>
</tr>
<tr>
<td>eslint-plugin-vue</td>
<td>{{ devDependencies["eslint-plugin-vue"] }}</td>
<td>axios</td>
<td>{{ dependencies["axios"] }}</td>
</tr>
<tr>
<td>babel-eslint</td>
<td>{{ devDependencies["babel-eslint"] }}</td>
<td>ant-design-vue</td>
<td>{{ dependencies["ant-design-vue"] }}</td>
</tr>
<tr>
<td>PRO版演示地址</td>
<td colspan="3">
<a
href="https://chu1204505056.gitee.io/vue-admin-beautiful-pro/#/index"
>
https://chu1204505056.gitee.io/vue-admin-beautiful-pro/#/index
</a>
</td>
</tr>
<tr>
<td>官方QQ群</td>
<td colspan="3">9724353191139183756</td>
</tr>
</table>
</a-card>
</div>
</template>
<script>
import { dependencies, devDependencies } from "*/package.json";
export default {
data() {
return {
updateTime: process.env.VUE_APP_UPDATE_TIME,
dependencies: dependencies,
devDependencies: devDependencies,
};
},
};
</script>
<style lang="less" scoped>
.version-information {
.table {
width: 100%;
color: #666;
border-collapse: collapse;
background-color: #fff;
td {
position: relative;
padding: 9px 15px;
font-size: 14px;
line-height: 20px;
border: 1px solid #e6e6e6;
&:nth-child(odd) {
width: 20%;
text-align: right;
background-color: #f7f7f7;
}
}
}
}
</style>

13
src/views/index/index.vue Normal file
View File

@ -0,0 +1,13 @@
<template>
<div class="index-container">
<version-information />
</div>
</template>
<script>
import VersionInformation from "./components/VersionInformation";
export default {
name: "Index",
components: { VersionInformation },
};
</script>

86
src/views/login/index.vue Normal file
View File

@ -0,0 +1,86 @@
<template>
<div class="login-container">
<h3>登录页样式未开发点击登录即可进入首页</h3>
<a-form
layout="inline"
:model="form"
@submit="handleSubmit"
@submit.prevent
>
<a-form-item>
<a-input v-model:value="form.username" placeholder="Username">
<template v-slot:prefix>
<UserOutlined style="color: rgba(0, 0, 0, 0.25)" />
</template>
</a-input>
</a-form-item>
<a-form-item>
<a-input
v-model:value="form.password"
type="password"
placeholder="Password"
>
<template v-slot:prefix>
<LockOutlined style="color: rgba(0, 0, 0, 0.25)" />
</template>
</a-input>
</a-form-item>
<a-form-item>
<a-button
type="primary"
html-type="submit"
:disabled="form.username === '' || form.password === ''"
>
登录
</a-button>
</a-form-item>
</a-form>
</div>
</template>
<script>
import { mapActions } from "vuex";
import { UserOutlined, LockOutlined } from "@ant-design/icons-vue";
export default {
name: "Login",
components: {
UserOutlined,
LockOutlined,
},
data() {
return {
form: {
username: "",
password: "",
},
redirect: undefined,
};
},
watch: {
$route: {
handler(route) {
this.redirect = (route.query && route.query.redirect) || "/";
},
immediate: true,
},
},
mounted() {
this.form.username = "admin";
this.form.password = "123456";
},
methods: {
...mapActions({
login: "user/login",
}),
handleRoute() {
return this.redirect === "/404" || this.redirect === "/401"
? "/"
: this.redirect;
},
async handleSubmit() {
await this.login(this.form);
await this.$router.push(this.handleRoute());
},
},
};
</script>

1
vue-admin-beautiful Submodule

@ -0,0 +1 @@
Subproject commit d5e576485fda378976370dc633fb0d9734295d6d

189
vue.config.js Normal file
View File

@ -0,0 +1,189 @@
/**
* @author chuzhixin 1204505056@qq.com
* @description vue.config.js全局配置
*/
const path = require("path");
const {
/* baseURL, */
publicPath,
assetsDir,
outputDir,
lintOnSave,
transpileDependencies,
title,
abbreviation,
devPort,
providePlugin,
build7z,
donation,
} = require("./src/config/settings");
const {
webpackBarName,
webpackBanner,
donationConsole,
} = require("zx-layouts");
if (donation) donationConsole();
const { version, author } = require("./package.json");
const Webpack = require("webpack");
const WebpackBar = require("webpackbar");
const FileManagerPlugin = require("filemanager-webpack-plugin");
const dayjs = require("dayjs");
const date = dayjs().format("YYYY_M_D");
const time = dayjs().format("YYYY-M-D HH:mm:ss");
const CompressionWebpackPlugin = require("compression-webpack-plugin");
const productionGzipExtensions = ["html", "js", "css", "svg"];
process.env.VUE_APP_TITLE = title || "vue-admin-beautiful";
process.env.VUE_APP_AUTHOR = author || "chuzhixin";
process.env.VUE_APP_UPDATE_TIME = time;
process.env.VUE_APP_VERSION = version;
const resolve = (dir) => {
return path.join(__dirname, dir);
};
const mockServer = () => {
if (process.env.NODE_ENV === "development") {
return require("./mock/mockServer.js");
} else {
return "";
}
};
module.exports = {
publicPath,
assetsDir,
outputDir,
lintOnSave,
transpileDependencies,
devServer: {
hot: true,
port: devPort,
open: true,
noInfo: false,
overlay: {
warnings: true,
errors: true,
},
// 注释掉的地方是前端配置代理访问后端的示例
// proxy: {
// [baseURL]: {
// target: `http://你的后端接口地址`,
// ws: true,
// changeOrigin: true,
// pathRewrite: {
// ["^/" + baseURL]: "",
// },
// },
// },
after: mockServer(),
},
configureWebpack() {
return {
resolve: {
alias: {
"@": resolve("src"),
"*": resolve(""),
},
},
plugins: [
new Webpack.ProvidePlugin(providePlugin),
new WebpackBar({
name: webpackBarName,
}),
],
};
},
chainWebpack(config) {
config.resolve.symlinks(true);
config.module.rule("svg").exclude.add(resolve("src/icon/remixIcon")).end();
config.module
.rule("remixIcon")
.test(/\.svg$/)
.include.add(resolve("src/icon/remixIcon"))
.end()
.use("svg-sprite-loader")
.loader("svg-sprite-loader")
.options({ symbolId: "remix-icon-[name]" })
.end();
config.when(process.env.NODE_ENV === "development", (config) => {
config.devtool("source-map");
});
config.when(process.env.NODE_ENV !== "development", (config) => {
config.performance.set("hints", false);
config.devtool("none");
config.optimization.splitChunks({
chunks: "all",
cacheGroups: {
libs: {
name: "vue-admin-beautiful-libs",
test: /[\\/]node_modules[\\/]/,
priority: 10,
chunks: "initial",
},
},
});
config
.plugin("banner")
.use(Webpack.BannerPlugin, [`${webpackBanner}${time}`])
.end();
config
.plugin("compression")
.use(CompressionWebpackPlugin, [
{
filename: "[path].gz[query]",
algorithm: "gzip",
test: new RegExp(
"\\.(" + productionGzipExtensions.join("|") + ")$"
),
threshold: 8192,
minRatio: 0.8,
},
])
.end();
});
if (build7z) {
config.when(process.env.NODE_ENV === "production", (config) => {
config
.plugin("fileManager")
.use(FileManagerPlugin, [
{
onEnd: {
delete: [`./${outputDir}/video`, `./${outputDir}/data`],
archive: [
{
source: `./${outputDir}`,
destination: `./${outputDir}/${abbreviation}_${outputDir}_${date}.7z`,
},
],
},
},
])
.end();
});
}
},
runtimeCompiler: true,
productionSourceMap: false,
css: {
requireModuleExtension: true,
sourceMap: true,
loaderOptions: {
less: {
lessOptions: {
javascriptEnabled: true,
modifyVars: {
"vab-color-blue": "#1890ff",
"vab-margin": "20px",
"vab-padding": "20px",
"vab-header-height": "65px",
},
},
},
},
},
};