feat: add pure-admin-thin
14
.editorconfig
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# http://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
insert_final_newline = false
|
||||||
|
trim_trailing_whitespace = false
|
14
.env
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# port
|
||||||
|
VITE_PORT = 8848
|
||||||
|
# title
|
||||||
|
VITE_TITLE = vue-pure-admin
|
||||||
|
# version
|
||||||
|
VITE_VERSION = 2.1.0
|
||||||
|
# open
|
||||||
|
VITE_OPEN = false
|
||||||
|
|
||||||
|
# public path
|
||||||
|
VITE_PUBLIC_PATH = /
|
||||||
|
|
||||||
|
# Cross-domain proxy, you can configure multiple
|
||||||
|
VITE_PROXY = [ ["/api", "http://127.0.0.1:3000" ] ]
|
14
.env.development
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# port
|
||||||
|
VITE_PORT = 8848
|
||||||
|
# title
|
||||||
|
VITE_TITLE = vue-pure-admin
|
||||||
|
# version
|
||||||
|
VITE_VERSION = 2.1.0
|
||||||
|
# open
|
||||||
|
VITE_OPEN = false
|
||||||
|
|
||||||
|
# public path
|
||||||
|
VITE_PUBLIC_PATH = /
|
||||||
|
|
||||||
|
# Cross-domain proxy, you can configure multiple
|
||||||
|
VITE_PROXY = [ ["/api", "http://127.0.0.1:3000" ] ]
|
2
.env.production
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# public path
|
||||||
|
VITE_PUBLIC_PATH = /manages/
|
4
.eslintignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
public
|
||||||
|
dist
|
||||||
|
*.d.ts
|
||||||
|
package.json
|
75
.eslintrc.js
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
node: true
|
||||||
|
},
|
||||||
|
globals: {
|
||||||
|
// Ref sugar (take 2)
|
||||||
|
$: "readonly",
|
||||||
|
$$: "readonly",
|
||||||
|
$ref: "readonly",
|
||||||
|
$shallowRef: "readonly",
|
||||||
|
$computed: "readonly",
|
||||||
|
|
||||||
|
// index.d.ts
|
||||||
|
// global.d.ts
|
||||||
|
Fn: "readonly",
|
||||||
|
PromiseFn: "readonly",
|
||||||
|
RefType: "readonly",
|
||||||
|
LabelValueOptions: "readonly",
|
||||||
|
EmitType: "readonly",
|
||||||
|
TargetContext: "readonly",
|
||||||
|
ComponentElRef: "readonly",
|
||||||
|
ComponentRef: "readonly",
|
||||||
|
ElRef: "readonly",
|
||||||
|
global: "readonly",
|
||||||
|
ForDataType: "readonly",
|
||||||
|
ComponentRoutes: "readonly",
|
||||||
|
|
||||||
|
// script setup
|
||||||
|
defineProps: "readonly",
|
||||||
|
defineEmits: "readonly",
|
||||||
|
defineExpose: "readonly",
|
||||||
|
withDefaults: "readonly"
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
"plugin:vue/vue3-essential",
|
||||||
|
"eslint:recommended",
|
||||||
|
"@vue/typescript/recommended",
|
||||||
|
"@vue/prettier",
|
||||||
|
"@vue/prettier/@typescript-eslint"
|
||||||
|
],
|
||||||
|
parser: "vue-eslint-parser",
|
||||||
|
parserOptions: {
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
sourceType: "module",
|
||||||
|
jsxPragma: "React",
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/no-explicit-any": "off", // any
|
||||||
|
"no-debugger": "off",
|
||||||
|
"@typescript-eslint/explicit-module-boundary-types": "off", // setup()
|
||||||
|
"@typescript-eslint/ban-types": "off",
|
||||||
|
"@typescript-eslint/ban-ts-comment": "off",
|
||||||
|
"@typescript-eslint/no-empty-function": "off",
|
||||||
|
"@typescript-eslint/no-non-null-assertion": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
argsIgnorePattern: "^_",
|
||||||
|
varsIgnorePattern: "^_"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
argsIgnorePattern: "^_",
|
||||||
|
varsIgnorePattern: "^_"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
18
.gitignore
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
tests/**/coverage/
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
6
.husky/commit-msg
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# shellcheck source=./_/husky.sh
|
||||||
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
npx --no-install commitlint --edit "$1"
|
9
.husky/common.sh
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
command_exists () {
|
||||||
|
command -v "$1" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Workaround for Windows 10, Git Bash and Yarn
|
||||||
|
if command_exists winpty && test -t 1; then
|
||||||
|
exec < /dev/tty
|
||||||
|
fi
|
10
.husky/lintstagedrc.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
module.exports = {
|
||||||
|
"*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"],
|
||||||
|
"{!(package)*.json,*.code-snippets,.!(browserslist)*rc}": [
|
||||||
|
"prettier --write--parser json"
|
||||||
|
],
|
||||||
|
"package.json": ["prettier --write"],
|
||||||
|
"*.vue": ["eslint --fix", "prettier --write", "stylelint --fix"],
|
||||||
|
"*.{vue,css,scss,postcss,less}": ["stylelint --fix", "prettier --write"],
|
||||||
|
"*.md": ["prettier --write"]
|
||||||
|
};
|
10
.husky/pre-commit
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
. "$(dirname "$0")/common.sh"
|
||||||
|
|
||||||
|
[ -n "$CI" ] && exit 0
|
||||||
|
|
||||||
|
# Format and submit code according to lintstagedrc.js configuration
|
||||||
|
npm run lint:lint-staged
|
||||||
|
|
||||||
|
npm run lint:pretty
|
7
.prettierrc.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
module.exports = {
|
||||||
|
bracketSpacing: true,
|
||||||
|
jsxBracketSameLine: true,
|
||||||
|
singleQuote: false,
|
||||||
|
arrowParens: "avoid",
|
||||||
|
trailingComma: "none"
|
||||||
|
};
|
3
.stylelintignore
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
/dist/*
|
||||||
|
/public/*
|
||||||
|
public/*
|
1
README.en-US.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
<h1>vue-pure-admin精简版</h1>
|
5
api/routes.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { http } from "/@/utils/http";
|
||||||
|
|
||||||
|
export const getAsyncRoutes = (data?: object) => {
|
||||||
|
return http.request("get", "/getAsyncRoutes", data);
|
||||||
|
};
|
6
babel.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
const productPlugins = [];
|
||||||
|
process.env.NODE_ENV === "production" &&
|
||||||
|
productPlugins.push("transform-remove-console");
|
||||||
|
module.exports = {
|
||||||
|
plugins: [...productPlugins]
|
||||||
|
};
|
19
build/proxy.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
type ProxyItem = [string, string];
|
||||||
|
|
||||||
|
type ProxyList = ProxyItem[];
|
||||||
|
|
||||||
|
const regExps = (value: string, reg: string): string => {
|
||||||
|
return value.replace(new RegExp(reg, "g"), "");
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createProxy(list: ProxyList = []) {
|
||||||
|
const ret: any = {};
|
||||||
|
for (const [prefix, target] of list) {
|
||||||
|
ret[prefix] = {
|
||||||
|
target: target,
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: (path: string) => regExps(path, prefix)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
32
build/utils.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
const warpperEnv = (envConf: Recordable): ViteEnv => {
|
||||||
|
const ret: any = {};
|
||||||
|
|
||||||
|
for (const envName of Object.keys(envConf)) {
|
||||||
|
let realName = envConf[envName].replace(/\\n/g, "\n");
|
||||||
|
realName =
|
||||||
|
realName === "true" ? true : realName === "false" ? false : realName;
|
||||||
|
|
||||||
|
if (envName === "VITE_PORT") {
|
||||||
|
realName = Number(realName);
|
||||||
|
}
|
||||||
|
if (envName === "VITE_PROXY" && realName) {
|
||||||
|
try {
|
||||||
|
realName = JSON.parse(realName.replace(/'/g, '"'));
|
||||||
|
} catch (error) {
|
||||||
|
realName = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ret[envName] = realName;
|
||||||
|
if (typeof realName === "string") {
|
||||||
|
process.env[envName] = realName;
|
||||||
|
} else if (typeof realName === "object") {
|
||||||
|
process.env[envName] = JSON.stringify(realName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
const loadEnv = (): ViteEnv => {
|
||||||
|
return import.meta.env;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { loadEnv, warpperEnv };
|
32
commitlint.config.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
module.exports = {
|
||||||
|
ignores: [commit => commit.includes("init")],
|
||||||
|
extends: ["@commitlint/config-conventional"],
|
||||||
|
rules: {
|
||||||
|
"body-leading-blank": [2, "always"],
|
||||||
|
"footer-leading-blank": [1, "always"],
|
||||||
|
"header-max-length": [2, "always", 108],
|
||||||
|
"subject-empty": [2, "never"],
|
||||||
|
"type-empty": [2, "never"],
|
||||||
|
"type-enum": [
|
||||||
|
2,
|
||||||
|
"always",
|
||||||
|
[
|
||||||
|
"feat",
|
||||||
|
"fix",
|
||||||
|
"perf",
|
||||||
|
"style",
|
||||||
|
"docs",
|
||||||
|
"test",
|
||||||
|
"refactor",
|
||||||
|
"build",
|
||||||
|
"ci",
|
||||||
|
"chore",
|
||||||
|
"revert",
|
||||||
|
"wip",
|
||||||
|
"workflow",
|
||||||
|
"types",
|
||||||
|
"release"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
84
index.html
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<link rel="stylesheet" href="/iconfont.css" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Pure-Admin-Thin</title>
|
||||||
|
<script>
|
||||||
|
window.process = {};
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app">
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background: #000;
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: "Reggae One", cursive;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 8vw;
|
||||||
|
overflow: hidden;
|
||||||
|
-webkit-text-stroke: 3px #7272a5;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
display: block;
|
||||||
|
font-size: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
color: green;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
p::before {
|
||||||
|
content: " ";
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
background-image: linear-gradient(45deg, #ff269b, #2ab5f5, #ffbf00);
|
||||||
|
mix-blend-mode: multiply;
|
||||||
|
}
|
||||||
|
|
||||||
|
p::after {
|
||||||
|
content: "";
|
||||||
|
background: radial-gradient(circle, #fff, #000 50%);
|
||||||
|
background-size: 25% 25%;
|
||||||
|
position: absolute;
|
||||||
|
top: -100%;
|
||||||
|
left: -100%;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
mix-blend-mode: color-dodge;
|
||||||
|
animation: mix 2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mix {
|
||||||
|
to {
|
||||||
|
transform: translate(50%, 50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="g-container">
|
||||||
|
<p>Pure-Admin</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
90
mock/asyncRoutes.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
// 根据角色动态生成路由
|
||||||
|
import { MockMethod } from "vite-plugin-mock";
|
||||||
|
|
||||||
|
// http://mockjs.com/examples.html#Object
|
||||||
|
const systemRouter = {
|
||||||
|
path: "/system",
|
||||||
|
name: "system",
|
||||||
|
redirect: "/system/user",
|
||||||
|
meta: {
|
||||||
|
icon: "el-icon-setting",
|
||||||
|
title: "message.hssysManagement",
|
||||||
|
showLink: true,
|
||||||
|
rank: 6
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "/system/user",
|
||||||
|
name: "user",
|
||||||
|
meta: {
|
||||||
|
title: "message.hsBaseinfo",
|
||||||
|
showLink: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/system/dict",
|
||||||
|
name: "dict",
|
||||||
|
meta: {
|
||||||
|
title: "message.hsDict",
|
||||||
|
showLink: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const permissionRouter = {
|
||||||
|
path: "/permission",
|
||||||
|
name: "permission",
|
||||||
|
redirect: "/permission/page",
|
||||||
|
meta: {
|
||||||
|
title: "message.permission",
|
||||||
|
icon: "el-icon-lollipop",
|
||||||
|
showLink: true,
|
||||||
|
rank: 3
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "/permission/page",
|
||||||
|
name: "permissionPage",
|
||||||
|
meta: {
|
||||||
|
title: "message.permissionPage",
|
||||||
|
showLink: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/permission/button",
|
||||||
|
name: "permissionButton",
|
||||||
|
meta: {
|
||||||
|
title: "message.permissionButton",
|
||||||
|
showLink: true,
|
||||||
|
authority: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加不同按钮权限到/permission/button页面中
|
||||||
|
function setDifAuthority(authority, routes) {
|
||||||
|
routes.children[1].meta.authority = [authority];
|
||||||
|
return routes;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
url: "/getAsyncRoutes",
|
||||||
|
method: "get",
|
||||||
|
response: ({ query }) => {
|
||||||
|
if (query.name === "admin") {
|
||||||
|
return {
|
||||||
|
code: 0,
|
||||||
|
info: [systemRouter, setDifAuthority("v-admin", permissionRouter)]
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
code: 0,
|
||||||
|
info: [setDifAuthority("v-test", permissionRouter)]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
] as MockMethod[];
|
89
package.json
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
{
|
||||||
|
"name": "vue-pure-admin",
|
||||||
|
"version": "2.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "cross-env --max_old_space_size=4096 vite",
|
||||||
|
"serve": "yarn dev",
|
||||||
|
"build": "rimraf dist && cross-env vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"preview:build": "yarn build && vite preview",
|
||||||
|
"clean:cache": "rm -rf node_modules && rm -rf .eslintcache && yarn cache clean && yarn",
|
||||||
|
"lint:eslint": "eslint --cache --max-warnings 0 \"{src,mock}/**/*.{vue,ts,tsx}\" --fix",
|
||||||
|
"lint:prettier": "prettier --write \"src/**/*.{js,json,tsx,css,less,scss,vue,html,md}\"",
|
||||||
|
"lint:stylelint": "stylelint --cache --fix \"**/*.{vue,css,scss,postcss,less}\" --cache --cache-location node_modules/.cache/stylelint/",
|
||||||
|
"lint:lint-staged": "lint-staged -c ./.husky/lintstagedrc.js",
|
||||||
|
"lint:pretty": "pretty-quick --staged",
|
||||||
|
"lint": "yarn lint:eslint && yarn lint:prettier && yarn lint:stylelint && yarn lint:pretty",
|
||||||
|
"prepare": "husky install"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@vueuse/core": "^6.5.3",
|
||||||
|
"animate.css": "^4.1.1",
|
||||||
|
"await-to-js": "^3.0.0",
|
||||||
|
"axios": "^0.21.1",
|
||||||
|
"dayjs": "^1.10.7",
|
||||||
|
"element-plus": "1.1.0-beta.20",
|
||||||
|
"element-resize-detector": "^1.2.3",
|
||||||
|
"font-awesome": "^4.7.0",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
|
"lowdb": "^3.0.0",
|
||||||
|
"mitt": "^3.0.0",
|
||||||
|
"mockjs": "^1.1.0",
|
||||||
|
"nprogress": "^0.2.0",
|
||||||
|
"path": "^0.12.7",
|
||||||
|
"path-to-regexp": "^6.2.0",
|
||||||
|
"pinia": "2.0.0-rc.10",
|
||||||
|
"resize-observer-polyfill": "^1.5.1",
|
||||||
|
"responsive-storage": "^1.0.11",
|
||||||
|
"typescript-cookie": "^1.0.0",
|
||||||
|
"vue": "^3.2.20",
|
||||||
|
"vue-i18n": "^9.2.0-beta.3",
|
||||||
|
"vue-router": "^4.0.11",
|
||||||
|
"vue-types": "^4.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@commitlint/cli": "^13.1.0",
|
||||||
|
"@commitlint/config-conventional": "^13.1.0",
|
||||||
|
"@types/element-resize-detector": "^1.1.3",
|
||||||
|
"@types/mockjs": "^1.0.3",
|
||||||
|
"@types/node": "^14.14.14",
|
||||||
|
"@types/nprogress": "^0.2.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^4.31.0",
|
||||||
|
"@typescript-eslint/parser": "^4.31.0",
|
||||||
|
"@vitejs/plugin-vue": "^1.6.0",
|
||||||
|
"@vitejs/plugin-vue-jsx": "^1.1.7",
|
||||||
|
"@vue/compiler-sfc": "^3.2.20",
|
||||||
|
"@vue/eslint-config-prettier": "^6.0.0",
|
||||||
|
"@vue/eslint-config-typescript": "^7.0.0",
|
||||||
|
"autoprefixer": "^10.2.4",
|
||||||
|
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||||
|
"chalk": "^2.4.2",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"eslint": "^7.30.0",
|
||||||
|
"eslint-plugin-prettier": "^3.4.0",
|
||||||
|
"eslint-plugin-vue": "^7.17.0",
|
||||||
|
"husky": "^7.0.2",
|
||||||
|
"lint-staged": "^11.1.2",
|
||||||
|
"postcss": "^8.2.6",
|
||||||
|
"postcss-import": "^14.0.0",
|
||||||
|
"prettier": "^2.3.2",
|
||||||
|
"pretty-quick": "^3.1.1",
|
||||||
|
"rimraf": "^3.0.2",
|
||||||
|
"sass": "^1.38.0",
|
||||||
|
"sass-loader": "^12.1.0",
|
||||||
|
"stylelint": "^13.13.1",
|
||||||
|
"stylelint-config-prettier": "^8.0.2",
|
||||||
|
"stylelint-config-standard": "^22.0.0",
|
||||||
|
"stylelint-order": "^4.1.0",
|
||||||
|
"typescript": "^4.4.2",
|
||||||
|
"unplugin-element-plus": "^0.1.0",
|
||||||
|
"vite": "^2.6.7",
|
||||||
|
"vite-plugin-mock": "^2.9.6",
|
||||||
|
"vite-svg-loader": "^2.2.0",
|
||||||
|
"vue-eslint-parser": "^7.10.0"
|
||||||
|
},
|
||||||
|
"repository": "git@github.com:xiaoxian521/vue-pure-admin.git",
|
||||||
|
"author": "xiaoxian521",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
3
postcss.config.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: [require("autoprefixer"), require("postcss-import")]
|
||||||
|
};
|
BIN
public/favicon.ico
Normal file
After Width: | Height: | Size: 66 KiB |
18
public/iconfont.css
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: "iconfont"; /* project id 1098500 */
|
||||||
|
src: url("//at.alicdn.com/t/font_1098500_3d6un9zwltz.eot");
|
||||||
|
src: url("//at.alicdn.com/t/font_1098500_3d6un9zwltz.eot?#iefix")
|
||||||
|
format("embedded-opentype"),
|
||||||
|
url("//at.alicdn.com/t/font_1098500_3d6un9zwltz.woff2") format("woff2"),
|
||||||
|
url("//at.alicdn.com/t/font_1098500_3d6un9zwltz.woff") format("woff"),
|
||||||
|
url("//at.alicdn.com/t/font_1098500_3d6un9zwltz.ttf") format("truetype"),
|
||||||
|
url("//at.alicdn.com/t/font_1098500_3d6un9zwltz.svg#iconfont") format("svg");
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconfont {
|
||||||
|
font-family: "iconfont" !important;
|
||||||
|
font-size: 16px;
|
||||||
|
font-style: normal;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
9
public/serverConfig.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"Version": "2.1.0",
|
||||||
|
"Title": "PureAdmin",
|
||||||
|
"FixedHeader": false,
|
||||||
|
"HiddenSideBar": false,
|
||||||
|
"KeepAlive": true,
|
||||||
|
"Locale": "zh",
|
||||||
|
"Layout": "vertical-dark"
|
||||||
|
}
|
28
src/App.vue
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<template>
|
||||||
|
<el-config-provider :locale="currentLocale">
|
||||||
|
<router-view />
|
||||||
|
</el-config-provider>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { ElConfigProvider } from "element-plus";
|
||||||
|
import zhCn from "element-plus/lib/locale/lang/zh-cn";
|
||||||
|
import en from "element-plus/lib/locale/lang/en";
|
||||||
|
export default {
|
||||||
|
name: "app",
|
||||||
|
components: {
|
||||||
|
[ElConfigProvider.name]: ElConfigProvider
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
// eslint-disable-next-line vue/return-in-computed-property
|
||||||
|
currentLocale() {
|
||||||
|
switch (this.$storage.locale?.locale) {
|
||||||
|
case "zh":
|
||||||
|
return zhCn;
|
||||||
|
case "en":
|
||||||
|
return en;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
BIN
src/assets/401.gif
Normal file
After Width: | Height: | Size: 160 KiB |
BIN
src/assets/404.png
Normal file
After Width: | Height: | Size: 96 KiB |
BIN
src/assets/404_cloud.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
src/assets/bg.png
Normal file
After Width: | Height: | Size: 1.1 MiB |
50
src/assets/iconfont/iconfont.css
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: "iconfont"; /* Project id 2208059 */
|
||||||
|
src: url("iconfont.woff2?t=1634092870259") format("woff2"),
|
||||||
|
url("iconfont.woff?t=1634092870259") format("woff"),
|
||||||
|
url("iconfont.ttf?t=1634092870259") format("truetype");
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconfont {
|
||||||
|
font-family: "iconfont" !important;
|
||||||
|
font-size: 16px;
|
||||||
|
font-style: normal;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-iconzuixinlianzai::before {
|
||||||
|
content: "\e6da";
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-iconxinpin::before {
|
||||||
|
content: "\e614";
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-iconxinpinrenqiwang::before {
|
||||||
|
content: "\e615";
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-iconinternationality::before {
|
||||||
|
content: "\e67a";
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-iconshanchu::before {
|
||||||
|
content: "\e617";
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-iconshow-main-container::before {
|
||||||
|
content: "\e878";
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-iconhidden-main-container::before {
|
||||||
|
content: "\e881";
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-iconexit-fullscreen::before {
|
||||||
|
content: "\e62a";
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-iconfullscreen::before {
|
||||||
|
content: "\e62b";
|
||||||
|
}
|
64
src/assets/iconfont/iconfont.js
Normal file
72
src/assets/iconfont/iconfont.json
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
{
|
||||||
|
"id": "2208059",
|
||||||
|
"name": "pure-admin",
|
||||||
|
"font_family": "iconfont",
|
||||||
|
"css_prefix_text": "team-icon",
|
||||||
|
"description": "pure-admin",
|
||||||
|
"glyphs": [
|
||||||
|
{
|
||||||
|
"icon_id": "2508809",
|
||||||
|
"name": "最新连载",
|
||||||
|
"font_class": "zuixinlianzai",
|
||||||
|
"unicode": "e6da",
|
||||||
|
"unicode_decimal": 59098
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "7795613",
|
||||||
|
"name": "新品",
|
||||||
|
"font_class": "xinpin",
|
||||||
|
"unicode": "e614",
|
||||||
|
"unicode_decimal": 58900
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "7795615",
|
||||||
|
"name": "新品人气王",
|
||||||
|
"font_class": "xinpinrenqiwang",
|
||||||
|
"unicode": "e615",
|
||||||
|
"unicode_decimal": 58901
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "18367956",
|
||||||
|
"name": "中英文2 中文",
|
||||||
|
"font_class": "internationality",
|
||||||
|
"unicode": "e67a",
|
||||||
|
"unicode_decimal": 59002
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "6184565",
|
||||||
|
"name": "删除",
|
||||||
|
"font_class": "shanchu",
|
||||||
|
"unicode": "e617",
|
||||||
|
"unicode_decimal": 58903
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "9626913",
|
||||||
|
"name": "全屏",
|
||||||
|
"font_class": "show-main-container",
|
||||||
|
"unicode": "e878",
|
||||||
|
"unicode_decimal": 59512
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "9626952",
|
||||||
|
"name": "退出全屏",
|
||||||
|
"font_class": "hidden-main-container",
|
||||||
|
"unicode": "e881",
|
||||||
|
"unicode_decimal": 59521
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "5698509",
|
||||||
|
"name": "全屏缩小",
|
||||||
|
"font_class": "exit-fullscreen",
|
||||||
|
"unicode": "e62a",
|
||||||
|
"unicode_decimal": 58922
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "5698510",
|
||||||
|
"name": "全屏显示",
|
||||||
|
"font_class": "fullscreen",
|
||||||
|
"unicode": "e62b",
|
||||||
|
"unicode_decimal": 58923
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
BIN
src/assets/iconfont/iconfont.ttf
Normal file
BIN
src/assets/iconfont/iconfont.woff
Normal file
BIN
src/assets/iconfont/iconfont.woff2
Normal file
BIN
src/assets/login.png
Normal file
After Width: | Height: | Size: 9.9 KiB |
1
src/assets/svg/close.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 36 36"><path d="M19.41 18l8.29-8.29a1 1 0 0 0-1.41-1.41L18 16.59l-8.29-8.3a1 1 0 0 0-1.42 1.42l8.3 8.29l-8.3 8.29A1 1 0 1 0 9.7 27.7l8.3-8.29l8.29 8.29a1 1 0 0 0 1.41-1.41z" fill="currentColor"></path></svg>
|
After Width: | Height: | Size: 395 B |
1
src/assets/svg/close_all.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 36 36"><path d="M26 17H10a1 1 0 0 0 0 2h16a1 1 0 0 0 0-2z" fill="currentColor"></path></svg>
|
After Width: | Height: | Size: 279 B |
1
src/assets/svg/close_left.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><g fill="none"><path d="M7 12l7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path><path d="M7 12l7-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path><path d="M21 12H7.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" ></path><path d="M3 3v18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></g></svg>
|
After Width: | Height: | Size: 647 B |
1
src/assets/svg/close_other.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 20 20"><path d="M3 5h14V3H3v2zm12 8V7H5v6h10zM3 17h14v-2H3v2z" fill="currentColor"></path></svg>
|
After Width: | Height: | Size: 284 B |
1
src/assets/svg/close_right.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><g transform="translate(24 0) scale(-1 1)"><g fill="none"><path d="M7 12l7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path><path d="M7 12l7-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path><path d="M21 12H7.5" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path><path d="M3 3v18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></g></g></svg>
|
After Width: | Height: | Size: 693 B |
1
src/assets/svg/exit_screen.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" class="re-screen" preserveAspectRatio="xMidYMid meet" viewBox="0 0 16 16"><g fill="currentColor"><path d="M3.5 4H1V3h2V1h1v2.5l-.5.5zM13 3V1h-1v2.5l.5.5H15V3h-2zm-1 9.5V15h1v-2h2v-1h-2.5l-.5.5zM1 12v1h2v2h1v-2.5l-.5-.5H1zm11-1.5l-.5.5h-7l-.5-.5v-5l.5-.5h7l.5.5v5zM10 7H6v2h4V7z"></path></g></svg>
|
After Width: | Height: | Size: 434 B |
1
src/assets/svg/full_screen.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" class="re-screen" preserveAspectRatio="xMidYMid meet" viewBox="0 0 16 16"><g fill="currentColor"><path d="M3 12h10V4H3v8zm2-6h6v4H5V6zM2 6H1V2.5l.5-.5H5v1H2v3zm13-3.5V6h-1V3h-3V2h3.5l.5.5zM14 10h1v3.5l-.5.5H11v-1h3v-3zM2 13h3v1H1.5l-.5-.5V10h1v3z"></path></g></svg>
|
After Width: | Height: | Size: 403 B |
1
src/assets/svg/globalization.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="globalization" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 512 512"><path d="M478.33 433.6l-90-218a22 22 0 0 0-40.67 0l-90 218a22 22 0 1 0 40.67 16.79L316.66 406h102.67l18.33 44.39A22 22 0 0 0 458 464a22 22 0 0 0 20.32-30.4zM334.83 362L368 281.65L401.17 362z" fill="currentColor"></path><path d="M267.84 342.92a22 22 0 0 0-4.89-30.7c-.2-.15-15-11.13-36.49-34.73c39.65-53.68 62.11-114.75 71.27-143.49H330a22 22 0 0 0 0-44H214V70a22 22 0 0 0-44 0v20H54a22 22 0 0 0 0 44h197.25c-9.52 26.95-27.05 69.5-53.79 108.36c-31.41-41.68-43.08-68.65-43.17-68.87a22 22 0 0 0-40.58 17c.58 1.38 14.55 34.23 52.86 83.93c.92 1.19 1.83 2.35 2.74 3.51c-39.24 44.35-77.74 71.86-93.85 80.74a22 22 0 1 0 21.07 38.63c2.16-1.18 48.6-26.89 101.63-85.59c22.52 24.08 38 35.44 38.93 36.1a22 22 0 0 0 30.75-4.9z" fill="currentColor"></path></svg>
|
After Width: | Height: | Size: 965 B |
1
src/assets/svg/refresh.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 512 512"><path d="M400 148l-21.12-24.57A191.43 191.43 0 0 0 240 64C134 64 48 150 48 256s86 192 192 192a192.09 192.09 0 0 0 181.07-128" fill="none" stroke="currentColor" stroke-linecap="square" stroke-miterlimit="10" stroke-width="32"></path><path d="M464 68.45V220a4 4 0 0 1-4 4H308.45a4 4 0 0 1-2.83-6.83L457.17 65.62a4 4 0 0 1 6.83 2.83z" fill="currentColor"></path></svg>
|
After Width: | Height: | Size: 561 B |
12
src/components/ReIcon/index.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { App } from "vue";
|
||||||
|
import icon from "./src/Icon.vue";
|
||||||
|
|
||||||
|
export const Icon = Object.assign(icon, {
|
||||||
|
install(app: App) {
|
||||||
|
app.component(icon.name, icon);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default {
|
||||||
|
Icon
|
||||||
|
};
|
97
src/components/ReIcon/src/Icon.vue
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export default {
|
||||||
|
name: "Icon"
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
content: {
|
||||||
|
type: String,
|
||||||
|
default: ""
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: Number,
|
||||||
|
default: 18
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
type: Number,
|
||||||
|
default: 20
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: Number,
|
||||||
|
default: 20
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: ""
|
||||||
|
},
|
||||||
|
svg: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "click"): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
let text = ref("");
|
||||||
|
|
||||||
|
let className = computed(() => {
|
||||||
|
if (props.content.indexOf("fa-") > -1) {
|
||||||
|
return props.content.indexOf("fa ") === 0
|
||||||
|
? props.content
|
||||||
|
: ["fa", props.content];
|
||||||
|
} else if (props.content.indexOf("el-icon-") > -1) {
|
||||||
|
return props.content;
|
||||||
|
} else if (props.content.indexOf("#") > -1) {
|
||||||
|
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
|
||||||
|
text.value = props.content;
|
||||||
|
return "iconfont";
|
||||||
|
} else {
|
||||||
|
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
|
||||||
|
text.value = props.content;
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let iconStyle = computed(() => {
|
||||||
|
return (
|
||||||
|
"font-size: " +
|
||||||
|
props.size +
|
||||||
|
"px; color: " +
|
||||||
|
props.color +
|
||||||
|
"; width: " +
|
||||||
|
props.width +
|
||||||
|
"px; height: " +
|
||||||
|
props.height +
|
||||||
|
"px; font-style: normal;"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const clickHandle = () => {
|
||||||
|
emit("click");
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<i
|
||||||
|
v-if="!props.svg"
|
||||||
|
:class="className"
|
||||||
|
:style="iconStyle"
|
||||||
|
v-html="text"
|
||||||
|
@click="clickHandle"
|
||||||
|
></i>
|
||||||
|
<svg
|
||||||
|
class="icon-svg"
|
||||||
|
v-if="props.svg"
|
||||||
|
aria-hidden="true"
|
||||||
|
:style="iconStyle"
|
||||||
|
@click="clickHandle"
|
||||||
|
>
|
||||||
|
<use :xlink:href="`#${props.content}`" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
195
src/components/ReInfo/index.vue
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, PropType, getCurrentInstance, watch, nextTick, toRef } from "vue";
|
||||||
|
import { useRouter, useRoute } from "vue-router";
|
||||||
|
import { initRouter } from "/@/router";
|
||||||
|
import { storageSession } from "/@/utils/storage";
|
||||||
|
|
||||||
|
export interface ContextProps {
|
||||||
|
userName: string;
|
||||||
|
passWord: string;
|
||||||
|
verify: number | null;
|
||||||
|
svg: any;
|
||||||
|
telephone?: number;
|
||||||
|
dynamicText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
ruleForm: {
|
||||||
|
type: Object as PropType<ContextProps>
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "onBehavior", evt: Object): void;
|
||||||
|
(e: "refreshVerify"): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const instance = getCurrentInstance();
|
||||||
|
|
||||||
|
const model = toRef(props, "ruleForm");
|
||||||
|
let tips = ref<string>("注册");
|
||||||
|
let tipsFalse = ref<string>("登录");
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
watch(
|
||||||
|
route,
|
||||||
|
async ({ path }): Promise<void> => {
|
||||||
|
await nextTick();
|
||||||
|
path.includes("register")
|
||||||
|
? (tips.value = "登录") && (tipsFalse.value = "注册")
|
||||||
|
: (tips.value = "注册") && (tipsFalse.value = "登录");
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const rules: Object = ref({
|
||||||
|
userName: [{ required: true, message: "请输入用户名", trigger: "blur" }],
|
||||||
|
passWord: [
|
||||||
|
{ required: true, message: "请输入密码", trigger: "blur" },
|
||||||
|
{ min: 6, message: "密码长度必须不小于6位", trigger: "blur" }
|
||||||
|
],
|
||||||
|
verify: [
|
||||||
|
{ required: true, message: "请输入验证码", trigger: "blur" },
|
||||||
|
{ type: "number", message: "验证码必须是数字类型", trigger: "blur" }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// 点击登录或注册
|
||||||
|
const onBehavior = (evt: Object): void => {
|
||||||
|
// @ts-expect-error
|
||||||
|
instance.refs.ruleForm.validate((valid: boolean) => {
|
||||||
|
if (valid) {
|
||||||
|
emit("onBehavior", evt);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 刷新验证码
|
||||||
|
const refreshVerify = (): void => {
|
||||||
|
emit("refreshVerify");
|
||||||
|
};
|
||||||
|
|
||||||
|
// 表单重置
|
||||||
|
const resetForm = (): void => {
|
||||||
|
// @ts-expect-error
|
||||||
|
instance.refs.ruleForm.resetFields();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 登录、注册页面切换
|
||||||
|
const changPage = (): void => {
|
||||||
|
tips.value === "注册" ? router.push("/register") : router.push("/login");
|
||||||
|
};
|
||||||
|
|
||||||
|
const noSecret = (): void => {
|
||||||
|
storageSession.setItem("info", {
|
||||||
|
username: "admin",
|
||||||
|
accessToken: "eyJhbGciOiJIUzUxMiJ9.test"
|
||||||
|
});
|
||||||
|
initRouter("admin").then(() => {});
|
||||||
|
router.push("/");
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="info">
|
||||||
|
<el-form :model="model" :rules="rules" ref="ruleForm" class="rule-form">
|
||||||
|
<el-form-item prop="userName">
|
||||||
|
<el-input
|
||||||
|
clearable
|
||||||
|
v-model="model.userName"
|
||||||
|
placeholder="请输入用户名"
|
||||||
|
prefix-icon="el-icon-user"
|
||||||
|
></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="passWord">
|
||||||
|
<el-input
|
||||||
|
clearable
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
v-model="model.passWord"
|
||||||
|
placeholder="请输入密码"
|
||||||
|
prefix-icon="el-icon-lock"
|
||||||
|
></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="verify">
|
||||||
|
<el-input
|
||||||
|
maxlength="2"
|
||||||
|
onkeyup="this.value=this.value.replace(/[^\d.]/g,'');"
|
||||||
|
v-model.number="model.verify"
|
||||||
|
placeholder="请输入验证码"
|
||||||
|
></el-input>
|
||||||
|
<span
|
||||||
|
class="verify"
|
||||||
|
title="刷新"
|
||||||
|
v-html="model.svg"
|
||||||
|
@click.prevent="refreshVerify"
|
||||||
|
></span>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click.prevent="onBehavior">{{
|
||||||
|
tipsFalse
|
||||||
|
}}</el-button>
|
||||||
|
<el-button @click="resetForm">重置</el-button>
|
||||||
|
<span class="tips" @click="changPage">{{ tips }}</span>
|
||||||
|
</el-form-item>
|
||||||
|
<span title="测试用户 直接登录" class="secret" @click="noSecret"
|
||||||
|
>免密登录</span
|
||||||
|
>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.info {
|
||||||
|
width: 30vw;
|
||||||
|
height: 48vh;
|
||||||
|
background: url("../../assets/login.png") no-repeat center;
|
||||||
|
background-size: cover;
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 20px;
|
||||||
|
right: 100px;
|
||||||
|
top: 30vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
@media screen and (max-width: 750px) {
|
||||||
|
width: 88vw;
|
||||||
|
right: 25px;
|
||||||
|
top: 22vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-form {
|
||||||
|
width: 80%;
|
||||||
|
|
||||||
|
.verify {
|
||||||
|
position: absolute;
|
||||||
|
margin: -10px 0 0 -120px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips {
|
||||||
|
color: #409eff;
|
||||||
|
float: right;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret {
|
||||||
|
color: #409eff;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
56
src/config/index.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { App } from "vue";
|
||||||
|
import axios from "axios";
|
||||||
|
let config: object = {};
|
||||||
|
|
||||||
|
const setConfig = (cfg?: unknown) => {
|
||||||
|
config = Object.assign(config, cfg);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getConfig = (key?: string): ServerConfigs => {
|
||||||
|
if (typeof key === "string") {
|
||||||
|
const arr = key.split(".");
|
||||||
|
if (arr && arr.length) {
|
||||||
|
let data = config;
|
||||||
|
arr.forEach(v => {
|
||||||
|
if (data && typeof data[v] !== "undefined") {
|
||||||
|
data = data[v];
|
||||||
|
} else {
|
||||||
|
data = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取项目动态全局配置
|
||||||
|
export const getServerConfig = async (app: App): Promise<undefined> => {
|
||||||
|
app.config.globalProperties.$config = getConfig();
|
||||||
|
return axios({
|
||||||
|
baseURL: "",
|
||||||
|
method: "get",
|
||||||
|
url:
|
||||||
|
process.env.NODE_ENV === "production"
|
||||||
|
? "/manages/serverConfig.json"
|
||||||
|
: "/serverConfig.json"
|
||||||
|
})
|
||||||
|
.then(({ data: config }) => {
|
||||||
|
let $config = app.config.globalProperties.$config;
|
||||||
|
// 自动注入项目配置
|
||||||
|
if (app && $config && typeof config === "object") {
|
||||||
|
$config = Object.assign($config, config);
|
||||||
|
app.config.globalProperties.$config = $config;
|
||||||
|
// 设置全局配置
|
||||||
|
setConfig($config);
|
||||||
|
}
|
||||||
|
// 设置全局baseURL
|
||||||
|
app.config.globalProperties.$baseUrl = $config.baseURL;
|
||||||
|
return $config;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
throw "请在public文件夹下添加serverConfig.json配置文件";
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export { getConfig, setConfig };
|
28
src/directives/elResizeDetector/index.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { Directive } from "vue";
|
||||||
|
import type { DirectiveBinding, VNode } from "vue";
|
||||||
|
import elementResizeDetectorMaker from "element-resize-detector";
|
||||||
|
import type { Erd } from "element-resize-detector";
|
||||||
|
import { emitter } from "/@/utils/mitt";
|
||||||
|
|
||||||
|
const erd: Erd = elementResizeDetectorMaker({
|
||||||
|
strategy: "scroll"
|
||||||
|
});
|
||||||
|
|
||||||
|
export const resize: Directive = {
|
||||||
|
mounted(el: HTMLElement, binding?: DirectiveBinding, vnode?: VNode) {
|
||||||
|
erd.listenTo(el, elem => {
|
||||||
|
const width = elem.offsetWidth;
|
||||||
|
const height = elem.offsetHeight;
|
||||||
|
if (binding?.instance) {
|
||||||
|
emitter.emit("resize", { detail: { width, height } });
|
||||||
|
} else {
|
||||||
|
vnode.el.dispatchEvent(
|
||||||
|
new CustomEvent("resize", { detail: { width, height } })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
unmounted(el: HTMLElement) {
|
||||||
|
erd.uninstall(el);
|
||||||
|
}
|
||||||
|
};
|
2
src/directives/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./permission";
|
||||||
|
export * from "./elResizeDetector";
|
18
src/directives/permission/index.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { usePermissionStoreHook } from "/@/store/modules/permission";
|
||||||
|
import { Directive } from "vue";
|
||||||
|
import type { DirectiveBinding } from "vue";
|
||||||
|
|
||||||
|
export const auth: Directive = {
|
||||||
|
mounted(el: HTMLElement, binding: DirectiveBinding) {
|
||||||
|
const { value } = binding;
|
||||||
|
if (value) {
|
||||||
|
const authRoles = value;
|
||||||
|
const hasAuth = usePermissionStoreHook().buttonAuth.includes(authRoles);
|
||||||
|
if (!hasAuth) {
|
||||||
|
el.style.display = "none";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error("need roles! Like v-auth=\"['admin','test']\"");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
71
src/layout/components/appMain.vue
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, getCurrentInstance } from "vue";
|
||||||
|
import { usePermissionStoreHook } from "/@/store/modules/permission";
|
||||||
|
|
||||||
|
const keepAlive: Boolean = ref(
|
||||||
|
getCurrentInstance().appContext.config.globalProperties.$config?.KeepAlive
|
||||||
|
);
|
||||||
|
|
||||||
|
const transition = computed(() => {
|
||||||
|
return route => {
|
||||||
|
return route.meta.transition;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="app-main">
|
||||||
|
<router-view>
|
||||||
|
<template #default="{ Component, route }">
|
||||||
|
<transition
|
||||||
|
:name="
|
||||||
|
transition(route) && route.meta.transition.enterTransition
|
||||||
|
? 'pure-classes-transition'
|
||||||
|
: (transition(route) && route.meta.transition.name) ||
|
||||||
|
'fade-transform'
|
||||||
|
"
|
||||||
|
:enter-active-class="
|
||||||
|
transition(route) &&
|
||||||
|
`animate__animated ${route.meta.transition.enterTransition}`
|
||||||
|
"
|
||||||
|
:leave-active-class="
|
||||||
|
transition(route) &&
|
||||||
|
`animate__animated ${route.meta.transition.leaveTransition}`
|
||||||
|
"
|
||||||
|
mode="out-in"
|
||||||
|
appear
|
||||||
|
>
|
||||||
|
<keep-alive
|
||||||
|
v-if="keepAlive"
|
||||||
|
:include="usePermissionStoreHook().cachePageList"
|
||||||
|
>
|
||||||
|
<component :is="Component" :key="route.fullPath" />
|
||||||
|
</keep-alive>
|
||||||
|
<component v-else :is="Component" :key="route.fullPath" />
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
</router-view>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.app-main {
|
||||||
|
min-height: calc(100vh - 70px);
|
||||||
|
width: 100%;
|
||||||
|
height: 90vh;
|
||||||
|
position: relative;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fixed-header + .app-main {
|
||||||
|
padding-top: 50px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.el-popup-parent--hidden {
|
||||||
|
.fixed-header {
|
||||||
|
padding-right: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
234
src/layout/components/navbar.vue
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { emitter } from "/@/utils/mitt";
|
||||||
|
import Hamburger from "./sidebar/hamBurger.vue";
|
||||||
|
import { useRouter, useRoute } from "vue-router";
|
||||||
|
import { storageSession } from "/@/utils/storage";
|
||||||
|
import Breadcrumb from "./sidebar/breadCrumb.vue";
|
||||||
|
import { useAppStoreHook } from "/@/store/modules/app";
|
||||||
|
import { unref, watch, getCurrentInstance } from "vue";
|
||||||
|
import { deviceDetection } from "/@/utils/deviceDetection";
|
||||||
|
import screenfull from "../components/screenfull/index.vue";
|
||||||
|
import globalization from "/@/assets/svg/globalization.svg";
|
||||||
|
|
||||||
|
const instance =
|
||||||
|
getCurrentInstance().appContext.config.globalProperties.$storage;
|
||||||
|
const pureApp = useAppStoreHook();
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
let usename = storageSession.getItem("info")?.username;
|
||||||
|
const { locale, t } = useI18n();
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => locale.value,
|
||||||
|
() => {
|
||||||
|
//@ts-ignore
|
||||||
|
document.title = t(unref(route.meta.title)); // 动态title
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 退出登录
|
||||||
|
const logout = (): void => {
|
||||||
|
storageSession.removeItem("info");
|
||||||
|
router.push("/login");
|
||||||
|
};
|
||||||
|
|
||||||
|
function onPanel() {
|
||||||
|
emitter.emit("openPanel");
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSideBar() {
|
||||||
|
pureApp.toggleSideBar();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 简体中文
|
||||||
|
function translationCh() {
|
||||||
|
instance.locale = { locale: "zh" };
|
||||||
|
locale.value = "zh";
|
||||||
|
}
|
||||||
|
|
||||||
|
// English
|
||||||
|
function translationEn() {
|
||||||
|
instance.locale = { locale: "en" };
|
||||||
|
locale.value = "en";
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="navbar">
|
||||||
|
<Hamburger
|
||||||
|
:is-active="pureApp.sidebar.opened"
|
||||||
|
class="hamburger-container"
|
||||||
|
@toggleClick="toggleSideBar"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Breadcrumb class="breadcrumb-container" />
|
||||||
|
|
||||||
|
<div class="vertical-header-right">
|
||||||
|
<!-- 全屏 -->
|
||||||
|
<screenfull v-show="!deviceDetection()" />
|
||||||
|
<!-- 国际化 -->
|
||||||
|
<el-dropdown trigger="click">
|
||||||
|
<globalization />
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu class="translation">
|
||||||
|
<el-dropdown-item
|
||||||
|
:style="{
|
||||||
|
background: locale === 'zh' ? '#1b2a47' : '',
|
||||||
|
color: locale === 'zh' ? '#f4f4f5' : '#000'
|
||||||
|
}"
|
||||||
|
@click="translationCh"
|
||||||
|
>简体中文</el-dropdown-item
|
||||||
|
>
|
||||||
|
<el-dropdown-item
|
||||||
|
:style="{
|
||||||
|
background: locale === 'en' ? '#1b2a47' : '',
|
||||||
|
color: locale === 'en' ? '#f4f4f5' : '#000'
|
||||||
|
}"
|
||||||
|
@click="translationEn"
|
||||||
|
>English</el-dropdown-item
|
||||||
|
>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
<!-- 退出登陆 -->
|
||||||
|
<el-dropdown trigger="click">
|
||||||
|
<span class="el-dropdown-link">
|
||||||
|
<img
|
||||||
|
src="https://avatars.githubusercontent.com/u/44761321?s=400&u=30907819abd29bb3779bc247910873e7c7f7c12f&v=4"
|
||||||
|
/>
|
||||||
|
<p>{{ usename }}</p>
|
||||||
|
</span>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu class="logout">
|
||||||
|
<el-dropdown-item icon="el-icon-switch-button" @click="logout">{{
|
||||||
|
$t("message.hsLoginOut")
|
||||||
|
}}</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
<i
|
||||||
|
class="el-icon-setting"
|
||||||
|
:title="$t('message.hssystemSet')"
|
||||||
|
@click="onPanel"
|
||||||
|
></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.navbar {
|
||||||
|
width: 100%;
|
||||||
|
height: 48px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||||
|
|
||||||
|
.hamburger-container {
|
||||||
|
line-height: 48px;
|
||||||
|
height: 100%;
|
||||||
|
float: left;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.3s;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.025);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vertical-header-right {
|
||||||
|
display: flex;
|
||||||
|
min-width: 280px;
|
||||||
|
height: 48px;
|
||||||
|
align-items: center;
|
||||||
|
color: #000000d9;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
.screen-full {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f6f6f6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.globalization {
|
||||||
|
height: 48px;
|
||||||
|
width: 40px;
|
||||||
|
padding: 11px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f6f6f6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dropdown-link {
|
||||||
|
width: 100px;
|
||||||
|
height: 48px;
|
||||||
|
padding: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-around;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #000000d9;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f6f6f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-icon-setting {
|
||||||
|
height: 48px;
|
||||||
|
width: 40px;
|
||||||
|
padding: 11px;
|
||||||
|
display: flex;
|
||||||
|
cursor: pointer;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f6f6f6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-container {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.translation {
|
||||||
|
.el-dropdown-menu__item {
|
||||||
|
padding: 0 40px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dropdown-menu__item:focus,
|
||||||
|
.el-dropdown-menu__item:not(.is-disabled):hover {
|
||||||
|
color: #606266;
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout {
|
||||||
|
.el-dropdown-menu__item {
|
||||||
|
padding: 0 18px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dropdown-menu__item:focus,
|
||||||
|
.el-dropdown-menu__item:not(.is-disabled):hover {
|
||||||
|
color: #606266;
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
150
src/layout/components/panel/index.vue
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { useEventListener, onClickOutside } from "@vueuse/core";
|
||||||
|
import { emitter } from "/@/utils/mitt";
|
||||||
|
|
||||||
|
let show = ref<Boolean>(false);
|
||||||
|
const target = ref(null);
|
||||||
|
onClickOutside(target, () => {
|
||||||
|
show.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const addEventClick = (): void => {
|
||||||
|
useEventListener("click", closeSidebar);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeSidebar = (evt: any): void => {
|
||||||
|
const parent = evt.target.closest(".right-panel");
|
||||||
|
if (!parent) {
|
||||||
|
show.value = false;
|
||||||
|
window.removeEventListener("click", closeSidebar);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
emitter.on("openPanel", () => {
|
||||||
|
show.value = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
addEventClick
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="{ show: show }" class="right-panel-container">
|
||||||
|
<div class="right-panel-background" />
|
||||||
|
<div ref="target" class="right-panel">
|
||||||
|
<div class="right-panel-items">
|
||||||
|
<div class="project-configuration">
|
||||||
|
<h3>项目配置</h3>
|
||||||
|
<i class="el-icon-close" @click="show = !show"></i>
|
||||||
|
</div>
|
||||||
|
<div style="border-bottom: 1px solid #dcdfe6"></div>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.showright-panel {
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
width: calc(100% - 15px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.right-panel-background {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s cubic-bezier(0.7, 0.3, 0.1, 1);
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-panel {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 300px;
|
||||||
|
height: 100vh;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.05);
|
||||||
|
transition: all 0.25s cubic-bezier(0.7, 0.3, 0.1, 1);
|
||||||
|
transform: translate(100%);
|
||||||
|
background: #fff;
|
||||||
|
z-index: 40000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show {
|
||||||
|
transition: all 0.3s cubic-bezier(0.7, 0.3, 0.1, 1);
|
||||||
|
|
||||||
|
.right-panel-background {
|
||||||
|
z-index: 20000;
|
||||||
|
opacity: 1;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-panel {
|
||||||
|
transform: translate(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.handle-button {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
position: absolute;
|
||||||
|
left: -48px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 24px;
|
||||||
|
border-radius: 6px 0 0 6px !important;
|
||||||
|
z-index: 0;
|
||||||
|
pointer-events: auto;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #fff;
|
||||||
|
line-height: 48px;
|
||||||
|
top: 45%;
|
||||||
|
background: rgb(24, 144, 255);
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 48px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-panel-items {
|
||||||
|
margin-top: 60px;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-configuration {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 30px;
|
||||||
|
position: fixed;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
top: 15px;
|
||||||
|
margin-left: 10px;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 20px;
|
||||||
|
margin-right: 20px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-divider--horizontal) {
|
||||||
|
width: 90%;
|
||||||
|
margin: 20px auto 0 auto;
|
||||||
|
}
|
||||||
|
</style>
|
31
src/layout/components/screenfull/index.vue
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useFullscreen } from "@vueuse/core";
|
||||||
|
const { isFullscreen, toggle } = useFullscreen();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="screen-full" @click="toggle">
|
||||||
|
<i
|
||||||
|
:title="
|
||||||
|
isFullscreen
|
||||||
|
? $t('message.hsexitfullscreen')
|
||||||
|
: $t('message.hsfullscreen')
|
||||||
|
"
|
||||||
|
:class="
|
||||||
|
isFullscreen
|
||||||
|
? 'iconfont team-iconexit-fullscreen'
|
||||||
|
: 'iconfont team-iconfullscreen'
|
||||||
|
"
|
||||||
|
></i>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.screen-full {
|
||||||
|
width: 36px;
|
||||||
|
height: 62px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
</style>
|
403
src/layout/components/setting/index.vue
Normal file
@ -0,0 +1,403 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { split } from "lodash-es";
|
||||||
|
import panel from "../panel/index.vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import { emitter } from "/@/utils/mitt";
|
||||||
|
import { templateRef } from "@vueuse/core";
|
||||||
|
import { debounce } from "/@/utils/debounce";
|
||||||
|
import { useAppStoreHook } from "/@/store/modules/app";
|
||||||
|
import { storageLocal, storageSession } from "/@/utils/storage";
|
||||||
|
import {
|
||||||
|
reactive,
|
||||||
|
ref,
|
||||||
|
unref,
|
||||||
|
watch,
|
||||||
|
useCssModule,
|
||||||
|
getCurrentInstance
|
||||||
|
} from "vue";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { isSelect } = useCssModule();
|
||||||
|
|
||||||
|
const instance =
|
||||||
|
getCurrentInstance().appContext.app.config.globalProperties.$storage;
|
||||||
|
|
||||||
|
// 默认灵动模式
|
||||||
|
const markValue = ref(storageLocal.getItem("showModel") || "smart");
|
||||||
|
|
||||||
|
const logoVal = ref(storageLocal.getItem("logoVal") || "1");
|
||||||
|
|
||||||
|
const localOperate = (key: string, value?: any, model?: string): any => {
|
||||||
|
model && model === "set"
|
||||||
|
? storageLocal.setItem(key, value)
|
||||||
|
: storageLocal.getItem(key);
|
||||||
|
};
|
||||||
|
|
||||||
|
const settings = reactive({
|
||||||
|
greyVal: storageLocal.getItem("greyVal"),
|
||||||
|
weekVal: storageLocal.getItem("weekVal"),
|
||||||
|
tagsVal: storageLocal.getItem("tagsVal")
|
||||||
|
});
|
||||||
|
|
||||||
|
settings.greyVal === null
|
||||||
|
? localOperate("greyVal", false, "set")
|
||||||
|
: document.querySelector("html")?.setAttribute("class", "html-grey");
|
||||||
|
|
||||||
|
settings.weekVal === null
|
||||||
|
? localOperate("weekVal", false, "set")
|
||||||
|
: document.querySelector("html")?.setAttribute("class", "html-weakness");
|
||||||
|
|
||||||
|
function toggleClass(flag: boolean, clsName: string, target?: HTMLElement) {
|
||||||
|
const targetEl = target || document.body;
|
||||||
|
let { className } = targetEl;
|
||||||
|
className = className.replace(clsName, "");
|
||||||
|
targetEl.className = flag ? `${className} ${clsName} ` : className;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 灰色模式设置
|
||||||
|
const greyChange = ({ value }): void => {
|
||||||
|
toggleClass(settings.greyVal, "html-grey", document.querySelector("html"));
|
||||||
|
value
|
||||||
|
? localOperate("greyVal", true, "set")
|
||||||
|
: localOperate("greyVal", false, "set");
|
||||||
|
};
|
||||||
|
|
||||||
|
// 色弱模式设置
|
||||||
|
const weekChange = ({ value }): void => {
|
||||||
|
toggleClass(
|
||||||
|
settings.weekVal,
|
||||||
|
"html-weakness",
|
||||||
|
document.querySelector("html")
|
||||||
|
);
|
||||||
|
value
|
||||||
|
? localOperate("weekVal", true, "set")
|
||||||
|
: localOperate("weekVal", false, "set");
|
||||||
|
};
|
||||||
|
|
||||||
|
const tagsChange = () => {
|
||||||
|
let showVal = settings.tagsVal;
|
||||||
|
showVal
|
||||||
|
? storageLocal.setItem("tagsVal", true)
|
||||||
|
: storageLocal.setItem("tagsVal", false);
|
||||||
|
emitter.emit("tagViewsChange", showVal);
|
||||||
|
};
|
||||||
|
|
||||||
|
function onReset() {
|
||||||
|
storageLocal.clear();
|
||||||
|
storageSession.clear();
|
||||||
|
router.push("/login");
|
||||||
|
}
|
||||||
|
|
||||||
|
function onChange({ label }) {
|
||||||
|
storageLocal.setItem("showModel", label);
|
||||||
|
emitter.emit("tagViewsShowModel", label);
|
||||||
|
}
|
||||||
|
|
||||||
|
const verticalDarkDom = templateRef<HTMLElement | null>(
|
||||||
|
"verticalDarkDom",
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const verticalLightDom = templateRef<HTMLElement | null>(
|
||||||
|
"verticalLightDom",
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const horizontalDarkDom = templateRef<HTMLElement | null>(
|
||||||
|
"horizontalDarkDom",
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const horizontalLightDom = templateRef<HTMLElement | null>(
|
||||||
|
"horizontalLightDom",
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
let dataTheme =
|
||||||
|
ref(storageLocal.getItem("responsive-layout")) ||
|
||||||
|
ref({
|
||||||
|
layout: "horizontal-dark"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (unref(dataTheme)) {
|
||||||
|
// 设置主题
|
||||||
|
let theme = split(unref(dataTheme).layout, "-")[1];
|
||||||
|
window.document.body.setAttribute("data-theme", theme);
|
||||||
|
// 设置导航模式
|
||||||
|
let layout = split(unref(dataTheme).layout, "-")[0];
|
||||||
|
window.document.body.setAttribute("data-layout", layout);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 侧边栏Logo
|
||||||
|
function logoChange() {
|
||||||
|
unref(logoVal) === "1"
|
||||||
|
? storageLocal.setItem("logoVal", "1")
|
||||||
|
: storageLocal.setItem("logoVal", "-1");
|
||||||
|
emitter.emit("logoChange", unref(logoVal));
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFalse(Doms): any {
|
||||||
|
Doms.forEach(v => {
|
||||||
|
toggleClass(false, isSelect, unref(v));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(instance, ({ layout }) => {
|
||||||
|
switch (layout["layout"]) {
|
||||||
|
case "vertical-dark":
|
||||||
|
toggleClass(true, isSelect, unref(verticalDarkDom));
|
||||||
|
debounce(
|
||||||
|
setFalse([verticalLightDom, horizontalDarkDom, horizontalLightDom]),
|
||||||
|
50
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "vertical-light":
|
||||||
|
toggleClass(true, isSelect, unref(verticalLightDom));
|
||||||
|
debounce(
|
||||||
|
setFalse([verticalDarkDom, horizontalDarkDom, horizontalLightDom]),
|
||||||
|
50
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "horizontal-dark":
|
||||||
|
toggleClass(true, isSelect, unref(horizontalDarkDom));
|
||||||
|
debounce(
|
||||||
|
setFalse([verticalDarkDom, verticalLightDom, horizontalLightDom]),
|
||||||
|
50
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "horizontal-light":
|
||||||
|
toggleClass(true, isSelect, unref(horizontalLightDom));
|
||||||
|
debounce(
|
||||||
|
setFalse([verticalDarkDom, verticalLightDom, horizontalDarkDom]),
|
||||||
|
50
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function setTheme(layout: string, theme: string) {
|
||||||
|
dataTheme.value.layout = `${layout}-${theme}`;
|
||||||
|
window.document.body.setAttribute("data-layout", layout);
|
||||||
|
window.document.body.setAttribute("data-theme", theme);
|
||||||
|
instance.layout = { layout: `${layout}-${theme}` };
|
||||||
|
useAppStoreHook().setLayout(layout);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<panel>
|
||||||
|
<el-divider>主题风格</el-divider>
|
||||||
|
<ul class="theme-stley">
|
||||||
|
<el-tooltip class="item" content="左侧菜单暗色模式" placement="bottom">
|
||||||
|
<li
|
||||||
|
:class="dataTheme.layout === 'vertical-dark' ? $style.isSelect : ''"
|
||||||
|
ref="verticalDarkDom"
|
||||||
|
@click="setTheme('vertical', 'dark')"
|
||||||
|
>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
</li>
|
||||||
|
</el-tooltip>
|
||||||
|
|
||||||
|
<el-tooltip class="item" content="左侧菜单亮色模式" placement="bottom">
|
||||||
|
<li
|
||||||
|
:class="dataTheme.layout === 'vertical-light' ? $style.isSelect : ''"
|
||||||
|
ref="verticalLightDom"
|
||||||
|
@click="setTheme('vertical', 'light')"
|
||||||
|
>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
</li>
|
||||||
|
</el-tooltip>
|
||||||
|
|
||||||
|
<el-tooltip class="item" content="顶部菜单暗色模式" placement="bottom">
|
||||||
|
<li
|
||||||
|
:class="dataTheme.layout === 'horizontal-dark' ? $style.isSelect : ''"
|
||||||
|
ref="horizontalDarkDom"
|
||||||
|
@click="setTheme('horizontal', 'dark')"
|
||||||
|
>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
</li>
|
||||||
|
</el-tooltip>
|
||||||
|
|
||||||
|
<el-tooltip class="item" content="顶部菜单亮色模式" placement="bottom">
|
||||||
|
<li
|
||||||
|
:class="
|
||||||
|
dataTheme.layout === 'horizontal-light' ? $style.isSelect : ''
|
||||||
|
"
|
||||||
|
ref="horizontalLightDom"
|
||||||
|
@click="setTheme('horizontal', 'light')"
|
||||||
|
>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
</li>
|
||||||
|
</el-tooltip>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<el-divider>界面显示</el-divider>
|
||||||
|
<ul class="setting">
|
||||||
|
<li>
|
||||||
|
<span>灰色模式</span>
|
||||||
|
<vxe-switch
|
||||||
|
v-model="settings.greyVal"
|
||||||
|
open-label="开"
|
||||||
|
close-label="关"
|
||||||
|
@change="greyChange"
|
||||||
|
></vxe-switch>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span>色弱模式</span>
|
||||||
|
<vxe-switch
|
||||||
|
v-model="settings.weekVal"
|
||||||
|
open-label="开"
|
||||||
|
close-label="关"
|
||||||
|
@change="weekChange"
|
||||||
|
></vxe-switch>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span>隐藏标签页</span>
|
||||||
|
<vxe-switch
|
||||||
|
v-model="settings.tagsVal"
|
||||||
|
open-label="开"
|
||||||
|
close-label="关"
|
||||||
|
@change="tagsChange"
|
||||||
|
></vxe-switch>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span>侧边栏Logo</span>
|
||||||
|
<vxe-switch
|
||||||
|
v-model="logoVal"
|
||||||
|
open-value="1"
|
||||||
|
close-value="-1"
|
||||||
|
open-label="开"
|
||||||
|
close-label="关"
|
||||||
|
@change="logoChange"
|
||||||
|
></vxe-switch>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<span>标签风格</span>
|
||||||
|
<vxe-radio-group v-model="markValue" @change="onChange">
|
||||||
|
<vxe-radio label="card" content="卡片"></vxe-radio>
|
||||||
|
<vxe-radio label="smart" content="灵动"></vxe-radio>
|
||||||
|
</vxe-radio-group>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<el-divider />
|
||||||
|
<vxe-button
|
||||||
|
status="danger"
|
||||||
|
style="width: 90%; margin: 24px 15px"
|
||||||
|
content="清空缓存并返回登录页"
|
||||||
|
icon="fa fa-sign-out"
|
||||||
|
@click="onReset"
|
||||||
|
></vxe-button>
|
||||||
|
</panel>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped module>
|
||||||
|
.isSelect {
|
||||||
|
border: 2px solid #0960bd;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.setting {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin: 25px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-divider__text) {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-stley {
|
||||||
|
margin-top: 25px;
|
||||||
|
width: 100%;
|
||||||
|
height: 180px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-around;
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin: 10px;
|
||||||
|
width: 36%;
|
||||||
|
height: 70px;
|
||||||
|
background: #f0f2f5;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 1px 2.5px 0 rgb(0 0 0 / 18%);
|
||||||
|
|
||||||
|
&:nth-child(1) {
|
||||||
|
div {
|
||||||
|
&:nth-child(1) {
|
||||||
|
width: 30%;
|
||||||
|
height: 100%;
|
||||||
|
background: #1b2a47;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(2) {
|
||||||
|
width: 70%;
|
||||||
|
height: 30%;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 0 1px #888;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(2) {
|
||||||
|
div {
|
||||||
|
&:nth-child(1) {
|
||||||
|
width: 30%;
|
||||||
|
height: 100%;
|
||||||
|
box-shadow: 0 0 1px #888;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 4px 0 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(2) {
|
||||||
|
width: 70%;
|
||||||
|
height: 30%;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 0 1px #888;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(3) {
|
||||||
|
div {
|
||||||
|
&:nth-child(1) {
|
||||||
|
width: 100%;
|
||||||
|
height: 30%;
|
||||||
|
background: #1b2a47;
|
||||||
|
box-shadow: 0 0 1px #888;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(4) {
|
||||||
|
div {
|
||||||
|
&:nth-child(1) {
|
||||||
|
width: 100%;
|
||||||
|
height: 30%;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 0 1px #888;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
79
src/layout/components/sidebar/breadCrumb.vue
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from "vue";
|
||||||
|
import { useRoute, useRouter, RouteLocationMatched } from "vue-router";
|
||||||
|
|
||||||
|
const levelList = ref([]);
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const isDashboard = (route: RouteLocationMatched): boolean | string => {
|
||||||
|
const name = route && (route.name as string);
|
||||||
|
if (!name) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return name.trim().toLocaleLowerCase() === "welcome".toLocaleLowerCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBreadcrumb = (): void => {
|
||||||
|
let matched = route.matched.filter(item => item.meta && item.meta.title);
|
||||||
|
const first = matched[0];
|
||||||
|
if (!isDashboard(first)) {
|
||||||
|
matched = [
|
||||||
|
{
|
||||||
|
path: "/welcome",
|
||||||
|
parentPath: "/",
|
||||||
|
meta: { title: "message.hshome" }
|
||||||
|
} as unknown as RouteLocationMatched
|
||||||
|
].concat(matched);
|
||||||
|
}
|
||||||
|
levelList.value = matched.filter(
|
||||||
|
item => item.meta && item.meta.title && item.meta.breadcrumb !== false
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
getBreadcrumb();
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.path,
|
||||||
|
() => getBreadcrumb()
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleLink = (item: RouteLocationMatched): any => {
|
||||||
|
const { redirect, path } = item;
|
||||||
|
if (redirect) {
|
||||||
|
router.push(redirect.toString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
router.push(path);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-breadcrumb class="app-breadcrumb" separator="/">
|
||||||
|
<transition-group appear name="breadcrumb">
|
||||||
|
<el-breadcrumb-item v-for="(item, index) in levelList" :key="item.path">
|
||||||
|
<span
|
||||||
|
v-if="item.redirect === 'noRedirect' || index == levelList.length - 1"
|
||||||
|
class="no-redirect"
|
||||||
|
>{{ $t(item.meta.title) }}</span
|
||||||
|
>
|
||||||
|
<a v-else @click.prevent="handleLink(item)">
|
||||||
|
{{ $t(item.meta.title) }}
|
||||||
|
</a>
|
||||||
|
</el-breadcrumb-item>
|
||||||
|
</transition-group>
|
||||||
|
</el-breadcrumb>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.app-breadcrumb.el-breadcrumb {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 50px;
|
||||||
|
|
||||||
|
.no-redirect {
|
||||||
|
color: #97a8be;
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
52
src/layout/components/sidebar/hamBurger.vue
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
export interface Props {
|
||||||
|
isActive: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
isActive: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "toggleClick"): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const toggleClick = () => {
|
||||||
|
emit("toggleClick");
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="classes.container" @click="toggleClick">
|
||||||
|
<svg
|
||||||
|
:class="['hamburger', props.isActive ? 'is-active' : '']"
|
||||||
|
viewBox="0 0 1024 1024"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="64"
|
||||||
|
height="64"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style module="classes" scoped>
|
||||||
|
.container {
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.hamburger {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-active {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
</style>
|
215
src/layout/components/sidebar/horizontal.vue
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
computed,
|
||||||
|
unref,
|
||||||
|
watch,
|
||||||
|
nextTick,
|
||||||
|
onMounted,
|
||||||
|
getCurrentInstance
|
||||||
|
} from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { emitter } from "/@/utils/mitt";
|
||||||
|
import { templateRef } from "@vueuse/core";
|
||||||
|
import SidebarItem from "./sidebarItem.vue";
|
||||||
|
import { algorithm } from "/@/utils/algorithm";
|
||||||
|
import screenfull from "../screenfull/index.vue";
|
||||||
|
import { useRoute, useRouter } from "vue-router";
|
||||||
|
import { storageSession } from "/@/utils/storage";
|
||||||
|
import { deviceDetection } from "/@/utils/deviceDetection";
|
||||||
|
import globalization from "/@/assets/svg/globalization.svg";
|
||||||
|
import { usePermissionStoreHook } from "/@/store/modules/permission";
|
||||||
|
|
||||||
|
const instance =
|
||||||
|
getCurrentInstance().appContext.config.globalProperties.$storage;
|
||||||
|
|
||||||
|
const title =
|
||||||
|
getCurrentInstance().appContext.config.globalProperties.$config?.Title;
|
||||||
|
|
||||||
|
const menuRef = templateRef<ElRef | null>("menu", null);
|
||||||
|
const routeStore = usePermissionStoreHook();
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const routers = useRouter().options.routes;
|
||||||
|
let usename = storageSession.getItem("info")?.username;
|
||||||
|
const { locale, t } = useI18n();
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => locale.value,
|
||||||
|
() => {
|
||||||
|
//@ts-ignore
|
||||||
|
// 动态title
|
||||||
|
document.title = t(unref(route.meta.title));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 退出登录
|
||||||
|
const logout = (): void => {
|
||||||
|
storageSession.removeItem("info");
|
||||||
|
router.push("/login");
|
||||||
|
};
|
||||||
|
|
||||||
|
function onPanel() {
|
||||||
|
emitter.emit("openPanel");
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeMenu = computed((): string => {
|
||||||
|
const { meta, path } = route;
|
||||||
|
if (meta.activeMenu) {
|
||||||
|
// @ts-ignore
|
||||||
|
return meta.activeMenu;
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
});
|
||||||
|
|
||||||
|
const menuSelect = (indexPath: string): void => {
|
||||||
|
let parentPath = "";
|
||||||
|
let parentPathIndex = indexPath.lastIndexOf("/");
|
||||||
|
if (parentPathIndex > 0) {
|
||||||
|
parentPath = indexPath.slice(0, parentPathIndex);
|
||||||
|
}
|
||||||
|
// 找到当前路由的信息
|
||||||
|
function findCurrentRoute(routes) {
|
||||||
|
return routes.map(item => {
|
||||||
|
if (item.path === indexPath) {
|
||||||
|
// 切换左侧菜单 通知标签页
|
||||||
|
emitter.emit("changLayoutRoute", {
|
||||||
|
indexPath,
|
||||||
|
parentPath
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (item.children) findCurrentRoute(item.children);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
findCurrentRoute(algorithm.increaseIndexes(routers));
|
||||||
|
};
|
||||||
|
|
||||||
|
function backHome() {
|
||||||
|
router.push("/welcome");
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleResize() {
|
||||||
|
// @ts-ignore
|
||||||
|
menuRef.value.handleResize();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 简体中文
|
||||||
|
function translationCh() {
|
||||||
|
instance.locale = { locale: "zh" };
|
||||||
|
locale.value = "zh";
|
||||||
|
handleResize();
|
||||||
|
}
|
||||||
|
|
||||||
|
// English
|
||||||
|
function translationEn() {
|
||||||
|
instance.locale = { locale: "en" };
|
||||||
|
locale.value = "en";
|
||||||
|
handleResize();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
nextTick(() => {
|
||||||
|
handleResize();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="horizontal-header">
|
||||||
|
<div class="horizontal-header-left" @click="backHome">
|
||||||
|
<i class="fa fa-optin-monster"></i>
|
||||||
|
<h4>{{ title }}</h4>
|
||||||
|
</div>
|
||||||
|
<el-menu
|
||||||
|
ref="menu"
|
||||||
|
:default-active="activeMenu"
|
||||||
|
unique-opened
|
||||||
|
router
|
||||||
|
class="horizontal-header-menu"
|
||||||
|
mode="horizontal"
|
||||||
|
@select="menuSelect"
|
||||||
|
>
|
||||||
|
<sidebar-item
|
||||||
|
v-for="route in routeStore.wholeRoutes"
|
||||||
|
:key="route.path"
|
||||||
|
:item="route"
|
||||||
|
:base-path="route.path"
|
||||||
|
/>
|
||||||
|
</el-menu>
|
||||||
|
<div class="horizontal-header-right">
|
||||||
|
<!-- 全屏 -->
|
||||||
|
<screenfull v-show="!deviceDetection()" />
|
||||||
|
<!-- 国际化 -->
|
||||||
|
<el-dropdown trigger="click">
|
||||||
|
<globalization />
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu class="translation">
|
||||||
|
<el-dropdown-item
|
||||||
|
:style="{
|
||||||
|
background: locale === 'zh' ? '#1b2a47' : '',
|
||||||
|
color: locale === 'zh' ? '#f4f4f5' : '#000'
|
||||||
|
}"
|
||||||
|
@click="translationCh"
|
||||||
|
>简体中文</el-dropdown-item
|
||||||
|
>
|
||||||
|
<el-dropdown-item
|
||||||
|
:style="{
|
||||||
|
background: locale === 'en' ? '#1b2a47' : '',
|
||||||
|
color: locale === 'en' ? '#f4f4f5' : '#000'
|
||||||
|
}"
|
||||||
|
@click="translationEn"
|
||||||
|
>English</el-dropdown-item
|
||||||
|
>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
<!-- 退出登陆 -->
|
||||||
|
<el-dropdown trigger="click">
|
||||||
|
<span class="el-dropdown-link">
|
||||||
|
<img
|
||||||
|
src="https://avatars.githubusercontent.com/u/44761321?s=400&u=30907819abd29bb3779bc247910873e7c7f7c12f&v=4"
|
||||||
|
/>
|
||||||
|
<p>{{ usename }}</p>
|
||||||
|
</span>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu class="logout">
|
||||||
|
<el-dropdown-item icon="el-icon-switch-button" @click="logout">{{
|
||||||
|
$t("message.hsLoginOut")
|
||||||
|
}}</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
<i
|
||||||
|
class="el-icon-setting"
|
||||||
|
:title="$t('message.hssystemSet')"
|
||||||
|
@click="onPanel"
|
||||||
|
></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.translation {
|
||||||
|
.el-dropdown-menu__item {
|
||||||
|
padding: 0 40px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dropdown-menu__item:focus,
|
||||||
|
.el-dropdown-menu__item:not(.is-disabled):hover {
|
||||||
|
color: #606266;
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout {
|
||||||
|
.el-dropdown-menu__item {
|
||||||
|
padding: 0 18px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dropdown-menu__item:focus,
|
||||||
|
.el-dropdown-menu__item:not(.is-disabled):hover {
|
||||||
|
color: #606266;
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
72
src/layout/components/sidebar/logo.vue
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { getCurrentInstance } from "vue";
|
||||||
|
const props = defineProps({
|
||||||
|
collapse: Boolean
|
||||||
|
});
|
||||||
|
|
||||||
|
const title =
|
||||||
|
getCurrentInstance().appContext.config.globalProperties.$config?.Title;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="sidebar-logo-container" :class="{ collapse: props.collapse }">
|
||||||
|
<transition name="sidebarLogoFade">
|
||||||
|
<router-link
|
||||||
|
v-if="props.collapse"
|
||||||
|
key="props.collapse"
|
||||||
|
:title="title"
|
||||||
|
class="sidebar-logo-link"
|
||||||
|
to="/"
|
||||||
|
>
|
||||||
|
<i class="fa fa-optin-monster"></i>
|
||||||
|
<h1 class="sidebar-title">{{ title }}</h1>
|
||||||
|
</router-link>
|
||||||
|
<router-link
|
||||||
|
v-else
|
||||||
|
key="expand"
|
||||||
|
:title="title"
|
||||||
|
class="sidebar-logo-link"
|
||||||
|
to="/"
|
||||||
|
>
|
||||||
|
<i class="fa fa-optin-monster"></i>
|
||||||
|
<h1 class="sidebar-title">{{ title }}</h1>
|
||||||
|
</router-link>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.sidebar-logo-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 50px;
|
||||||
|
text-align: center;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.sidebar-logo-link {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.sidebar-title {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0;
|
||||||
|
color: #1890ff;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 20px;
|
||||||
|
margin-top: 16px;
|
||||||
|
font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fa-optin-monster {
|
||||||
|
font-size: 30px;
|
||||||
|
color: #1890ff;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse {
|
||||||
|
.sidebar-logo {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
99
src/layout/components/sidebar/sidebarItem.vue
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import path from "path";
|
||||||
|
import { PropType, ref } from "vue";
|
||||||
|
import { childrenType } from "../../types";
|
||||||
|
import Icon from "/@/components/ReIcon/src/Icon.vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
item: {
|
||||||
|
type: Object as PropType<childrenType>
|
||||||
|
},
|
||||||
|
isNest: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
basePath: {
|
||||||
|
type: String,
|
||||||
|
default: ""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const onlyOneChild: childrenType = ref(null);
|
||||||
|
|
||||||
|
function hasOneShowingChild(
|
||||||
|
children: childrenType[] = [],
|
||||||
|
parent: childrenType
|
||||||
|
) {
|
||||||
|
const showingChildren = children.filter((item: any) => {
|
||||||
|
onlyOneChild.value = item;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (showingChildren.length === 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showingChildren.length === 0) {
|
||||||
|
onlyOneChild.value = { ...parent, path: "", noShowingChildren: true };
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolvePath(routePath) {
|
||||||
|
return path.resolve(props.basePath, routePath);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<template
|
||||||
|
v-if="
|
||||||
|
hasOneShowingChild(props.item.children, props.item) &&
|
||||||
|
(!onlyOneChild.children || onlyOneChild.noShowingChildren)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<el-menu-item
|
||||||
|
:index="resolvePath(onlyOneChild.path)"
|
||||||
|
:class="{ 'submenu-title-noDropdown': !isNest }"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
:class="
|
||||||
|
onlyOneChild.meta.icon || (props.item.meta && props.item.meta.icon)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<template #title>
|
||||||
|
<span>{{ $t(onlyOneChild.meta.title) }}</span>
|
||||||
|
<Icon
|
||||||
|
v-if="onlyOneChild.meta.extraIcon"
|
||||||
|
:svg="onlyOneChild.meta.extraIcon.svg ? true : false"
|
||||||
|
:content="`${onlyOneChild.meta.extraIcon.name}`"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-menu-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-sub-menu
|
||||||
|
v-else
|
||||||
|
ref="subMenu"
|
||||||
|
:index="resolvePath(props.item.path)"
|
||||||
|
popper-append-to-body
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
<i :class="props.item.meta.icon"></i>
|
||||||
|
<span>{{ $t(props.item.meta.title) }}</span>
|
||||||
|
<Icon
|
||||||
|
v-if="props.item.meta.extraIcon"
|
||||||
|
:svg="props.item.meta.extraIcon.svg ? true : false"
|
||||||
|
:content="`${props.item.meta.extraIcon.name}`"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<sidebar-item
|
||||||
|
v-for="child in props.item.children"
|
||||||
|
:key="child.path"
|
||||||
|
:is-nest="true"
|
||||||
|
:item="child"
|
||||||
|
:base-path="resolvePath(child.path)"
|
||||||
|
class="nest-menu"
|
||||||
|
/>
|
||||||
|
</el-sub-menu>
|
||||||
|
</template>
|
82
src/layout/components/sidebar/vertical.vue
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Logo from "./logo.vue";
|
||||||
|
import { emitter } from "/@/utils/mitt";
|
||||||
|
import SidebarItem from "./sidebarItem.vue";
|
||||||
|
import { algorithm } from "/@/utils/algorithm";
|
||||||
|
import { storageLocal } from "/@/utils/storage";
|
||||||
|
import { useRoute, useRouter } from "vue-router";
|
||||||
|
import { computed, ref, onBeforeMount } from "vue";
|
||||||
|
import { useAppStoreHook } from "/@/store/modules/app";
|
||||||
|
import { usePermissionStoreHook } from "/@/store/modules/permission";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const pureApp = useAppStoreHook();
|
||||||
|
const router = useRouter().options.routes;
|
||||||
|
const routeStore = usePermissionStoreHook();
|
||||||
|
const showLogo = ref(storageLocal.getItem("logoVal") || "1");
|
||||||
|
const isCollapse = computed(() => {
|
||||||
|
return !pureApp.getSidebarStatus;
|
||||||
|
});
|
||||||
|
const activeMenu = computed((): string => {
|
||||||
|
const { meta, path } = route;
|
||||||
|
if (meta.activeMenu) {
|
||||||
|
// @ts-ignore
|
||||||
|
return meta.activeMenu;
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
});
|
||||||
|
|
||||||
|
const menuSelect = (indexPath: string): void => {
|
||||||
|
let parentPath = "";
|
||||||
|
let parentPathIndex = indexPath.lastIndexOf("/");
|
||||||
|
if (parentPathIndex > 0) {
|
||||||
|
parentPath = indexPath.slice(0, parentPathIndex);
|
||||||
|
}
|
||||||
|
// 找到当前路由的信息
|
||||||
|
// eslint-disable-next-line no-inner-declarations
|
||||||
|
function findCurrentRoute(routes) {
|
||||||
|
return routes.map(item => {
|
||||||
|
if (item.path === indexPath) {
|
||||||
|
// 切换左侧菜单 通知标签页
|
||||||
|
emitter.emit("changLayoutRoute", {
|
||||||
|
indexPath,
|
||||||
|
parentPath
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (item.children) findCurrentRoute(item.children);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
findCurrentRoute(algorithm.increaseIndexes(router));
|
||||||
|
};
|
||||||
|
|
||||||
|
onBeforeMount(() => {
|
||||||
|
emitter.on("logoChange", key => {
|
||||||
|
showLogo.value = key;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="['sidebar-container', showLogo ? 'has-logo' : '']">
|
||||||
|
<Logo v-if="showLogo === '1'" :collapse="isCollapse" />
|
||||||
|
<el-scrollbar wrap-class="scrollbar-wrapper">
|
||||||
|
<el-menu
|
||||||
|
:default-active="activeMenu"
|
||||||
|
:collapse="isCollapse"
|
||||||
|
unique-opened
|
||||||
|
router
|
||||||
|
:collapse-transition="false"
|
||||||
|
mode="vertical"
|
||||||
|
@select="menuSelect"
|
||||||
|
>
|
||||||
|
<sidebar-item
|
||||||
|
v-for="route in routeStore.wholeRoutes"
|
||||||
|
:key="route.path"
|
||||||
|
:item="route"
|
||||||
|
:base-path="route.path"
|
||||||
|
/>
|
||||||
|
</el-menu>
|
||||||
|
</el-scrollbar>
|
||||||
|
</div>
|
||||||
|
</template>
|
807
src/layout/components/tag/index.vue
Normal file
@ -0,0 +1,807 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
ref,
|
||||||
|
watch,
|
||||||
|
onBeforeMount,
|
||||||
|
unref,
|
||||||
|
nextTick,
|
||||||
|
computed,
|
||||||
|
getCurrentInstance,
|
||||||
|
ComputedRef
|
||||||
|
} from "vue";
|
||||||
|
import { RouteConfigs, relativeStorageType, tagsViewsType } from "../../types";
|
||||||
|
import { emitter } from "/@/utils/mitt";
|
||||||
|
import { templateRef } from "@vueuse/core";
|
||||||
|
import { handleAliveRoute } from "/@/router";
|
||||||
|
import { storageLocal } from "/@/utils/storage";
|
||||||
|
import { useRoute, useRouter } from "vue-router";
|
||||||
|
import { usePermissionStoreHook } from "/@/store/modules/permission";
|
||||||
|
import { toggleClass, removeClass, hasClass } from "/@/utils/operate";
|
||||||
|
|
||||||
|
import close from "/@/assets/svg/close.svg";
|
||||||
|
import refresh from "/@/assets/svg/refresh.svg";
|
||||||
|
import closeAll from "/@/assets/svg/close_all.svg";
|
||||||
|
import closeLeft from "/@/assets/svg/close_left.svg";
|
||||||
|
import closeOther from "/@/assets/svg/close_other.svg";
|
||||||
|
import closeRight from "/@/assets/svg/close_right.svg";
|
||||||
|
|
||||||
|
let refreshButton = "refresh-button";
|
||||||
|
const instance = getCurrentInstance();
|
||||||
|
|
||||||
|
// 响应式storage
|
||||||
|
let relativeStorage: relativeStorageType;
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const showTags = ref(storageLocal.getItem("tagsVal") || false);
|
||||||
|
const containerDom = templateRef<HTMLElement | null>("containerDom", null);
|
||||||
|
const activeIndex = ref(-1);
|
||||||
|
let routerArrays: Array<RouteConfigs> = [
|
||||||
|
{
|
||||||
|
path: "/welcome",
|
||||||
|
parentPath: "/",
|
||||||
|
meta: {
|
||||||
|
title: "message.hshome",
|
||||||
|
icon: "el-icon-s-home",
|
||||||
|
showLink: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const tagsViews = ref<Array<tagsViewsType>>([
|
||||||
|
{
|
||||||
|
icon: refresh,
|
||||||
|
text: "message.hsreload",
|
||||||
|
divided: false,
|
||||||
|
disabled: false,
|
||||||
|
show: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: close,
|
||||||
|
text: "message.hscloseCurrentTab",
|
||||||
|
divided: false,
|
||||||
|
disabled: routerArrays.length > 1 ? false : true,
|
||||||
|
show: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: closeLeft,
|
||||||
|
text: "message.hscloseLeftTabs",
|
||||||
|
divided: true,
|
||||||
|
disabled: routerArrays.length > 1 ? false : true,
|
||||||
|
show: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: closeRight,
|
||||||
|
text: "message.hscloseRightTabs",
|
||||||
|
divided: false,
|
||||||
|
disabled: routerArrays.length > 1 ? false : true,
|
||||||
|
show: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: closeOther,
|
||||||
|
text: "message.hscloseOtherTabs",
|
||||||
|
divided: true,
|
||||||
|
disabled: routerArrays.length > 2 ? false : true,
|
||||||
|
show: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: closeAll,
|
||||||
|
text: "message.hscloseAllTabs",
|
||||||
|
divided: false,
|
||||||
|
disabled: routerArrays.length > 1 ? false : true,
|
||||||
|
show: true
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
const dynamicTagList: ComputedRef<Array<RouteConfigs>> = computed(() => {
|
||||||
|
return relativeStorage.routesInStorage;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 显示模式,默认灵动模式显示
|
||||||
|
const showModel = ref(storageLocal.getItem("showModel") || "smart");
|
||||||
|
if (!showModel.value) {
|
||||||
|
storageLocal.setItem("showModel", "card");
|
||||||
|
}
|
||||||
|
|
||||||
|
let visible = ref(false);
|
||||||
|
let buttonLeft = ref(0);
|
||||||
|
let buttonTop = ref(0);
|
||||||
|
|
||||||
|
// 当前右键选中的路由信息
|
||||||
|
let currentSelect = ref({});
|
||||||
|
|
||||||
|
function dynamicRouteTag(value: string, parentPath: string): void {
|
||||||
|
const hasValue = relativeStorage.routesInStorage.some((item: any) => {
|
||||||
|
return item.path === value;
|
||||||
|
});
|
||||||
|
|
||||||
|
function concatPath(arr: object[], value: string, parentPath: string) {
|
||||||
|
if (!hasValue) {
|
||||||
|
arr.forEach((arrItem: any) => {
|
||||||
|
let pathConcat = parentPath + arrItem.path;
|
||||||
|
if (arrItem.path === value || pathConcat === value) {
|
||||||
|
routerArrays.push({
|
||||||
|
path: value,
|
||||||
|
parentPath: `/${parentPath.split("/")[1]}`,
|
||||||
|
meta: arrItem.meta
|
||||||
|
});
|
||||||
|
relativeStorage.routesInStorage = routerArrays;
|
||||||
|
} else {
|
||||||
|
if (arrItem.children && arrItem.children.length > 0) {
|
||||||
|
concatPath(arrItem.children, value, parentPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
concatPath(router.options.routes, value, parentPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新加载
|
||||||
|
function onFresh() {
|
||||||
|
toggleClass(true, refreshButton, document.querySelector(".rotate"));
|
||||||
|
const { fullPath } = unref(route);
|
||||||
|
router.replace({
|
||||||
|
path: "/redirect" + fullPath
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
removeClass(document.querySelector(".rotate"), refreshButton);
|
||||||
|
}, 600);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteDynamicTag(obj: any, current: any, tag?: string) {
|
||||||
|
let valueIndex: number = routerArrays.findIndex((item: any) => {
|
||||||
|
return item.path === obj.path;
|
||||||
|
});
|
||||||
|
|
||||||
|
const spliceRoute = (start?: number, end?: number, other?: boolean): void => {
|
||||||
|
if (other) {
|
||||||
|
relativeStorage.routesInStorage = [
|
||||||
|
{
|
||||||
|
path: "/welcome",
|
||||||
|
parentPath: "/",
|
||||||
|
meta: {
|
||||||
|
title: "message.hshome",
|
||||||
|
icon: "el-icon-s-home",
|
||||||
|
showLink: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
obj
|
||||||
|
];
|
||||||
|
routerArrays = relativeStorage.routesInStorage;
|
||||||
|
} else {
|
||||||
|
routerArrays.splice(start, end);
|
||||||
|
relativeStorage.routesInStorage = routerArrays;
|
||||||
|
}
|
||||||
|
router.push(obj.path);
|
||||||
|
// 删除缓存路由
|
||||||
|
handleAliveRoute(route.matched, "delete");
|
||||||
|
};
|
||||||
|
|
||||||
|
if (tag === "other") {
|
||||||
|
spliceRoute(1, 1, true);
|
||||||
|
} else if (tag === "left") {
|
||||||
|
spliceRoute(1, valueIndex - 1);
|
||||||
|
} else if (tag === "right") {
|
||||||
|
spliceRoute(valueIndex + 1, routerArrays.length);
|
||||||
|
} else {
|
||||||
|
// 从当前匹配到的路径中删除
|
||||||
|
spliceRoute(valueIndex, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current === obj.path) {
|
||||||
|
// 如果删除当前激活tag就自动切换到最后一个tag
|
||||||
|
let newRoute: any = routerArrays.slice(-1);
|
||||||
|
nextTick(() => {
|
||||||
|
router.push({
|
||||||
|
path: newRoute[0].path
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteMenu(item, tag?: string) {
|
||||||
|
deleteDynamicTag(item, item.path, tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClickDrop(key, item, selectRoute?: RouteConfigs) {
|
||||||
|
if (item && item.disabled) return;
|
||||||
|
// 当前路由信息
|
||||||
|
switch (key) {
|
||||||
|
case 0:
|
||||||
|
// 重新加载
|
||||||
|
onFresh();
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
// 关闭当前标签页
|
||||||
|
selectRoute
|
||||||
|
? deleteMenu({ path: selectRoute.path, meta: selectRoute.meta })
|
||||||
|
: deleteMenu({ path: route.path, meta: route.meta });
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
// 关闭左侧标签页
|
||||||
|
selectRoute
|
||||||
|
? deleteMenu(
|
||||||
|
{
|
||||||
|
path: selectRoute.path,
|
||||||
|
meta: selectRoute.meta
|
||||||
|
},
|
||||||
|
"left"
|
||||||
|
)
|
||||||
|
: deleteMenu({ path: route.path, meta: route.meta }, "left");
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
// 关闭右侧标签页
|
||||||
|
selectRoute
|
||||||
|
? deleteMenu(
|
||||||
|
{
|
||||||
|
path: selectRoute.path,
|
||||||
|
meta: selectRoute.meta
|
||||||
|
},
|
||||||
|
"right"
|
||||||
|
)
|
||||||
|
: deleteMenu({ path: route.path, meta: route.meta }, "right");
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
// 关闭其他标签页
|
||||||
|
selectRoute
|
||||||
|
? deleteMenu(
|
||||||
|
{
|
||||||
|
path: selectRoute.path,
|
||||||
|
meta: selectRoute.meta
|
||||||
|
},
|
||||||
|
"other"
|
||||||
|
)
|
||||||
|
: deleteMenu({ path: route.path, meta: route.meta }, "other");
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
// 关闭全部标签页
|
||||||
|
routerArrays.splice(1, routerArrays.length);
|
||||||
|
relativeStorage.routesInStorage = routerArrays;
|
||||||
|
usePermissionStoreHook().clearAllCachePage();
|
||||||
|
router.push("/welcome");
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
showMenuModel(route.fullPath);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触发右键中菜单的点击事件
|
||||||
|
function selectTag(key, item) {
|
||||||
|
onClickDrop(key, item, currentSelect.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeMenu() {
|
||||||
|
visible.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showMenus(value: boolean) {
|
||||||
|
Array.of(1, 2, 3, 4, 5).forEach(v => {
|
||||||
|
tagsViews.value[v].show = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function disabledMenus(value: boolean) {
|
||||||
|
Array.of(1, 2, 3, 4, 5).forEach(v => {
|
||||||
|
tagsViews.value[v].disabled = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查当前右键的菜单两边是否存在别的菜单,如果左侧的菜单是首页,则不显示关闭左侧标签页,如果右侧没有菜单,则不显示关闭右侧标签页
|
||||||
|
function showMenuModel(currentPath: string, refresh = false) {
|
||||||
|
let allRoute = unref(relativeStorage.routesInStorage);
|
||||||
|
let routeLength = unref(relativeStorage.routesInStorage).length;
|
||||||
|
// currentIndex为1时,左侧的菜单是首页,则不显示关闭左侧标签页
|
||||||
|
let currentIndex = allRoute.findIndex(v => v.path === currentPath);
|
||||||
|
// 如果currentIndex等于routeLength-1,右侧没有菜单,则不显示关闭右侧标签页
|
||||||
|
showMenus(true);
|
||||||
|
|
||||||
|
if (refresh) {
|
||||||
|
tagsViews.value[0].show = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentIndex === 1 && routeLength !== 2) {
|
||||||
|
// 左侧的菜单是首页,右侧存在别的菜单
|
||||||
|
tagsViews.value[2].show = false;
|
||||||
|
Array.of(1, 3, 4, 5).forEach(v => {
|
||||||
|
tagsViews.value[v].disabled = false;
|
||||||
|
});
|
||||||
|
tagsViews.value[2].disabled = true;
|
||||||
|
} else if (currentIndex === 1 && routeLength === 2) {
|
||||||
|
disabledMenus(false);
|
||||||
|
// 左侧的菜单是首页,右侧不存在别的菜单
|
||||||
|
Array.of(2, 3, 4).forEach(v => {
|
||||||
|
tagsViews.value[v].show = false;
|
||||||
|
tagsViews.value[v].disabled = true;
|
||||||
|
});
|
||||||
|
} else if (routeLength - 1 === currentIndex && currentIndex !== 0) {
|
||||||
|
// 当前路由是所有路由中的最后一个
|
||||||
|
tagsViews.value[3].show = false;
|
||||||
|
Array.of(1, 2, 4, 5).forEach(v => {
|
||||||
|
tagsViews.value[v].disabled = false;
|
||||||
|
});
|
||||||
|
tagsViews.value[3].disabled = true;
|
||||||
|
} else if (currentIndex === 0 || currentPath === "/redirect/welcome") {
|
||||||
|
// 当前路由为首页
|
||||||
|
disabledMenus(true);
|
||||||
|
} else {
|
||||||
|
disabledMenus(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openMenu(tag, e) {
|
||||||
|
closeMenu();
|
||||||
|
if (tag.path === "/welcome") {
|
||||||
|
// 右键菜单为首页,只显示刷新
|
||||||
|
showMenus(false);
|
||||||
|
tagsViews.value[0].show = true;
|
||||||
|
} else if (route.path !== tag.path) {
|
||||||
|
// 右键菜单不匹配当前路由,隐藏刷新
|
||||||
|
tagsViews.value[0].show = false;
|
||||||
|
showMenuModel(tag.path);
|
||||||
|
} else if (
|
||||||
|
// eslint-disable-next-line no-dupe-else-if
|
||||||
|
relativeStorage.routesInStorage.length === 2 &&
|
||||||
|
route.path !== tag.path
|
||||||
|
) {
|
||||||
|
showMenus(true);
|
||||||
|
// 只有两个标签时不显示关闭其他标签页
|
||||||
|
tagsViews.value[4].show = false;
|
||||||
|
} else if (route.path === tag.path) {
|
||||||
|
// 右键当前激活的菜单
|
||||||
|
showMenuModel(tag.path, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentSelect.value = tag;
|
||||||
|
const menuMinWidth = 105;
|
||||||
|
const offsetLeft = unref(containerDom).getBoundingClientRect().left;
|
||||||
|
const offsetWidth = unref(containerDom).offsetWidth;
|
||||||
|
const maxLeft = offsetWidth - menuMinWidth;
|
||||||
|
const left = e.clientX - offsetLeft + 5;
|
||||||
|
if (left > maxLeft) {
|
||||||
|
buttonLeft.value = maxLeft;
|
||||||
|
} else {
|
||||||
|
buttonLeft.value = left;
|
||||||
|
}
|
||||||
|
buttonTop.value = e.clientY + 10;
|
||||||
|
setTimeout(() => {
|
||||||
|
visible.value = true;
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触发tags标签切换
|
||||||
|
function tagOnClick(item) {
|
||||||
|
showMenuModel(item.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 鼠标移入
|
||||||
|
function onMouseenter(item, index) {
|
||||||
|
if (index) activeIndex.value = index;
|
||||||
|
if (unref(showModel) === "smart") {
|
||||||
|
if (hasClass(instance.refs["schedule" + index], "schedule-active")) return;
|
||||||
|
toggleClass(true, "schedule-in", instance.refs["schedule" + index]);
|
||||||
|
toggleClass(false, "schedule-out", instance.refs["schedule" + index]);
|
||||||
|
} else {
|
||||||
|
if (hasClass(instance.refs["dynamic" + index], "card-active")) return;
|
||||||
|
toggleClass(true, "card-in", instance.refs["dynamic" + index]);
|
||||||
|
toggleClass(false, "card-out", instance.refs["dynamic" + index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 鼠标移出
|
||||||
|
function onMouseleave(item, index) {
|
||||||
|
activeIndex.value = -1;
|
||||||
|
if (unref(showModel) === "smart") {
|
||||||
|
if (hasClass(instance.refs["schedule" + index], "schedule-active")) return;
|
||||||
|
toggleClass(false, "schedule-in", instance.refs["schedule" + index]);
|
||||||
|
toggleClass(true, "schedule-out", instance.refs["schedule" + index]);
|
||||||
|
} else {
|
||||||
|
if (hasClass(instance.refs["dynamic" + index], "card-active")) return;
|
||||||
|
toggleClass(false, "card-in", instance.refs["dynamic" + index]);
|
||||||
|
toggleClass(true, "card-out", instance.refs["dynamic" + index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => visible.value,
|
||||||
|
val => {
|
||||||
|
if (val) {
|
||||||
|
document.body.addEventListener("click", closeMenu);
|
||||||
|
} else {
|
||||||
|
document.body.removeEventListener("click", closeMenu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
onBeforeMount(() => {
|
||||||
|
if (!instance) return;
|
||||||
|
relativeStorage = instance.appContext.app.config.globalProperties.$storage;
|
||||||
|
routerArrays = relativeStorage.routesInStorage ?? routerArrays;
|
||||||
|
|
||||||
|
// 根据当前路由初始化操作标签页的禁用状态
|
||||||
|
showMenuModel(route.fullPath);
|
||||||
|
|
||||||
|
// 触发隐藏标签页
|
||||||
|
emitter.on("tagViewsChange", key => {
|
||||||
|
if (unref(showTags) === key) return;
|
||||||
|
showTags.value = key;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 改变标签风格
|
||||||
|
emitter.on("tagViewsShowModel", key => {
|
||||||
|
showModel.value = key;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 接收侧边栏切换传递过来的参数
|
||||||
|
emitter.on("changLayoutRoute", ({ indexPath, parentPath }) => {
|
||||||
|
dynamicRouteTag(indexPath, parentPath);
|
||||||
|
setTimeout(() => {
|
||||||
|
showMenuModel(indexPath);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div ref="containerDom" class="tags-view" v-if="!showTags">
|
||||||
|
<el-scrollbar wrap-class="scrollbar-wrapper" class="scroll-container">
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in dynamicTagList"
|
||||||
|
:key="index"
|
||||||
|
:ref="'dynamic' + index"
|
||||||
|
:class="[
|
||||||
|
'scroll-item is-closable',
|
||||||
|
$route.path === item.path ? 'is-active' : '',
|
||||||
|
$route.path === item.path && showModel === 'card' ? 'card-active' : ''
|
||||||
|
]"
|
||||||
|
@contextmenu.prevent="openMenu(item, $event)"
|
||||||
|
@mouseenter.prevent="onMouseenter(item, index)"
|
||||||
|
@mouseleave.prevent="onMouseleave(item, index)"
|
||||||
|
>
|
||||||
|
<router-link :to="item.path" @click="tagOnClick(item)">{{
|
||||||
|
$t(item.meta.title)
|
||||||
|
}}</router-link>
|
||||||
|
<span
|
||||||
|
v-if="
|
||||||
|
($route.path === item.path && index !== 0) ||
|
||||||
|
(index === activeIndex && index !== 0)
|
||||||
|
"
|
||||||
|
class="el-icon-close"
|
||||||
|
@click="deleteMenu(item)"
|
||||||
|
></span>
|
||||||
|
<div
|
||||||
|
:ref="'schedule' + index"
|
||||||
|
v-if="showModel !== 'card'"
|
||||||
|
:class="[$route.path === item.path ? 'schedule-active' : '']"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</el-scrollbar>
|
||||||
|
<!-- 右键菜单按钮 -->
|
||||||
|
<transition name="el-zoom-in-top">
|
||||||
|
<ul
|
||||||
|
v-show="visible"
|
||||||
|
:key="Math.random()"
|
||||||
|
:style="{ left: buttonLeft + 'px', top: buttonTop + 'px' }"
|
||||||
|
class="contextmenu"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(item, key) in tagsViews"
|
||||||
|
:key="key"
|
||||||
|
style="display: flex; align-items: center"
|
||||||
|
>
|
||||||
|
<li v-if="item.show" @click="selectTag(key, item)">
|
||||||
|
<component :is="item.icon" :key="key" />
|
||||||
|
{{ $t(item.text) }}
|
||||||
|
</li>
|
||||||
|
</div>
|
||||||
|
</ul>
|
||||||
|
</transition>
|
||||||
|
<!-- 右侧功能按钮 -->
|
||||||
|
<ul class="right-button">
|
||||||
|
<li>
|
||||||
|
<i
|
||||||
|
:title="$t('message.hsrefreshRoute')"
|
||||||
|
class="el-icon-refresh-right rotate"
|
||||||
|
@click="onFresh"
|
||||||
|
></i>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<el-dropdown trigger="click" placement="bottom-end">
|
||||||
|
<i class="el-icon-arrow-down"></i>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item
|
||||||
|
v-for="(item, key) in tagsViews"
|
||||||
|
:key="key"
|
||||||
|
:divided="item.divided"
|
||||||
|
:disabled="item.disabled"
|
||||||
|
@click="onClickDrop(key, item)"
|
||||||
|
>
|
||||||
|
<component :is="item.icon" :key="key" />
|
||||||
|
{{ $t(item.text) }}
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<slot></slot>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@keyframes scheduleInWidth {
|
||||||
|
from {
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes scheduleOutWidth {
|
||||||
|
from {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@-webkit-keyframes rotate {
|
||||||
|
from {
|
||||||
|
-webkit-transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
-webkit-transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@-moz-keyframes rotate {
|
||||||
|
from {
|
||||||
|
-moz-transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
-moz-transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@-o-keyframes rotate {
|
||||||
|
from {
|
||||||
|
-o-transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
-o-transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes rotate {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-view {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
box-shadow: 0 0 1px #888;
|
||||||
|
|
||||||
|
.scroll-item {
|
||||||
|
border-radius: 3px 3px 0 0;
|
||||||
|
padding: 2px 6px;
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
margin-right: 4px;
|
||||||
|
height: 28px;
|
||||||
|
line-height: 25px;
|
||||||
|
transition: all 0.4s;
|
||||||
|
|
||||||
|
.el-icon-close {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #1890ff;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-radius: 50%;
|
||||||
|
color: #fff;
|
||||||
|
background: #b4bccc;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-closable:not(:first-child) {
|
||||||
|
&:hover {
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #666;
|
||||||
|
padding: 0 4px 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-container {
|
||||||
|
padding: 5px 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
|
.scroll-item {
|
||||||
|
&:nth-child(1) {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
height: 40px;
|
||||||
|
overflow-x: hidden !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 右键菜单
|
||||||
|
.contextmenu {
|
||||||
|
margin: 0;
|
||||||
|
background: #fff;
|
||||||
|
z-index: 3000;
|
||||||
|
position: absolute;
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 5px 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #000000d9;
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 13px;
|
||||||
|
white-space: nowrap;
|
||||||
|
outline: 0;
|
||||||
|
box-shadow: 0 2px 8px rgb(0 0 0 / 15%);
|
||||||
|
|
||||||
|
li {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 7px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
display: block;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: #fff;
|
||||||
|
font-size: 16px;
|
||||||
|
|
||||||
|
li {
|
||||||
|
width: 40px;
|
||||||
|
height: 38px;
|
||||||
|
line-height: 38px;
|
||||||
|
text-align: center;
|
||||||
|
border-right: 1px solid #ccc;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dropdown-menu {
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
li {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
display: block;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dropdown-menu__item:not(.is-disabled):hover {
|
||||||
|
color: #606266;
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-dropdown-menu__item) i {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dropdown-menu__item--divided::before {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dropdown-menu__item.is-disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-active {
|
||||||
|
background-color: #eaf4fe;
|
||||||
|
position: relative;
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 卡片模式
|
||||||
|
.card-active {
|
||||||
|
border: 1px solid #1890ff;
|
||||||
|
}
|
||||||
|
// 卡片模式下鼠标移入显示蓝色边框
|
||||||
|
.card-in {
|
||||||
|
border: 1px solid #1890ff;
|
||||||
|
color: #1890ff;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 卡片模式下鼠标移出隐藏蓝色边框
|
||||||
|
.card-out {
|
||||||
|
border: none;
|
||||||
|
color: #666;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 灵动模式
|
||||||
|
.schedule-active {
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: #1890ff;
|
||||||
|
}
|
||||||
|
// 灵动模式下鼠标移入显示蓝色进度条
|
||||||
|
.schedule-in {
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: #1890ff;
|
||||||
|
animation: scheduleInWidth 400ms ease-in;
|
||||||
|
}
|
||||||
|
// 灵动模式下鼠标移出隐藏蓝色进度条
|
||||||
|
.schedule-out {
|
||||||
|
width: 0;
|
||||||
|
height: 2px;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: #1890ff;
|
||||||
|
animation: scheduleOutWidth 400ms ease-in;
|
||||||
|
}
|
||||||
|
// 刷新按钮动画效果
|
||||||
|
.refresh-button {
|
||||||
|
-webkit-animation: rotate 600ms linear infinite;
|
||||||
|
-moz-animation: rotate 600ms linear infinite;
|
||||||
|
-o-animation: rotate 600ms linear infinite;
|
||||||
|
animation: rotate 600ms linear infinite;
|
||||||
|
}
|
||||||
|
</style>
|
253
src/layout/index.vue
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { routerArrays } from "./types";
|
||||||
|
export default {
|
||||||
|
computed: {
|
||||||
|
layout() {
|
||||||
|
if (!this.$storage.layout) {
|
||||||
|
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
|
||||||
|
this.$storage.layout = { layout: "vertical-dark" };
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!this.$storage.routesInStorage ||
|
||||||
|
this.$storage.routesInStorage.length === 0
|
||||||
|
) {
|
||||||
|
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
|
||||||
|
this.$storage.routesInStorage = routerArrays;
|
||||||
|
}
|
||||||
|
if (!this.$storage.locale) {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
this.$storage.locale = { locale: "zh" };
|
||||||
|
useI18n().locale.value = "zh";
|
||||||
|
}
|
||||||
|
return this.$storage?.layout.layout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
ref,
|
||||||
|
unref,
|
||||||
|
reactive,
|
||||||
|
computed,
|
||||||
|
onMounted,
|
||||||
|
watchEffect,
|
||||||
|
useCssModule,
|
||||||
|
onBeforeMount,
|
||||||
|
getCurrentInstance
|
||||||
|
} from "vue";
|
||||||
|
import { setType } from "./types";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { emitter } from "/@/utils/mitt";
|
||||||
|
import { toggleClass } from "/@/utils/operate";
|
||||||
|
import { useEventListener } from "@vueuse/core";
|
||||||
|
import { storageLocal } from "/@/utils/storage";
|
||||||
|
import { useAppStoreHook } from "/@/store/modules/app";
|
||||||
|
import fullScreen from "/@/assets/svg/full_screen.svg";
|
||||||
|
import exitScreen from "/@/assets/svg/exit_screen.svg";
|
||||||
|
import { useSettingStoreHook } from "/@/store/modules/settings";
|
||||||
|
|
||||||
|
import navbar from "./components/navbar.vue";
|
||||||
|
import tag from "./components/tag/index.vue";
|
||||||
|
import appMain from "./components/appMain.vue";
|
||||||
|
import setting from "./components/setting/index.vue";
|
||||||
|
import Vertical from "./components/sidebar/vertical.vue";
|
||||||
|
import Horizontal from "./components/sidebar/horizontal.vue";
|
||||||
|
|
||||||
|
const pureSetting = useSettingStoreHook();
|
||||||
|
const { hiddenMainContainer } = useCssModule();
|
||||||
|
|
||||||
|
const instance =
|
||||||
|
getCurrentInstance().appContext.app.config.globalProperties.$storage;
|
||||||
|
|
||||||
|
const hiddenSideBar = ref(
|
||||||
|
getCurrentInstance().appContext.config.globalProperties.$config?.HiddenSideBar
|
||||||
|
);
|
||||||
|
|
||||||
|
const set: setType = reactive({
|
||||||
|
sidebar: computed(() => {
|
||||||
|
return useAppStoreHook().sidebar;
|
||||||
|
}),
|
||||||
|
|
||||||
|
device: computed(() => {
|
||||||
|
return useAppStoreHook().device;
|
||||||
|
}),
|
||||||
|
|
||||||
|
fixedHeader: computed(() => {
|
||||||
|
return pureSetting.fixedHeader;
|
||||||
|
}),
|
||||||
|
|
||||||
|
classes: computed(() => {
|
||||||
|
return {
|
||||||
|
hideSidebar: !set.sidebar.opened,
|
||||||
|
openSidebar: set.sidebar.opened,
|
||||||
|
withoutAnimation: set.sidebar.withoutAnimation,
|
||||||
|
mobile: set.device === "mobile"
|
||||||
|
};
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleClickOutside = (params: boolean) => {
|
||||||
|
useAppStoreHook().closeSideBar({ withoutAnimation: params });
|
||||||
|
};
|
||||||
|
|
||||||
|
function setTheme(layoutModel: string) {
|
||||||
|
let { layout } = storageLocal.getItem("responsive-layout");
|
||||||
|
let theme = layout.match(/-(.*)/)[1];
|
||||||
|
window.document.body.setAttribute("data-layout", layoutModel);
|
||||||
|
window.document.body.setAttribute("data-theme", theme);
|
||||||
|
instance.layout = { layout: `${layoutModel}-${theme}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听容器
|
||||||
|
emitter.on("resize", ({ detail }) => {
|
||||||
|
let { width } = detail;
|
||||||
|
width <= 670 ? setTheme("vertical") : setTheme(useAppStoreHook().layout);
|
||||||
|
});
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (set.device === "mobile" && !set.sidebar.opened) {
|
||||||
|
handleClickOutside(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const $_isMobile = () => {
|
||||||
|
const rect = document.body.getBoundingClientRect();
|
||||||
|
return rect.width - 1 < 992;
|
||||||
|
};
|
||||||
|
|
||||||
|
const $_resizeHandler = () => {
|
||||||
|
if (!document.hidden) {
|
||||||
|
const isMobile = $_isMobile();
|
||||||
|
useAppStoreHook().toggleDevice(isMobile ? "mobile" : "desktop");
|
||||||
|
if (isMobile) {
|
||||||
|
handleClickOutside(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function onFullScreen() {
|
||||||
|
if (unref(hiddenSideBar)) {
|
||||||
|
hiddenSideBar.value = false;
|
||||||
|
toggleClass(
|
||||||
|
false,
|
||||||
|
hiddenMainContainer,
|
||||||
|
document.querySelector(".main-container")
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
hiddenSideBar.value = true;
|
||||||
|
toggleClass(
|
||||||
|
true,
|
||||||
|
hiddenMainContainer,
|
||||||
|
document.querySelector(".main-container")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const isMobile = $_isMobile();
|
||||||
|
if (isMobile) {
|
||||||
|
useAppStoreHook().toggleDevice("mobile");
|
||||||
|
handleClickOutside(true);
|
||||||
|
}
|
||||||
|
toggleClass(
|
||||||
|
unref(hiddenSideBar),
|
||||||
|
hiddenMainContainer,
|
||||||
|
document.querySelector(".main-container")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeMount(() => {
|
||||||
|
useEventListener("resize", $_resizeHandler);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="['app-wrapper', set.classes]" v-resize>
|
||||||
|
<div
|
||||||
|
v-show="
|
||||||
|
set.device === 'mobile' &&
|
||||||
|
set.sidebar.opened &&
|
||||||
|
layout.includes('vertical')
|
||||||
|
"
|
||||||
|
class="drawer-bg"
|
||||||
|
@click="handleClickOutside(false)"
|
||||||
|
/>
|
||||||
|
<Vertical v-show="!hiddenSideBar && layout.includes('vertical')" />
|
||||||
|
<div class="main-container">
|
||||||
|
<div :class="{ 'fixed-header': set.fixedHeader }">
|
||||||
|
<!-- 顶部导航栏 -->
|
||||||
|
<navbar v-show="!hiddenSideBar && layout.includes('vertical')" />
|
||||||
|
<!-- tabs标签页 -->
|
||||||
|
<Horizontal v-show="!hiddenSideBar && layout.includes('horizontal')" />
|
||||||
|
<tag>
|
||||||
|
<span @click="onFullScreen">
|
||||||
|
<fullScreen v-if="!hiddenSideBar" />
|
||||||
|
<exitScreen v-else />
|
||||||
|
</span>
|
||||||
|
</tag>
|
||||||
|
</div>
|
||||||
|
<!-- 主体内容 -->
|
||||||
|
<app-main />
|
||||||
|
</div>
|
||||||
|
<!-- 系统设置 -->
|
||||||
|
<setting />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped module>
|
||||||
|
.hiddenMainContainer {
|
||||||
|
margin-left: 0 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@mixin clearfix {
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
display: table;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-wrapper {
|
||||||
|
@include clearfix;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&.mobile.openSidebar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-bg {
|
||||||
|
background: #000;
|
||||||
|
opacity: 0.3;
|
||||||
|
width: 100%;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fixed-header {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 9;
|
||||||
|
width: calc(100% - 210px);
|
||||||
|
transition: width 0.28s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile .fixed-header {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.re-screen {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
64
src/layout/types.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
export type RouteConfigs = {
|
||||||
|
path?: string;
|
||||||
|
parentPath?: string;
|
||||||
|
meta?: {
|
||||||
|
title?: string;
|
||||||
|
icon?: string;
|
||||||
|
showLink?: boolean;
|
||||||
|
savedPosition?: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type relativeStorageType = {
|
||||||
|
routesInStorage: Array<RouteConfigs>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type tagsViewsType = {
|
||||||
|
icon: string;
|
||||||
|
text: string;
|
||||||
|
divided: boolean;
|
||||||
|
disabled: boolean;
|
||||||
|
show: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface setType {
|
||||||
|
sidebar: {
|
||||||
|
opened: boolean;
|
||||||
|
withoutAnimation: boolean;
|
||||||
|
};
|
||||||
|
device: string;
|
||||||
|
fixedHeader: boolean;
|
||||||
|
classes: {
|
||||||
|
hideSidebar: boolean;
|
||||||
|
openSidebar: boolean;
|
||||||
|
withoutAnimation: boolean;
|
||||||
|
mobile: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const routerArrays: Array<RouteConfigs> = [
|
||||||
|
{
|
||||||
|
path: "/welcome",
|
||||||
|
parentPath: "/",
|
||||||
|
meta: {
|
||||||
|
title: "message.hshome",
|
||||||
|
icon: "el-icon-s-home",
|
||||||
|
showLink: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export type childrenType = {
|
||||||
|
path?: string;
|
||||||
|
noShowingChildren?: boolean;
|
||||||
|
children?: childrenType[];
|
||||||
|
value: unknown;
|
||||||
|
meta?: {
|
||||||
|
icon?: string;
|
||||||
|
title?: string;
|
||||||
|
extraIcon?: {
|
||||||
|
svg?: boolean;
|
||||||
|
name?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
31
src/main.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import App from "./App.vue";
|
||||||
|
import router from "./router";
|
||||||
|
import { setupStore } from "/@/store";
|
||||||
|
import { getServerConfig } from "./config";
|
||||||
|
import { createApp, Directive } from "vue";
|
||||||
|
import { usI18n } from "../src/plugins/i18n";
|
||||||
|
import { useElementPlus } from "../src/plugins/element-plus";
|
||||||
|
import { injectResponsiveStorage } from "/@/utils/storage/responsive";
|
||||||
|
|
||||||
|
import "animate.css";
|
||||||
|
// 导入公共样式
|
||||||
|
import "./style/index.scss";
|
||||||
|
// 导入字体图标
|
||||||
|
import "./assets/iconfont/iconfont.js";
|
||||||
|
import "./assets/iconfont/iconfont.css";
|
||||||
|
|
||||||
|
const app = createApp(App);
|
||||||
|
|
||||||
|
// 自定义指令
|
||||||
|
import * as directives from "/@/directives";
|
||||||
|
Object.keys(directives).forEach(key => {
|
||||||
|
app.directive(key, (directives as { [key: string]: Directive })[key]);
|
||||||
|
});
|
||||||
|
|
||||||
|
getServerConfig(app).then(async config => {
|
||||||
|
injectResponsiveStorage(app, config);
|
||||||
|
setupStore(app);
|
||||||
|
app.use(router).use(useElementPlus).use(usI18n);
|
||||||
|
await router.isReady();
|
||||||
|
app.mount("#app");
|
||||||
|
});
|
8
src/mockProdServer.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { createProdMockServer } from "vite-plugin-mock/es/createProdMockServer";
|
||||||
|
import asyncRoutesMock from "../mock/asyncRoutes";
|
||||||
|
|
||||||
|
export const mockModules = [...asyncRoutesMock];
|
||||||
|
|
||||||
|
export function setupProdMockServer() {
|
||||||
|
createProdMockServer(mockModules);
|
||||||
|
}
|
84
src/plugins/element-plus/index.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { App, Component } from "vue";
|
||||||
|
import {
|
||||||
|
ElTag,
|
||||||
|
ElAffix,
|
||||||
|
ElSkeleton,
|
||||||
|
ElBreadcrumb,
|
||||||
|
ElBreadcrumbItem,
|
||||||
|
ElScrollbar,
|
||||||
|
ElSubMenu,
|
||||||
|
ElButton,
|
||||||
|
ElCol,
|
||||||
|
ElRow,
|
||||||
|
ElSpace,
|
||||||
|
ElDivider,
|
||||||
|
ElCard,
|
||||||
|
ElDropdown,
|
||||||
|
ElDialog,
|
||||||
|
ElMenu,
|
||||||
|
ElMenuItem,
|
||||||
|
ElDropdownItem,
|
||||||
|
ElDropdownMenu,
|
||||||
|
ElIcon,
|
||||||
|
ElInput,
|
||||||
|
ElForm,
|
||||||
|
ElFormItem,
|
||||||
|
ElLoading,
|
||||||
|
ElPopover,
|
||||||
|
ElPopper,
|
||||||
|
ElTooltip,
|
||||||
|
ElDrawer,
|
||||||
|
ElPagination,
|
||||||
|
ElAlert,
|
||||||
|
ElRadioButton,
|
||||||
|
ElRadioGroup,
|
||||||
|
ElDescriptions,
|
||||||
|
ElDescriptionsItem
|
||||||
|
} from "element-plus";
|
||||||
|
|
||||||
|
const components = [
|
||||||
|
ElTag,
|
||||||
|
ElAffix,
|
||||||
|
ElSkeleton,
|
||||||
|
ElBreadcrumb,
|
||||||
|
ElBreadcrumbItem,
|
||||||
|
ElScrollbar,
|
||||||
|
ElSubMenu,
|
||||||
|
ElButton,
|
||||||
|
ElCol,
|
||||||
|
ElRow,
|
||||||
|
ElSpace,
|
||||||
|
ElDivider,
|
||||||
|
ElCard,
|
||||||
|
ElDropdown,
|
||||||
|
ElDialog,
|
||||||
|
ElMenu,
|
||||||
|
ElMenuItem,
|
||||||
|
ElDropdownItem,
|
||||||
|
ElDropdownMenu,
|
||||||
|
ElIcon,
|
||||||
|
ElInput,
|
||||||
|
ElForm,
|
||||||
|
ElFormItem,
|
||||||
|
ElPopover,
|
||||||
|
ElPopper,
|
||||||
|
ElTooltip,
|
||||||
|
ElDrawer,
|
||||||
|
ElPagination,
|
||||||
|
ElAlert,
|
||||||
|
ElRadioButton,
|
||||||
|
ElRadioGroup,
|
||||||
|
ElDescriptions,
|
||||||
|
ElDescriptionsItem
|
||||||
|
];
|
||||||
|
|
||||||
|
const plugins = [ElLoading];
|
||||||
|
|
||||||
|
export function useElementPlus(app: App) {
|
||||||
|
components.forEach((component: Component) => {
|
||||||
|
app.component(component.name, component);
|
||||||
|
});
|
||||||
|
plugins.forEach(plugin => {
|
||||||
|
app.use(plugin);
|
||||||
|
});
|
||||||
|
}
|
78
src/plugins/i18n/config.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
// element-plus国际化
|
||||||
|
import enLocale from "element-plus/lib/locale/lang/en";
|
||||||
|
import zhLocale from "element-plus/lib/locale/lang/zh-cn";
|
||||||
|
|
||||||
|
// 导航菜单配置
|
||||||
|
export const menusConfig = {
|
||||||
|
zh: {
|
||||||
|
message: {
|
||||||
|
hshome: "首页",
|
||||||
|
hserror: "错误页面",
|
||||||
|
hsfourZeroFour: "404",
|
||||||
|
hsfourZeroOne: "401"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
message: {
|
||||||
|
hshome: "Home",
|
||||||
|
hserror: "Error Page",
|
||||||
|
hsfourZeroFour: "404",
|
||||||
|
hsfourZeroOne: "401"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 按钮配置
|
||||||
|
export const buttonConfig = {
|
||||||
|
zh: {
|
||||||
|
message: {
|
||||||
|
hsLoginOut: "退出系统",
|
||||||
|
hsfullscreen: "全屏",
|
||||||
|
hsexitfullscreen: "退出全屏",
|
||||||
|
hsrefreshRoute: "刷新路由",
|
||||||
|
hslogin: "登陆",
|
||||||
|
hsregister: "注册",
|
||||||
|
hsexpendAll: "全部展开",
|
||||||
|
hscollapseAll: "全部折叠",
|
||||||
|
hssystemSet: "系统设置",
|
||||||
|
hsreload: "重新加载",
|
||||||
|
hscloseCurrentTab: "关闭当前标签页",
|
||||||
|
hscloseLeftTabs: "关闭左侧标签页",
|
||||||
|
hscloseRightTabs: "关闭右侧标签页",
|
||||||
|
hscloseOtherTabs: "关闭其他标签页",
|
||||||
|
hscloseAllTabs: "关闭全部标签页"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
message: {
|
||||||
|
hsLoginOut: "loginOut",
|
||||||
|
hsfullscreen: "fullScreen",
|
||||||
|
hsexitfullscreen: "exitFullscreen",
|
||||||
|
hsrefreshRoute: "refreshRoute",
|
||||||
|
hslogin: "login",
|
||||||
|
hsregister: "register",
|
||||||
|
hsexpendAll: "Expand All",
|
||||||
|
hscollapseAll: "Collapse All",
|
||||||
|
hssystemSet: "System Set",
|
||||||
|
hsreload: "Reload",
|
||||||
|
hscloseCurrentTab: "Close CurrentTab",
|
||||||
|
hscloseLeftTabs: "Close LeftTabs",
|
||||||
|
hscloseRightTabs: "Close RightTabs",
|
||||||
|
hscloseOtherTabs: "Close OtherTabs",
|
||||||
|
hscloseAllTabs: "Close AllTabs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const localesList = [menusConfig, buttonConfig];
|
||||||
|
|
||||||
|
export const localesConfigs = {
|
||||||
|
zh: {
|
||||||
|
message: Object.assign({}, ...localesList.map(v => v.zh.message)),
|
||||||
|
...zhLocale
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
message: Object.assign({}, ...localesList.map(v => v.en.message)),
|
||||||
|
...enLocale
|
||||||
|
}
|
||||||
|
};
|
14
src/plugins/i18n/index.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { App } from "vue";
|
||||||
|
import { createI18n } from "vue-i18n";
|
||||||
|
import { localesConfigs } from "./config";
|
||||||
|
import { storageLocal } from "/@/utils/storage";
|
||||||
|
|
||||||
|
export const i18n = createI18n({
|
||||||
|
locale: storageLocal.getItem("responsive-locale")?.locale ?? "zh",
|
||||||
|
fallbackLocale: "en",
|
||||||
|
messages: localesConfigs
|
||||||
|
});
|
||||||
|
|
||||||
|
export function usI18n(app: App) {
|
||||||
|
app.use(i18n);
|
||||||
|
}
|
237
src/router/index.ts
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
import {
|
||||||
|
Router,
|
||||||
|
createRouter,
|
||||||
|
RouteComponent,
|
||||||
|
createWebHashHistory,
|
||||||
|
RouteRecordNormalized
|
||||||
|
} from "vue-router";
|
||||||
|
import { split } from "lodash-es";
|
||||||
|
import { i18n } from "/@/plugins/i18n";
|
||||||
|
import { openLink } from "/@/utils/link";
|
||||||
|
import NProgress from "/@/utils/progress";
|
||||||
|
import { useTimeoutFn } from "@vueuse/core";
|
||||||
|
import { storageSession, storageLocal } from "/@/utils/storage";
|
||||||
|
import { usePermissionStoreHook } from "/@/store/modules/permission";
|
||||||
|
|
||||||
|
// 静态路由
|
||||||
|
import homeRouter from "./modules/home";
|
||||||
|
import Layout from "/@/layout/index.vue";
|
||||||
|
import errorRouter from "./modules/error";
|
||||||
|
// 动态路由
|
||||||
|
import { getAsyncRoutes } from "/@/api/routes";
|
||||||
|
|
||||||
|
// https://cn.vitejs.dev/guide/features.html#glob-import
|
||||||
|
const modulesRoutes = import.meta.glob("/src/views/*/*/*.vue");
|
||||||
|
|
||||||
|
const constantRoutes: Array<RouteComponent> = [homeRouter, errorRouter];
|
||||||
|
|
||||||
|
// 按照路由中meta下的rank等级升序来排序路由
|
||||||
|
export const ascending = arr => {
|
||||||
|
return arr.sort((a: any, b: any) => {
|
||||||
|
return a?.meta?.rank - b?.meta?.rank;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 将所有静态路由导出
|
||||||
|
export const constantRoutesArr: Array<RouteComponent> =
|
||||||
|
ascending(constantRoutes);
|
||||||
|
|
||||||
|
// 过滤meta中showLink为false的路由
|
||||||
|
export const filterTree = data => {
|
||||||
|
const newTree = data.filter(v => v.meta.showLink);
|
||||||
|
newTree.forEach(v => v.children && (v.children = filterTree(v.children)));
|
||||||
|
return newTree;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 从路由中提取keepAlive为true的name组成数组(此处本项目中并没有用到,只是暴露个方法)
|
||||||
|
export const getAliveRoute = () => {
|
||||||
|
const alivePageList = [];
|
||||||
|
const recursiveSearch = treeLists => {
|
||||||
|
if (!treeLists || !treeLists.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < treeLists.length; i++) {
|
||||||
|
if (treeLists[i]?.meta?.keepAlive) alivePageList.push(treeLists[i].name);
|
||||||
|
recursiveSearch(treeLists[i].children);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
recursiveSearch(router.options.routes);
|
||||||
|
return alivePageList;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理缓存路由(添加、删除、刷新)
|
||||||
|
export const handleAliveRoute = (
|
||||||
|
matched: RouteRecordNormalized[],
|
||||||
|
mode?: string
|
||||||
|
) => {
|
||||||
|
switch (mode) {
|
||||||
|
case "add":
|
||||||
|
matched.forEach(v => {
|
||||||
|
usePermissionStoreHook().cacheOperate({ mode: "add", name: v.name });
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "delete":
|
||||||
|
usePermissionStoreHook().cacheOperate({
|
||||||
|
mode: "delete",
|
||||||
|
name: matched[matched.length - 1].name
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
usePermissionStoreHook().cacheOperate({
|
||||||
|
mode: "delete",
|
||||||
|
name: matched[matched.length - 1].name
|
||||||
|
});
|
||||||
|
useTimeoutFn(() => {
|
||||||
|
matched.forEach(v => {
|
||||||
|
usePermissionStoreHook().cacheOperate({ mode: "add", name: v.name });
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 过滤后端传来的动态路由 重新生成规范路由
|
||||||
|
export const addAsyncRoutes = (arrRoutes: Array<RouteComponent>) => {
|
||||||
|
if (!arrRoutes || !arrRoutes.length) return;
|
||||||
|
arrRoutes.forEach((v: any) => {
|
||||||
|
if (v.redirect) {
|
||||||
|
v.component = Layout;
|
||||||
|
} else {
|
||||||
|
v.component = modulesRoutes[`/src/views${v.path}/index.vue`];
|
||||||
|
}
|
||||||
|
if (v.children) {
|
||||||
|
addAsyncRoutes(v.children);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return arrRoutes;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建路由实例
|
||||||
|
export const router: Router = createRouter({
|
||||||
|
history: createWebHashHistory(),
|
||||||
|
routes: filterTree(ascending(constantRoutes)).concat(...remainingRouter),
|
||||||
|
scrollBehavior(to, from, savedPosition) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
if (savedPosition) {
|
||||||
|
return savedPosition;
|
||||||
|
} else {
|
||||||
|
if (from.meta.saveSrollTop) {
|
||||||
|
const top: number =
|
||||||
|
document.documentElement.scrollTop || document.body.scrollTop;
|
||||||
|
resolve({ left: 0, top });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化路由
|
||||||
|
export const initRouter = name => {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
getAsyncRoutes({ name }).then(({ info }) => {
|
||||||
|
if (info.length === 0) {
|
||||||
|
usePermissionStoreHook().changeSetting(info);
|
||||||
|
} else {
|
||||||
|
addAsyncRoutes(info).map((v: any) => {
|
||||||
|
// 防止重复添加路由
|
||||||
|
if (
|
||||||
|
router.options.routes.findIndex(value => value.path === v.path) !==
|
||||||
|
-1
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// 切记将路由push到routes后还需要使用addRoute,这样路由才能正常跳转
|
||||||
|
router.options.routes.push(v);
|
||||||
|
// 最终路由进行升序
|
||||||
|
ascending(router.options.routes);
|
||||||
|
router.addRoute(v.name, v);
|
||||||
|
usePermissionStoreHook().changeSetting(info);
|
||||||
|
}
|
||||||
|
resolve(router);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
router.addRoute({
|
||||||
|
path: "/:pathMatch(.*)",
|
||||||
|
redirect: "/error/404"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置路由
|
||||||
|
export function resetRouter() {
|
||||||
|
router.getRoutes().forEach(route => {
|
||||||
|
const { name } = route;
|
||||||
|
if (name) {
|
||||||
|
router.hasRoute(name) && router.removeRoute(name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 路由白名单
|
||||||
|
const whiteList = ["/login", "/register"];
|
||||||
|
|
||||||
|
router.beforeEach((to, _from, next) => {
|
||||||
|
if (to.meta?.keepAlive) {
|
||||||
|
const newMatched = to.matched;
|
||||||
|
handleAliveRoute(newMatched, "add");
|
||||||
|
// 页面整体刷新和点击标签页刷新
|
||||||
|
if (_from.name === undefined || _from.name === "redirect") {
|
||||||
|
handleAliveRoute(newMatched);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const name = storageSession.getItem("info");
|
||||||
|
NProgress.start();
|
||||||
|
const externalLink = to?.redirectedFrom?.fullPath;
|
||||||
|
// @ts-ignore
|
||||||
|
const { t } = i18n.global;
|
||||||
|
// @ts-ignore
|
||||||
|
if (!externalLink) to.meta.title ? (document.title = t(to.meta.title)) : "";
|
||||||
|
if (name) {
|
||||||
|
if (_from?.name) {
|
||||||
|
// 如果路由包含http 则是超链接 反之是普通路由
|
||||||
|
if (externalLink && externalLink.includes("http")) {
|
||||||
|
openLink(`http${split(externalLink, "http")[1]}`);
|
||||||
|
NProgress.done();
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 刷新
|
||||||
|
if (usePermissionStoreHook().wholeRoutes.length === 0)
|
||||||
|
initRouter(name.username).then((router: Router) => {
|
||||||
|
router.push(to.path);
|
||||||
|
// 刷新页面更新标签栏与页面路由匹配
|
||||||
|
const localRoutes = storageLocal.getItem(
|
||||||
|
"responsive-routesInStorage"
|
||||||
|
);
|
||||||
|
const optionsRoutes = router.options?.routes;
|
||||||
|
const newLocalRoutes = [];
|
||||||
|
optionsRoutes.forEach(ors => {
|
||||||
|
localRoutes.forEach(lrs => {
|
||||||
|
if (ors.path === lrs.parentPath) {
|
||||||
|
newLocalRoutes.push(lrs);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
storageLocal.setItem("responsive-routesInStorage", newLocalRoutes);
|
||||||
|
});
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (to.path !== "/login") {
|
||||||
|
if (whiteList.indexOf(to.path) !== -1) {
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
next({ path: "/login" });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.afterEach(() => {
|
||||||
|
NProgress.done();
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
36
src/router/modules/error.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import Layout from "/@/layout/index.vue";
|
||||||
|
|
||||||
|
const errorRouter = {
|
||||||
|
path: "/error",
|
||||||
|
name: "error",
|
||||||
|
component: Layout,
|
||||||
|
redirect: "/error/401",
|
||||||
|
meta: {
|
||||||
|
icon: "el-icon-position",
|
||||||
|
title: "message.hserror",
|
||||||
|
showLink: true,
|
||||||
|
rank: 7
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "/error/401",
|
||||||
|
name: "401",
|
||||||
|
component: () => import("/@/views/error/401.vue"),
|
||||||
|
meta: {
|
||||||
|
title: "message.hsfourZeroOne",
|
||||||
|
showLink: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/error/404",
|
||||||
|
name: "404",
|
||||||
|
component: () => import("/@/views/error/404.vue"),
|
||||||
|
meta: {
|
||||||
|
title: "message.hsfourZeroFour",
|
||||||
|
showLink: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
export default errorRouter;
|
26
src/router/modules/home.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import Layout from "/@/layout/index.vue";
|
||||||
|
|
||||||
|
const homeRouter = {
|
||||||
|
path: "/",
|
||||||
|
name: "home",
|
||||||
|
component: Layout,
|
||||||
|
redirect: "/welcome",
|
||||||
|
meta: {
|
||||||
|
icon: "el-icon-s-home",
|
||||||
|
showLink: true,
|
||||||
|
rank: 0
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "/welcome",
|
||||||
|
name: "welcome",
|
||||||
|
component: () => import("/@/views/welcome.vue"),
|
||||||
|
meta: {
|
||||||
|
title: "message.hshome",
|
||||||
|
showLink: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
export default homeRouter;
|
44
src/router/modules/remaining.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import Layout from "/@/layout/index.vue";
|
||||||
|
|
||||||
|
const remainingRouter = [
|
||||||
|
{
|
||||||
|
path: "/login",
|
||||||
|
name: "login",
|
||||||
|
component: () => import("/@/views/login.vue"),
|
||||||
|
meta: {
|
||||||
|
title: "message.hslogin",
|
||||||
|
showLink: false,
|
||||||
|
rank: 101
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/register",
|
||||||
|
name: "register",
|
||||||
|
component: () => import("/@/views/register.vue"),
|
||||||
|
meta: {
|
||||||
|
title: "message.hsregister",
|
||||||
|
showLink: false,
|
||||||
|
rank: 102
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/redirect",
|
||||||
|
name: "redirect",
|
||||||
|
component: Layout,
|
||||||
|
meta: {
|
||||||
|
icon: "el-icon-s-home",
|
||||||
|
title: "message.hshome",
|
||||||
|
showLink: false,
|
||||||
|
rank: 104
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "/redirect/:path(.*)",
|
||||||
|
name: "redirect",
|
||||||
|
component: () => import("/@/views/redirect.vue")
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export default remainingRouter;
|
9
src/store/index.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import type { App } from "vue";
|
||||||
|
import { createPinia } from "pinia";
|
||||||
|
const store = createPinia();
|
||||||
|
|
||||||
|
export function setupStore(app: App<Element>) {
|
||||||
|
app.use(store);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { store };
|
72
src/store/modules/app.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { storageLocal } from "/@/utils/storage";
|
||||||
|
import { deviceDetection } from "/@/utils/deviceDetection";
|
||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { store } from "/@/store";
|
||||||
|
|
||||||
|
interface AppState {
|
||||||
|
sidebar: {
|
||||||
|
opened: boolean;
|
||||||
|
withoutAnimation: boolean;
|
||||||
|
};
|
||||||
|
layout: string;
|
||||||
|
device: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAppStore = defineStore({
|
||||||
|
id: "pure-app",
|
||||||
|
state: (): AppState => ({
|
||||||
|
sidebar: {
|
||||||
|
opened: storageLocal.getItem("sidebarStatus")
|
||||||
|
? !!+storageLocal.getItem("sidebarStatus")
|
||||||
|
: true,
|
||||||
|
withoutAnimation: false
|
||||||
|
},
|
||||||
|
layout:
|
||||||
|
storageLocal.getItem("responsive-layout")?.layout.match(/(.*)-/)[1] ??
|
||||||
|
"vertical",
|
||||||
|
device: deviceDetection() ? "mobile" : "desktop"
|
||||||
|
}),
|
||||||
|
getters: {
|
||||||
|
getSidebarStatus() {
|
||||||
|
return this.sidebar.opened;
|
||||||
|
},
|
||||||
|
getDevice() {
|
||||||
|
return this.device;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
TOGGLE_SIDEBAR() {
|
||||||
|
this.sidebar.opened = !this.sidebar.opened;
|
||||||
|
this.sidebar.withoutAnimation = false;
|
||||||
|
if (this.sidebar.opened) {
|
||||||
|
storageLocal.setItem("sidebarStatus", 1);
|
||||||
|
} else {
|
||||||
|
storageLocal.setItem("sidebarStatus", 0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
CLOSE_SIDEBAR(withoutAnimation: boolean) {
|
||||||
|
storageLocal.setItem("sidebarStatus", 0);
|
||||||
|
this.sidebar.opened = false;
|
||||||
|
this.sidebar.withoutAnimation = withoutAnimation;
|
||||||
|
},
|
||||||
|
TOGGLE_DEVICE(device: string) {
|
||||||
|
this.device = device;
|
||||||
|
},
|
||||||
|
async toggleSideBar() {
|
||||||
|
await this.TOGGLE_SIDEBAR();
|
||||||
|
},
|
||||||
|
closeSideBar(withoutAnimation) {
|
||||||
|
this.CLOSE_SIDEBAR(withoutAnimation);
|
||||||
|
},
|
||||||
|
toggleDevice(device) {
|
||||||
|
this.TOGGLE_DEVICE(device);
|
||||||
|
},
|
||||||
|
setLayout(layout) {
|
||||||
|
this.layout = layout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export function useAppStoreHook() {
|
||||||
|
return useAppStore(store);
|
||||||
|
}
|
62
src/store/modules/permission.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { store } from "/@/store";
|
||||||
|
import { cacheType } from "./types";
|
||||||
|
import { constantRoutesArr, ascending, filterTree } from "/@/router/index";
|
||||||
|
|
||||||
|
export const usePermissionStore = defineStore({
|
||||||
|
id: "pure-permission",
|
||||||
|
state: () => ({
|
||||||
|
// 静态路由
|
||||||
|
constantRoutes: constantRoutesArr,
|
||||||
|
wholeRoutes: [],
|
||||||
|
buttonAuth: [],
|
||||||
|
// 缓存页面keepAlive
|
||||||
|
cachePageList: []
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
asyncActionRoutes(routes) {
|
||||||
|
if (this.wholeRoutes.length > 0) return;
|
||||||
|
this.wholeRoutes = filterTree(
|
||||||
|
ascending(this.constantRoutes.concat(routes))
|
||||||
|
);
|
||||||
|
|
||||||
|
const getButtonAuth = (arrRoutes: Array<string>) => {
|
||||||
|
if (!arrRoutes || !arrRoutes.length) return;
|
||||||
|
arrRoutes.forEach((v: any) => {
|
||||||
|
if (v.meta && v.meta.authority) {
|
||||||
|
this.buttonAuth.push(...v.meta.authority);
|
||||||
|
}
|
||||||
|
if (v.children) {
|
||||||
|
getButtonAuth(v.children);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
getButtonAuth(this.wholeRoutes);
|
||||||
|
},
|
||||||
|
async changeSetting(routes) {
|
||||||
|
await this.asyncActionRoutes(routes);
|
||||||
|
},
|
||||||
|
cacheOperate({ mode, name }: cacheType) {
|
||||||
|
switch (mode) {
|
||||||
|
case "add":
|
||||||
|
this.cachePageList.push(name);
|
||||||
|
this.cachePageList = [...new Set(this.cachePageList)];
|
||||||
|
break;
|
||||||
|
case "delete":
|
||||||
|
// eslint-disable-next-line no-case-declarations
|
||||||
|
const delIndex = this.cachePageList.findIndex(v => v === name);
|
||||||
|
this.cachePageList.splice(delIndex, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 清空缓存页面
|
||||||
|
clearAllCachePage() {
|
||||||
|
this.cachePageList = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export function usePermissionStoreHook() {
|
||||||
|
return usePermissionStore(store);
|
||||||
|
}
|
39
src/store/modules/settings.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { store } from "/@/store";
|
||||||
|
import { getConfig } from "/@/config";
|
||||||
|
|
||||||
|
interface SettingState {
|
||||||
|
title: string;
|
||||||
|
fixedHeader: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSettingStore = defineStore({
|
||||||
|
id: "pure-setting",
|
||||||
|
state: (): SettingState => ({
|
||||||
|
title: getConfig().Title,
|
||||||
|
fixedHeader: getConfig().FixedHeader
|
||||||
|
}),
|
||||||
|
getters: {
|
||||||
|
getTitle() {
|
||||||
|
return this.title;
|
||||||
|
},
|
||||||
|
getFixedHeader() {
|
||||||
|
return this.fixedHeader;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
CHANGE_SETTING({ key, value }) {
|
||||||
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
|
if (this.hasOwnProperty(key)) {
|
||||||
|
this[key] = value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
changeSetting(data) {
|
||||||
|
this.CHANGE_SETTING(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export function useSettingStoreHook() {
|
||||||
|
return useSettingStore(store);
|
||||||
|
}
|
6
src/store/modules/types.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { RouteRecordName } from "vue-router";
|
||||||
|
|
||||||
|
export type cacheType = {
|
||||||
|
mode: string;
|
||||||
|
name?: RouteRecordName;
|
||||||
|
};
|
54
src/style/element-ui.scss
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
// cover some element-plus styles
|
||||||
|
|
||||||
|
.el-breadcrumb__inner,
|
||||||
|
.el-breadcrumb__inner a {
|
||||||
|
font-weight: 400 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-upload {
|
||||||
|
input[type="file"] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-upload__input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog {
|
||||||
|
transform: none;
|
||||||
|
left: 0;
|
||||||
|
position: relative;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
// refine element ui upload
|
||||||
|
.upload-container {
|
||||||
|
.el-upload {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.el-upload-dragger {
|
||||||
|
width: 100%;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// dropdown
|
||||||
|
.el-dropdown-menu {
|
||||||
|
padding: 2px 0 2px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// to fix el-date-picker css style
|
||||||
|
.el-range-separator {
|
||||||
|
box-sizing: content-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-loading-mask {
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// el-tooltip的权重
|
||||||
|
.is-dark {
|
||||||
|
z-index: 99999 !important;
|
||||||
|
}
|
111
src/style/index.scss
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
@import "./mixin.scss";
|
||||||
|
@import "./transition.scss";
|
||||||
|
@import "./element-ui.scss";
|
||||||
|
@import "./sidebar.scss";
|
||||||
|
|
||||||
|
body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB,
|
||||||
|
Microsoft YaHei, Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:focus,
|
||||||
|
a:active {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a,
|
||||||
|
a:focus,
|
||||||
|
a:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clearfix {
|
||||||
|
&::after {
|
||||||
|
visibility: hidden;
|
||||||
|
display: block;
|
||||||
|
font-size: 0;
|
||||||
|
content: " ";
|
||||||
|
clear: both;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// main-container global css
|
||||||
|
.app-container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login,
|
||||||
|
.register {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
overflow-x: hidden;
|
||||||
|
background: url("../assets/bg.png") no-repeat center;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 头部用户信息样式重置 */
|
||||||
|
.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 灰色模式
|
||||||
|
.html-grey {
|
||||||
|
filter: grayscale(100%);
|
||||||
|
-webkit-filter: grayscale(100%);
|
||||||
|
-moz-filter: grayscale(100%);
|
||||||
|
-ms-filter: grayscale(100%);
|
||||||
|
-o-filter: grayscale(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 色弱模式
|
||||||
|
.html-weakness {
|
||||||
|
filter: invert(80%);
|
||||||
|
-webkit-filter: invert(80%);
|
||||||
|
-moz-filter: invert(80%);
|
||||||
|
-ms-filter: invert(80%);
|
||||||
|
-o-filter: invert(80%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-spacing {
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-spacing {
|
||||||
|
margin: 0;
|
||||||
|
}
|
28
src/style/mixin.scss
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
@mixin clearfix {
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
display: table;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin scrollBar {
|
||||||
|
&::-webkit-scrollbar-track-piece {
|
||||||
|
background: #d3dce6;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: #99a9bf;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin relative {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
563
src/style/sidebar.scss
Normal file
@ -0,0 +1,563 @@
|
|||||||
|
@mixin merge-style(
|
||||||
|
// 菜单选中后字体样式
|
||||||
|
$subMenuActiveText,
|
||||||
|
//菜单背景
|
||||||
|
$menuBg,
|
||||||
|
// 鼠标覆盖菜单时的背景
|
||||||
|
$menuHover,
|
||||||
|
// 子菜单背景
|
||||||
|
$subMenuBg,
|
||||||
|
// 鼠标覆盖子菜单时的背景
|
||||||
|
$subMenuHover,
|
||||||
|
// vertical模式下主体内容距离网页文档左侧的距离
|
||||||
|
$sideBarWidth,
|
||||||
|
$navTextColor
|
||||||
|
) {
|
||||||
|
$menuText: #7a80b4;
|
||||||
|
$menuActiveText: #7a80b4;
|
||||||
|
|
||||||
|
.main-container {
|
||||||
|
min-height: 100%;
|
||||||
|
transition: margin-left 0.28s;
|
||||||
|
margin-left: $sideBarWidth;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-popper.is-light {
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-container {
|
||||||
|
transition: width 0.28s;
|
||||||
|
width: $sideBarWidth;
|
||||||
|
background-color: $menuBg;
|
||||||
|
height: 100%;
|
||||||
|
position: fixed;
|
||||||
|
font-size: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1001;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 0 1px #888;
|
||||||
|
|
||||||
|
.scrollbar-wrapper {
|
||||||
|
overflow-x: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.horizontal-collapse-transition {
|
||||||
|
transition: 0s width ease-in-out, 0s padding-left ease-in-out,
|
||||||
|
0s padding-right ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-scrollbar__bar.is-vertical {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-scrollbar {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.has-logo {
|
||||||
|
.el-scrollbar {
|
||||||
|
height: calc(100% - 50px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-horizontal {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-menu {
|
||||||
|
border: none;
|
||||||
|
height: 100%;
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-menu-item,
|
||||||
|
.el-sub-menu__title {
|
||||||
|
color: $menuText;
|
||||||
|
}
|
||||||
|
|
||||||
|
// menu hover
|
||||||
|
.submenu-title-noDropdown,
|
||||||
|
.el-sub-menu__title {
|
||||||
|
// background: $menuBg;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $menuHover !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-active > .el-sub-menu__title,
|
||||||
|
.is-active.submenu-title-noDropdown {
|
||||||
|
color: $subMenuActiveText !important;
|
||||||
|
|
||||||
|
i {
|
||||||
|
color: $subMenuActiveText !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-active {
|
||||||
|
transition: color 0.3s;
|
||||||
|
color: $subMenuActiveText !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-menu .el-menu--inline .el-sub-menu__title,
|
||||||
|
& .el-sub-menu .el-menu-item {
|
||||||
|
font-size: 12px;
|
||||||
|
min-width: $sideBarWidth !important;
|
||||||
|
background-color: $subMenuBg !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $subMenuHover !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.horizontal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
background-color: $menuBg;
|
||||||
|
width: 100%;
|
||||||
|
height: 62px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.horizontal-header-left {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
width: auto;
|
||||||
|
min-width: 200px;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $menuHover;
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 30px;
|
||||||
|
color: #1890ff;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $navTextColor;
|
||||||
|
transition: all 0.5s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.horizontal-header-menu {
|
||||||
|
height: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.horizontal-header-right {
|
||||||
|
display: flex;
|
||||||
|
min-width: 280px;
|
||||||
|
align-items: center;
|
||||||
|
color: $navTextColor;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
.screen-full {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $menuHover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.globalization {
|
||||||
|
height: 62px;
|
||||||
|
width: 40px;
|
||||||
|
padding: 11px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: $navTextColor;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $menuHover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dropdown-link {
|
||||||
|
width: 100px;
|
||||||
|
height: 62px;
|
||||||
|
padding: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-around;
|
||||||
|
cursor: pointer;
|
||||||
|
color: $navTextColor;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $menuHover;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-icon-setting {
|
||||||
|
height: 62px;
|
||||||
|
width: 40px;
|
||||||
|
padding: 11px;
|
||||||
|
display: flex;
|
||||||
|
cursor: pointer;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $menuHover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-menu {
|
||||||
|
border: none;
|
||||||
|
height: 100%;
|
||||||
|
background-color: transparent;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-menu-item,
|
||||||
|
.el-sub-menu__title {
|
||||||
|
color: $menuText;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenu-title-noDropdown,
|
||||||
|
.el-sub-menu__title {
|
||||||
|
height: 60px;
|
||||||
|
background: $menuBg;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $menuHover !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-active > .el-sub-menu__title,
|
||||||
|
.is-active.submenu-title-noDropdown {
|
||||||
|
color: $subMenuActiveText !important;
|
||||||
|
border-bottom-color: #409eff;
|
||||||
|
|
||||||
|
i {
|
||||||
|
color: $subMenuActiveText !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-active {
|
||||||
|
transition: color 0.3s;
|
||||||
|
color: $subMenuActiveText !important;
|
||||||
|
border-bottom-color: #409eff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// vertical菜单折叠
|
||||||
|
.el-menu--vertical {
|
||||||
|
.el-menu--popup {
|
||||||
|
background-color: $subMenuBg !important;
|
||||||
|
|
||||||
|
.el-menu-item {
|
||||||
|
color: $menuText;
|
||||||
|
background-color: $subMenuBg;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $subMenuHover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-sub-menu__title {
|
||||||
|
color: $menuText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .el-menu {
|
||||||
|
i {
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-active > .el-sub-menu__title,
|
||||||
|
.is-active.submenu-title-noDropdown {
|
||||||
|
color: $subMenuActiveText !important;
|
||||||
|
|
||||||
|
i {
|
||||||
|
color: $subMenuActiveText !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 子菜单中还有子菜单
|
||||||
|
.el-menu .el-sub-menu__title {
|
||||||
|
font-size: 12px;
|
||||||
|
min-width: $sideBarWidth !important;
|
||||||
|
background-color: $subMenuBg !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $menuHover !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-active {
|
||||||
|
transition: color 0.3s;
|
||||||
|
color: $subMenuActiveText !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nest-menu .el-sub-menu > .el-sub-menu__title,
|
||||||
|
.el-menu-item {
|
||||||
|
&:hover {
|
||||||
|
background-color: $menuHover !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// horizontal菜单折叠
|
||||||
|
.el-menu--horizontal {
|
||||||
|
.el-menu--popup {
|
||||||
|
background-color: $subMenuBg !important;
|
||||||
|
|
||||||
|
.el-menu-item {
|
||||||
|
color: $menuText;
|
||||||
|
background-color: $subMenuBg;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $subMenuHover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-sub-menu__title {
|
||||||
|
color: $menuText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 无子菜单时激活border-bottom
|
||||||
|
.router-link-exact-active > .submenu-title-noDropdown {
|
||||||
|
height: 60px;
|
||||||
|
border-bottom: 2px solid var(--el-menu-active-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 子菜单中还有子菜单
|
||||||
|
.el-menu .el-sub-menu__title {
|
||||||
|
font-size: 12px;
|
||||||
|
min-width: $sideBarWidth !important;
|
||||||
|
background-color: $subMenuBg !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $menuHover !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .el-menu {
|
||||||
|
i {
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-active > .el-sub-menu__title,
|
||||||
|
.is-active.submenu-title-noDropdown {
|
||||||
|
color: $subMenuActiveText !important;
|
||||||
|
|
||||||
|
i {
|
||||||
|
color: $subMenuActiveText !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-active {
|
||||||
|
transition: color 0.3s;
|
||||||
|
color: $subMenuActiveText !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nest-menu .el-sub-menu > .el-sub-menu__title,
|
||||||
|
.el-menu-item {
|
||||||
|
&:hover {
|
||||||
|
background-color: $menuHover !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-scrollbar__wrap {
|
||||||
|
overflow: auto;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-menu--collapse .el-menu .el-sub-menu {
|
||||||
|
min-width: $sideBarWidth !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手机端
|
||||||
|
.mobile {
|
||||||
|
.main-container {
|
||||||
|
margin-left: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-container {
|
||||||
|
transition: transform 0.28s;
|
||||||
|
width: $sideBarWidth !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hideSidebar {
|
||||||
|
.sidebar-container {
|
||||||
|
pointer-events: none;
|
||||||
|
transition-duration: 0.3s;
|
||||||
|
transform: translate3d(-$sideBarWidth, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.withoutAnimation {
|
||||||
|
.main-container,
|
||||||
|
.sidebar-container {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-layout="vertical"] {
|
||||||
|
.hideSidebar {
|
||||||
|
.fixed-header {
|
||||||
|
width: calc(100% - 54px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-container {
|
||||||
|
width: 54px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-container {
|
||||||
|
margin-left: 54px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenu-title-noDropdown {
|
||||||
|
padding: 0 !important;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.el-tooltip {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-sub-menu {
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
& > .el-sub-menu__title {
|
||||||
|
.el-sub-menu__icon-arrow {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-menu--collapse {
|
||||||
|
margin-left: -5px; //需优化的地方
|
||||||
|
.el-sub-menu {
|
||||||
|
& > .el-sub-menu__title {
|
||||||
|
& > span {
|
||||||
|
height: 0;
|
||||||
|
width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
visibility: hidden;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// vertical模式下暗色主题
|
||||||
|
body[data-layout="vertical"][data-theme="dark"] {
|
||||||
|
$subMenuActiveText: #f4f4f5;
|
||||||
|
$menuBg: #1b2a47;
|
||||||
|
$menuHover: #2a395b;
|
||||||
|
$subMenuBg: #1f2d3d;
|
||||||
|
$subMenuHover: #001528;
|
||||||
|
$sideBarWidth: 210px;
|
||||||
|
$navTextColor: #fff;
|
||||||
|
|
||||||
|
@include merge-style(
|
||||||
|
$subMenuActiveText,
|
||||||
|
$menuBg,
|
||||||
|
$menuHover,
|
||||||
|
$subMenuBg,
|
||||||
|
$subMenuHover,
|
||||||
|
$sideBarWidth,
|
||||||
|
$navTextColor
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// vertical模式下亮色主题
|
||||||
|
body[data-layout="vertical"][data-theme="light"] {
|
||||||
|
$subMenuActiveText: #409eff;
|
||||||
|
$menuBg: #fff;
|
||||||
|
$menuHover: #e0ebf6;
|
||||||
|
$subMenuBg: #fff;
|
||||||
|
$subMenuHover: #e0ebf6;
|
||||||
|
$sideBarWidth: 210px;
|
||||||
|
$navTextColor: #7a80b4;
|
||||||
|
|
||||||
|
@include merge-style(
|
||||||
|
$subMenuActiveText,
|
||||||
|
$menuBg,
|
||||||
|
$menuHover,
|
||||||
|
$subMenuBg,
|
||||||
|
$subMenuHover,
|
||||||
|
$sideBarWidth,
|
||||||
|
$navTextColor
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// horizontal模式下暗色主题
|
||||||
|
body[data-layout="horizontal"][data-theme="dark"] {
|
||||||
|
$subMenuActiveText: #f4f4f5;
|
||||||
|
$menuBg: #1b2a47;
|
||||||
|
$menuHover: #2a395b;
|
||||||
|
$subMenuBg: #1f2d3d;
|
||||||
|
$subMenuHover: #001528;
|
||||||
|
$sideBarWidth: 0;
|
||||||
|
$navTextColor: #fff;
|
||||||
|
|
||||||
|
@include merge-style(
|
||||||
|
$subMenuActiveText,
|
||||||
|
$menuBg,
|
||||||
|
$menuHover,
|
||||||
|
$subMenuBg,
|
||||||
|
$subMenuHover,
|
||||||
|
$sideBarWidth,
|
||||||
|
$navTextColor
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// horizontal模式下亮色主题
|
||||||
|
body[data-layout="horizontal"][data-theme="light"] {
|
||||||
|
$subMenuActiveText: #409eff;
|
||||||
|
$menuBg: #fff;
|
||||||
|
$menuHover: #e0ebf6;
|
||||||
|
$subMenuBg: #fff;
|
||||||
|
$subMenuHover: #e0ebf6;
|
||||||
|
$sideBarWidth: 0;
|
||||||
|
$navTextColor: #7a80b4;
|
||||||
|
|
||||||
|
@include merge-style(
|
||||||
|
$subMenuActiveText,
|
||||||
|
$menuBg,
|
||||||
|
$menuHover,
|
||||||
|
$subMenuBg,
|
||||||
|
$subMenuHover,
|
||||||
|
$sideBarWidth,
|
||||||
|
$navTextColor
|
||||||
|
);
|
||||||
|
}
|
44
src/style/transition.scss
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
// global transition css
|
||||||
|
|
||||||
|
/* fade */
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.28s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter,
|
||||||
|
.fade-leave-active {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* fade-transform */
|
||||||
|
.fade-transform-leave-active,
|
||||||
|
.fade-transform-enter-active {
|
||||||
|
transition: all 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-transform-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-30px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-transform-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(30px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* breadcrumb transition */
|
||||||
|
.breadcrumb-enter-active,
|
||||||
|
.breadcrumb-leave-active {
|
||||||
|
transition: all 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-enter-from,
|
||||||
|
.breadcrumb-leave-active {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-leave-active {
|
||||||
|
position: absolute;
|
||||||
|
}
|
21
src/utils/algorithm/index.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
interface ProxyAlgorithm {
|
||||||
|
increaseIndexes<T>(val: Array<T>): Array<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class algorithmProxy implements ProxyAlgorithm {
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
// 数组每一项添加索引字段
|
||||||
|
public increaseIndexes<T>(val: Array<T>): Array<T> {
|
||||||
|
return Object.keys(val)
|
||||||
|
.map(v => {
|
||||||
|
return {
|
||||||
|
...val[v],
|
||||||
|
key: v
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(v => v.meta && v.meta.showLink);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const algorithm = new algorithmProxy();
|
12
src/utils/debounce/index.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// 延迟函数
|
||||||
|
export const delay = (timeout: number) =>
|
||||||
|
new Promise(resolve => setTimeout(resolve, timeout));
|
||||||
|
|
||||||
|
// 防抖函数
|
||||||
|
export const debounce = (fn: () => Fn, timeout: number) => {
|
||||||
|
let timmer: TimeoutHandle;
|
||||||
|
return () => {
|
||||||
|
timmer ? clearTimeout(timmer) : null;
|
||||||
|
timmer = setTimeout(fn, timeout);
|
||||||
|
};
|
||||||
|
};
|
37
src/utils/deviceDetection/index.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
interface deviceInter {
|
||||||
|
match: Fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BrowserInter {
|
||||||
|
browser: string;
|
||||||
|
version: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检测设备类型(手机返回true,反之)
|
||||||
|
export const deviceDetection = () => {
|
||||||
|
const sUserAgent: deviceInter = navigator.userAgent.toLowerCase();
|
||||||
|
// const bIsIpad = sUserAgent.match(/ipad/i) == "ipad";
|
||||||
|
const bIsIphoneOs = sUserAgent.match(/iphone os/i) == "iphone os";
|
||||||
|
const bIsMidp = sUserAgent.match(/midp/i) == "midp";
|
||||||
|
const bIsUc7 = sUserAgent.match(/rv:1.2.3.4/i) == "rv:1.2.3.4";
|
||||||
|
const bIsUc = sUserAgent.match(/ucweb/i) == "ucweb";
|
||||||
|
const bIsAndroid = sUserAgent.match(/android/i) == "android";
|
||||||
|
const bIsCE = sUserAgent.match(/windows ce/i) == "windows ce";
|
||||||
|
const bIsWM = sUserAgent.match(/windows mobile/i) == "windows mobile";
|
||||||
|
return (
|
||||||
|
bIsIphoneOs || bIsMidp || bIsUc7 || bIsUc || bIsAndroid || bIsCE || bIsWM
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取浏览器型号以及版本
|
||||||
|
export const getBrowserInfo = () => {
|
||||||
|
const ua = navigator.userAgent.toLowerCase();
|
||||||
|
const re = /(msie|firefox|chrome|opera|version).*?([\d.]+)/;
|
||||||
|
const m = ua.match(re);
|
||||||
|
const Sys: BrowserInter = {
|
||||||
|
browser: m[1].replace(/version/, "'safari"),
|
||||||
|
version: m[2]
|
||||||
|
};
|
||||||
|
|
||||||
|
return Sys;
|
||||||
|
};
|
32
src/utils/http/config.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { AxiosRequestConfig } from "axios";
|
||||||
|
import { excludeProps } from "./utils";
|
||||||
|
/**
|
||||||
|
* 默认配置
|
||||||
|
*/
|
||||||
|
export const defaultConfig: AxiosRequestConfig = {
|
||||||
|
baseURL: "",
|
||||||
|
//10秒超时
|
||||||
|
timeout: 10000,
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json, text/plain, */*",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Requested-With": "XMLHttpRequest"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export function genConfig(config?: AxiosRequestConfig): AxiosRequestConfig {
|
||||||
|
if (!config) {
|
||||||
|
return defaultConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { headers } = config;
|
||||||
|
if (headers && typeof headers === "object") {
|
||||||
|
defaultConfig.headers = {
|
||||||
|
...defaultConfig.headers,
|
||||||
|
...headers
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { ...excludeProps(config!, "headers"), ...defaultConfig };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const METHODS = ["post", "get", "put", "delete", "option", "patch"];
|
248
src/utils/http/core.ts
Normal file
@ -0,0 +1,248 @@
|
|||||||
|
import Axios, {
|
||||||
|
AxiosRequestConfig,
|
||||||
|
CancelTokenStatic,
|
||||||
|
AxiosInstance
|
||||||
|
} from "axios";
|
||||||
|
|
||||||
|
import NProgress from "../progress";
|
||||||
|
|
||||||
|
import { genConfig } from "./config";
|
||||||
|
|
||||||
|
import { transformConfigByMethod } from "./utils";
|
||||||
|
|
||||||
|
import {
|
||||||
|
cancelTokenType,
|
||||||
|
RequestMethods,
|
||||||
|
EnclosureHttpRequestConfig,
|
||||||
|
EnclosureHttpResoponse,
|
||||||
|
EnclosureHttpError
|
||||||
|
} from "./types.d";
|
||||||
|
|
||||||
|
class EnclosureHttp {
|
||||||
|
constructor() {
|
||||||
|
this.httpInterceptorsRequest();
|
||||||
|
this.httpInterceptorsResponse();
|
||||||
|
}
|
||||||
|
// 初始化配置对象
|
||||||
|
private static initConfig: EnclosureHttpRequestConfig = {};
|
||||||
|
|
||||||
|
// 保存当前Axios实例对象
|
||||||
|
private static axiosInstance: AxiosInstance = Axios.create(genConfig());
|
||||||
|
|
||||||
|
// 保存 EnclosureHttp实例
|
||||||
|
private static EnclosureHttpInstance: EnclosureHttp;
|
||||||
|
|
||||||
|
// axios取消对象
|
||||||
|
private CancelToken: CancelTokenStatic = Axios.CancelToken;
|
||||||
|
|
||||||
|
// 取消的凭证数组
|
||||||
|
private sourceTokenList: Array<cancelTokenType> = [];
|
||||||
|
|
||||||
|
// 记录当前这一次cancelToken的key
|
||||||
|
private currentCancelTokenKey = "";
|
||||||
|
|
||||||
|
private beforeRequestCallback: EnclosureHttpRequestConfig["beforeRequestCallback"] =
|
||||||
|
undefined;
|
||||||
|
|
||||||
|
private beforeResponseCallback: EnclosureHttpRequestConfig["beforeResponseCallback"] =
|
||||||
|
undefined;
|
||||||
|
|
||||||
|
public get cancelTokenList(): Array<cancelTokenType> {
|
||||||
|
return this.sourceTokenList;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
public set cancelTokenList(value) {
|
||||||
|
throw new Error("cancelTokenList不允许赋值");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 私有构造不允许实例化
|
||||||
|
* @returns void 0
|
||||||
|
*/
|
||||||
|
// constructor() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 生成唯一取消key
|
||||||
|
* @param config axios配置
|
||||||
|
* @returns string
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
private static genUniqueKey(config: EnclosureHttpRequestConfig): string {
|
||||||
|
return `${config.url}--${JSON.stringify(config.data)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 取消重复请求
|
||||||
|
* @returns void 0
|
||||||
|
*/
|
||||||
|
private cancelRepeatRequest(): void {
|
||||||
|
const temp: { [key: string]: boolean } = {};
|
||||||
|
|
||||||
|
this.sourceTokenList = this.sourceTokenList.reduce<Array<cancelTokenType>>(
|
||||||
|
(res: Array<cancelTokenType>, cancelToken: cancelTokenType) => {
|
||||||
|
const { cancelKey, cancelExecutor } = cancelToken;
|
||||||
|
if (!temp[cancelKey]) {
|
||||||
|
temp[cancelKey] = true;
|
||||||
|
res.push(cancelToken);
|
||||||
|
} else {
|
||||||
|
cancelExecutor();
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 删除指定的CancelToken
|
||||||
|
* @returns void 0
|
||||||
|
*/
|
||||||
|
private deleteCancelTokenByCancelKey(cancelKey: string): void {
|
||||||
|
this.sourceTokenList =
|
||||||
|
this.sourceTokenList.length < 1
|
||||||
|
? this.sourceTokenList.filter(
|
||||||
|
cancelToken => cancelToken.cancelKey !== cancelKey
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 拦截请求
|
||||||
|
* @returns void 0
|
||||||
|
*/
|
||||||
|
|
||||||
|
private httpInterceptorsRequest(): void {
|
||||||
|
EnclosureHttp.axiosInstance.interceptors.request.use(
|
||||||
|
(config: EnclosureHttpRequestConfig) => {
|
||||||
|
const $config = config;
|
||||||
|
NProgress.start(); // 每次切换页面时,调用进度条
|
||||||
|
const cancelKey = EnclosureHttp.genUniqueKey($config);
|
||||||
|
$config.cancelToken = new this.CancelToken(
|
||||||
|
(cancelExecutor: (cancel: any) => void) => {
|
||||||
|
this.sourceTokenList.push({ cancelKey, cancelExecutor });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this.cancelRepeatRequest();
|
||||||
|
this.currentCancelTokenKey = cancelKey;
|
||||||
|
// 优先判断post/get等方法是否传入回掉,否则执行初始化设置等回掉
|
||||||
|
if (typeof this.beforeRequestCallback === "function") {
|
||||||
|
this.beforeRequestCallback($config);
|
||||||
|
this.beforeRequestCallback = undefined;
|
||||||
|
return $config;
|
||||||
|
}
|
||||||
|
if (EnclosureHttp.initConfig.beforeRequestCallback) {
|
||||||
|
EnclosureHttp.initConfig.beforeRequestCallback($config);
|
||||||
|
return $config;
|
||||||
|
}
|
||||||
|
return $config;
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 清空当前cancelTokenList
|
||||||
|
* @returns void 0
|
||||||
|
*/
|
||||||
|
public clearCancelTokenList(): void {
|
||||||
|
this.sourceTokenList.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 拦截响应
|
||||||
|
* @returns void 0
|
||||||
|
*/
|
||||||
|
private httpInterceptorsResponse(): void {
|
||||||
|
const instance = EnclosureHttp.axiosInstance;
|
||||||
|
instance.interceptors.response.use(
|
||||||
|
(response: EnclosureHttpResoponse) => {
|
||||||
|
// 请求每次成功一次就删除当前canceltoken标记
|
||||||
|
const cancelKey = EnclosureHttp.genUniqueKey(response.config);
|
||||||
|
this.deleteCancelTokenByCancelKey(cancelKey);
|
||||||
|
// 优先判断post/get等方法是否传入回掉,否则执行初始化设置等回掉
|
||||||
|
if (typeof this.beforeResponseCallback === "function") {
|
||||||
|
this.beforeResponseCallback(response);
|
||||||
|
this.beforeResponseCallback = undefined;
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
if (EnclosureHttp.initConfig.beforeResponseCallback) {
|
||||||
|
EnclosureHttp.initConfig.beforeResponseCallback(response);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
NProgress.done();
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
(error: EnclosureHttpError) => {
|
||||||
|
const $error = error;
|
||||||
|
// 判断当前的请求中是否在 取消token数组理存在,如果存在则移除(单次请求流程)
|
||||||
|
if (this.currentCancelTokenKey) {
|
||||||
|
const haskey = this.sourceTokenList.filter(
|
||||||
|
cancelToken => cancelToken.cancelKey === this.currentCancelTokenKey
|
||||||
|
).length;
|
||||||
|
if (haskey) {
|
||||||
|
this.sourceTokenList = this.sourceTokenList.filter(
|
||||||
|
cancelToken =>
|
||||||
|
cancelToken.cancelKey !== this.currentCancelTokenKey
|
||||||
|
);
|
||||||
|
this.currentCancelTokenKey = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$error.isCancelRequest = Axios.isCancel($error);
|
||||||
|
NProgress.done();
|
||||||
|
// 所有的响应异常 区分来源为取消请求/非取消请求
|
||||||
|
return Promise.reject($error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public request<T>(
|
||||||
|
method: RequestMethods,
|
||||||
|
url: string,
|
||||||
|
param?: AxiosRequestConfig,
|
||||||
|
axiosConfig?: EnclosureHttpRequestConfig
|
||||||
|
): Promise<T> {
|
||||||
|
const config = transformConfigByMethod(param, {
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
...axiosConfig
|
||||||
|
} as EnclosureHttpRequestConfig);
|
||||||
|
// 单独处理自定义请求/响应回掉
|
||||||
|
if (axiosConfig?.beforeRequestCallback) {
|
||||||
|
this.beforeRequestCallback = axiosConfig.beforeRequestCallback;
|
||||||
|
}
|
||||||
|
if (axiosConfig?.beforeResponseCallback) {
|
||||||
|
this.beforeResponseCallback = axiosConfig.beforeResponseCallback;
|
||||||
|
}
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
EnclosureHttp.axiosInstance
|
||||||
|
.request(config)
|
||||||
|
.then((response: undefined) => {
|
||||||
|
resolve(response);
|
||||||
|
})
|
||||||
|
.catch((error: any) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public post<T>(
|
||||||
|
url: string,
|
||||||
|
params?: T,
|
||||||
|
config?: EnclosureHttpRequestConfig
|
||||||
|
): Promise<T> {
|
||||||
|
return this.request<T>("post", url, params, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get<T>(
|
||||||
|
url: string,
|
||||||
|
params?: T,
|
||||||
|
config?: EnclosureHttpRequestConfig
|
||||||
|
): Promise<T> {
|
||||||
|
return this.request<T>("get", url, params, config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EnclosureHttp;
|
2
src/utils/http/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import EnclosureHttp from "./core";
|
||||||
|
export const http = new EnclosureHttp();
|
50
src/utils/http/types.d.ts
vendored
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import Axios, {
|
||||||
|
AxiosRequestConfig,
|
||||||
|
Canceler,
|
||||||
|
AxiosResponse,
|
||||||
|
Method,
|
||||||
|
AxiosError
|
||||||
|
} from "axios";
|
||||||
|
|
||||||
|
import { METHODS } from "./config";
|
||||||
|
|
||||||
|
export type cancelTokenType = { cancelKey: string; cancelExecutor: Canceler };
|
||||||
|
|
||||||
|
export type RequestMethods = Extract<
|
||||||
|
Method,
|
||||||
|
"get" | "post" | "put" | "delete" | "patch" | "option" | "head"
|
||||||
|
>;
|
||||||
|
|
||||||
|
export interface EnclosureHttpRequestConfig extends AxiosRequestConfig {
|
||||||
|
beforeRequestCallback?: (request: EnclosureHttpRequestConfig) => void; // 请求发送之前
|
||||||
|
beforeResponseCallback?: (response: EnclosureHttpResoponse) => void; // 相应返回之前
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EnclosureHttpResoponse extends AxiosResponse {
|
||||||
|
config: EnclosureHttpRequestConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EnclosureHttpError extends AxiosError {
|
||||||
|
isCancelRequest?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class EnclosureHttp {
|
||||||
|
cancelTokenList: Array<cancelTokenType>;
|
||||||
|
clearCancelTokenList(): void;
|
||||||
|
request<T>(
|
||||||
|
method: RequestMethods,
|
||||||
|
url: string,
|
||||||
|
param?: AxiosRequestConfig,
|
||||||
|
axiosConfig?: EnclosureHttpRequestConfig
|
||||||
|
): Promise<T>;
|
||||||
|
post<T>(
|
||||||
|
url: string,
|
||||||
|
params?: T,
|
||||||
|
config?: EnclosureHttpRequestConfig
|
||||||
|
): Promise<T>;
|
||||||
|
get<T>(
|
||||||
|
url: string,
|
||||||
|
params?: T,
|
||||||
|
config?: EnclosureHttpRequestConfig
|
||||||
|
): Promise<T>;
|
||||||
|
}
|
29
src/utils/http/utils.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { EnclosureHttpRequestConfig } from "./types.d";
|
||||||
|
|
||||||
|
export function excludeProps<T extends { [key: string]: any }>(
|
||||||
|
origin: T,
|
||||||
|
prop: string
|
||||||
|
): { [key: string]: T } {
|
||||||
|
return Object.keys(origin)
|
||||||
|
.filter(key => !prop.includes(key))
|
||||||
|
.reduce((res, key) => {
|
||||||
|
res[key] = origin[key];
|
||||||
|
return res;
|
||||||
|
}, {} as { [key: string]: T });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function transformConfigByMethod(
|
||||||
|
params: any,
|
||||||
|
config: EnclosureHttpRequestConfig
|
||||||
|
): EnclosureHttpRequestConfig {
|
||||||
|
const { method } = config;
|
||||||
|
const props = ["delete", "get", "head", "options"].includes(
|
||||||
|
method!.toLocaleLowerCase()
|
||||||
|
)
|
||||||
|
? "params"
|
||||||
|
: "data";
|
||||||
|
return {
|
||||||
|
...config,
|
||||||
|
[props]: params
|
||||||
|
};
|
||||||
|
}
|
101
src/utils/is.ts
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
const toString = Object.prototype.toString;
|
||||||
|
|
||||||
|
export function is(val: unknown, type: string) {
|
||||||
|
return toString.call(val) === `[object ${type}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isDef<T = unknown>(val?: T): val is T {
|
||||||
|
return typeof val !== "undefined";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isUnDef<T = unknown>(val?: T): val is T {
|
||||||
|
return !isDef(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isObject(val: any): val is Record<any, any> {
|
||||||
|
return val !== null && is(val, "Object");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isEmpty<T = unknown>(val: T): val is T {
|
||||||
|
if (isArray(val) || isString(val)) {
|
||||||
|
return val.length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (val instanceof Map || val instanceof Set) {
|
||||||
|
return val.size === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isObject(val)) {
|
||||||
|
return Object.keys(val).length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isDate(val: unknown): val is Date {
|
||||||
|
return is(val, "Date");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isNull(val: unknown): val is null {
|
||||||
|
return val === null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isNullAndUnDef(val: unknown): val is null | undefined {
|
||||||
|
return isUnDef(val) && isNull(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isNullOrUnDef(val: unknown): val is null | undefined {
|
||||||
|
return isUnDef(val) || isNull(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isNumber(val: unknown): val is number {
|
||||||
|
return is(val, "Number");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPromise<T = any>(val: unknown): val is Promise<T> {
|
||||||
|
return (
|
||||||
|
is(val, "Promise") &&
|
||||||
|
isObject(val) &&
|
||||||
|
isFunction(val.then) &&
|
||||||
|
isFunction(val.catch)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isString(val: unknown): val is string {
|
||||||
|
return is(val, "String");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isFunction(val: unknown): val is Function {
|
||||||
|
return typeof val === "function";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isBoolean(val: unknown): val is boolean {
|
||||||
|
return is(val, "Boolean");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isRegExp(val: unknown): val is RegExp {
|
||||||
|
return is(val, "RegExp");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isArray(val: any): val is Array<any> {
|
||||||
|
return val && Array.isArray(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isWindow(val: any): val is Window {
|
||||||
|
return typeof window !== "undefined" && is(val, "Window");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isElement(val: unknown): val is Element {
|
||||||
|
return isObject(val) && !!val.tagName;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isServer = typeof window === "undefined";
|
||||||
|
|
||||||
|
export const isClient = !isServer;
|
||||||
|
|
||||||
|
export function isUrl(path: string): boolean {
|
||||||
|
const reg =
|
||||||
|
/(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/;
|
||||||
|
return reg.test(path);
|
||||||
|
}
|
12
src/utils/link.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export const openLink = (link: string) => {
|
||||||
|
const $a: HTMLElement = document.createElement("a");
|
||||||
|
$a.setAttribute("href", link);
|
||||||
|
$a.setAttribute("target", "_blank");
|
||||||
|
$a.setAttribute("rel", "noreferrer noopener");
|
||||||
|
$a.setAttribute("id", "external");
|
||||||
|
document.getElementById("external") &&
|
||||||
|
document.body.removeChild(document.getElementById("external"));
|
||||||
|
document.body.appendChild($a);
|
||||||
|
$a.click();
|
||||||
|
$a.remove();
|
||||||
|
};
|
54
src/utils/loaders/index.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
interface ProxyLoader {
|
||||||
|
loadCss(src: string): any;
|
||||||
|
loadScript(src: string): Promise<any>;
|
||||||
|
loadScriptConcurrent(src: Array<string>): Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class loaderProxy implements ProxyLoader {
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
protected scriptLoaderCache: Array<string> = [];
|
||||||
|
|
||||||
|
public loadCss = (src: string): any => {
|
||||||
|
const element: HTMLLinkElement = document.createElement("link");
|
||||||
|
element.rel = "stylesheet";
|
||||||
|
element.href = src;
|
||||||
|
document.body.appendChild(element);
|
||||||
|
};
|
||||||
|
|
||||||
|
public loadScript = async (src: string): Promise<any> => {
|
||||||
|
if (this.scriptLoaderCache.includes(src)) {
|
||||||
|
return src;
|
||||||
|
} else {
|
||||||
|
const element: HTMLScriptElement = document.createElement("script");
|
||||||
|
element.src = src;
|
||||||
|
document.body.appendChild(element);
|
||||||
|
element.onload = () => {
|
||||||
|
return this.scriptLoaderCache.push(src);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public loadScriptConcurrent = async (
|
||||||
|
srcList: Array<string>
|
||||||
|
): Promise<any> => {
|
||||||
|
if (Array.isArray(srcList)) {
|
||||||
|
const len: number = srcList.length;
|
||||||
|
if (len > 0) {
|
||||||
|
let count = 0;
|
||||||
|
srcList.map(src => {
|
||||||
|
if (src) {
|
||||||
|
this.loadScript(src).then(() => {
|
||||||
|
count++;
|
||||||
|
if (count === len) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loader = new loaderProxy();
|
38
src/utils/message/index.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
|
||||||
|
// 消息
|
||||||
|
const Message = (message: string): any => {
|
||||||
|
return ElMessage({
|
||||||
|
showClose: true,
|
||||||
|
message
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 成功
|
||||||
|
const successMessage = (message: string): any => {
|
||||||
|
return ElMessage({
|
||||||
|
showClose: true,
|
||||||
|
message,
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 警告
|
||||||
|
const warnMessage = (message: string): any => {
|
||||||
|
return ElMessage({
|
||||||
|
showClose: true,
|
||||||
|
message,
|
||||||
|
type: "warning"
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 失败
|
||||||
|
const errorMessage = (message: string): any => {
|
||||||
|
return ElMessage({
|
||||||
|
showClose: true,
|
||||||
|
message,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Message, successMessage, warnMessage, errorMessage };
|