mirror of
https://gitee.com/chu1204505056/vue-admin-beautiful.git
synced 2025-04-06 03:58:00 +08:00
基于antdv vue3.0的版本首次提交 感谢vue作者尤雨溪 感谢antdv作者唐金州
This commit is contained in:
parent
d5e576485f
commit
841b4bc494
3
.browserslistrc
Normal file
3
.browserslistrc
Normal file
@ -0,0 +1,3 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
14
.eslintrc.js
Normal file
14
.eslintrc.js
Normal 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
10
.gitattributes
vendored
Normal 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
24
.gitignore
vendored
Normal 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
3
.stylelintrc.js
Normal file
@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: ["stylelint-config-recess-order", "stylelint-config-prettier"],
|
||||
};
|
3
babel.config.js
Normal file
3
babel.config.js
Normal file
@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
presets: ["@vue/cli-plugin-babel/preset"],
|
||||
};
|
16
deploy.sh
Normal file
16
deploy.sh
Normal 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
59
mock/controller/router.js
Normal 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
103
mock/controller/user.js
Normal 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
16
mock/index.js
Normal 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
94
mock/mockServer.js
Normal 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
43
mock/utils/index.js
Normal 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
62
package.json
Normal 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
16
prettier.config.js
Normal 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
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
17
public/index.html
Normal file
17
public/index.html
Normal 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
6
src/App.vue
Normal 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
9
src/api/router.js
Normal 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
43
src/api/user.js
Normal 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",
|
||||
});
|
||||
}
|
BIN
src/assets/error_images/401.png
Normal file
BIN
src/assets/error_images/401.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 147 KiB |
BIN
src/assets/error_images/404.png
Normal file
BIN
src/assets/error_images/404.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 70 KiB |
BIN
src/assets/error_images/cloud.png
Normal file
BIN
src/assets/error_images/cloud.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.0 KiB |
BIN
src/assets/logo.png
Normal file
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
73
src/config/settings.js
Normal file
@ -0,0 +1,73 @@
|
||||
/**
|
||||
* @author chuzhixin 1204505056@qq.com
|
||||
* @description 全局变量配置
|
||||
*/
|
||||
module.exports = {
|
||||
//开发以及部署时的URL,hash模式时在不确定二级目录名称的情况下建议使用""代表相对路径或者"/二级目录/",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
201
src/layout/index.vue
Normal 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>
|
53
src/layout/vab-avatar/index.vue
Normal file
53
src/layout/vab-avatar/index.vue
Normal 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>
|
51
src/layout/vab-content/index.vue
Normal file
51
src/layout/vab-content/index.vue
Normal 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>
|
24
src/layout/vab-icon/index.vue
Normal file
24
src/layout/vab-icon/index.vue
Normal 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>
|
37
src/layout/vab-logo/index.vue
Normal file
37
src/layout/vab-logo/index.vue
Normal 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>
|
42
src/layout/vab-menu/components/MenuItem.vue
Normal file
42
src/layout/vab-menu/components/MenuItem.vue
Normal 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>
|
34
src/layout/vab-menu/components/Submenu.vue
Normal file
34
src/layout/vab-menu/components/Submenu.vue
Normal 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>
|
60
src/layout/vab-menu/index.vue
Normal file
60
src/layout/vab-menu/index.vue
Normal 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>
|
159
src/layout/vab-tabs/index.vue
Normal file
159
src/layout/vab-tabs/index.vue
Normal 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
17
src/main.js
Normal 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
87
src/router/index.js
Normal 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
17
src/store/index.js
Normal 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
33
src/store/modules/acl.js
Normal 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 };
|
60
src/store/modules/routes.js
Normal file
60
src/store/modules/routes.js
Normal 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 };
|
179
src/store/modules/settings.js
Normal file
179
src/store/modules/settings.js
Normal 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 };
|
146
src/store/modules/tagsBar.js
Normal file
146
src/store/modules/tagsBar.js
Normal 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
138
src/store/modules/user.js
Normal 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 重置accessToken、roles、ability、router等
|
||||
* @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
66
src/utils/accessToken.js
Normal 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
51
src/utils/hasRole.js
Normal 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
265
src/utils/index.js
Normal 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
15
src/utils/pageTitle.js
Normal 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
126
src/utils/request.js
Normal 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
77
src/utils/routes.js
Normal 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
52
src/utils/static.js
Normal 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
268
src/utils/validate.js
Normal 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(整数部分为0~180,必须输入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(整数部分为0~90,必须输入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
5
src/vab/index.js
Normal file
@ -0,0 +1,5 @@
|
||||
// 加载插件
|
||||
const requirePlugin = require.context("./plugins", true, /\.js$/);
|
||||
requirePlugin.keys().forEach((fileName) => {
|
||||
requirePlugin(fileName);
|
||||
});
|
74
src/vab/plugins/permissions.js
Normal file
74
src/vab/plugins/permissions.js
Normal 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
379
src/vab/styles/normalize.less
vendored
Normal 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
29
src/vab/styles/vab.less
Normal 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
212
src/views/401.vue
Normal 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 {{ 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
210
src/views/404.vue
Normal 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 {{ 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>
|
90
src/views/index/components/VersionInformation.vue
Normal file
90
src/views/index/components/VersionInformation.vue
Normal file
@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<div class="index-container">
|
||||
<h3>
|
||||
欢迎体验全网首个基于vue3.0开发的vue
|
||||
admin框架vue-admin-beautiful-mini,vue3.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">972435319、1139183756</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
13
src/views/index/index.vue
Normal 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
86
src/views/login/index.vue
Normal 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
1
vue-admin-beautiful
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit d5e576485fda378976370dc633fb0d9734295d6d
|
189
vue.config.js
Normal file
189
vue.config.js
Normal 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",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user