xiaoxian521
3 years ago
127 changed files with 11709 additions and 2 deletions
-
14.editorconfig
-
14.env
-
14.env.development
-
2.env.production
-
4.eslintignore
-
75.eslintrc.js
-
18.gitignore
-
6.husky/commit-msg
-
9.husky/common.sh
-
10.husky/lintstagedrc.js
-
10.husky/pre-commit
-
7.prettierrc.js
-
3.stylelintignore
-
1README.en-US.md
-
3README.md
-
5api/routes.ts
-
6babel.config.js
-
19build/proxy.ts
-
32build/utils.ts
-
32commitlint.config.js
-
84index.html
-
90mock/asyncRoutes.ts
-
89package.json
-
3postcss.config.js
-
BINpublic/favicon.ico
-
18public/iconfont.css
-
9public/serverConfig.json
-
28src/App.vue
-
BINsrc/assets/401.gif
-
BINsrc/assets/404.png
-
BINsrc/assets/404_cloud.png
-
BINsrc/assets/bg.png
-
50src/assets/iconfont/iconfont.css
-
64src/assets/iconfont/iconfont.js
-
72src/assets/iconfont/iconfont.json
-
BINsrc/assets/iconfont/iconfont.ttf
-
BINsrc/assets/iconfont/iconfont.woff
-
BINsrc/assets/iconfont/iconfont.woff2
-
BINsrc/assets/login.png
-
1src/assets/svg/close.svg
-
1src/assets/svg/close_all.svg
-
1src/assets/svg/close_left.svg
-
1src/assets/svg/close_other.svg
-
1src/assets/svg/close_right.svg
-
1src/assets/svg/exit_screen.svg
-
1src/assets/svg/full_screen.svg
-
1src/assets/svg/globalization.svg
-
1src/assets/svg/refresh.svg
-
12src/components/ReIcon/index.ts
-
97src/components/ReIcon/src/Icon.vue
-
195src/components/ReInfo/index.vue
-
56src/config/index.ts
-
28src/directives/elResizeDetector/index.ts
-
2src/directives/index.ts
-
18src/directives/permission/index.ts
-
71src/layout/components/appMain.vue
-
234src/layout/components/navbar.vue
-
150src/layout/components/panel/index.vue
-
31src/layout/components/screenfull/index.vue
-
403src/layout/components/setting/index.vue
-
79src/layout/components/sidebar/breadCrumb.vue
-
52src/layout/components/sidebar/hamBurger.vue
-
215src/layout/components/sidebar/horizontal.vue
-
72src/layout/components/sidebar/logo.vue
-
99src/layout/components/sidebar/sidebarItem.vue
-
82src/layout/components/sidebar/vertical.vue
-
807src/layout/components/tag/index.vue
-
253src/layout/index.vue
-
64src/layout/types.ts
-
31src/main.ts
-
8src/mockProdServer.ts
-
84src/plugins/element-plus/index.ts
-
78src/plugins/i18n/config.ts
-
14src/plugins/i18n/index.ts
-
237src/router/index.ts
-
36src/router/modules/error.ts
-
26src/router/modules/home.ts
-
44src/router/modules/remaining.ts
-
9src/store/index.ts
-
72src/store/modules/app.ts
-
62src/store/modules/permission.ts
-
39src/store/modules/settings.ts
-
6src/store/modules/types.ts
-
54src/style/element-ui.scss
-
111src/style/index.scss
-
28src/style/mixin.scss
-
563src/style/sidebar.scss
-
44src/style/transition.scss
-
21src/utils/algorithm/index.ts
-
12src/utils/debounce/index.ts
-
37src/utils/deviceDetection/index.ts
-
32src/utils/http/config.ts
-
248src/utils/http/core.ts
-
2src/utils/http/index.ts
-
50src/utils/http/types.d.ts
-
29src/utils/http/utils.ts
-
101src/utils/is.ts
-
12src/utils/link.ts
-
54src/utils/loaders/index.ts
-
38src/utils/message/index.ts
@ -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 |
@ -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" ] ] |
@ -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" ] ] |
@ -0,0 +1,2 @@ |
|||
# public path |
|||
VITE_PUBLIC_PATH = /manages/ |
@ -0,0 +1,4 @@ |
|||
public |
|||
dist |
|||
*.d.ts |
|||
package.json |
@ -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: "^_" |
|||
} |
|||
] |
|||
} |
|||
}; |
@ -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 |
@ -0,0 +1,6 @@ |
|||
#!/bin/sh |
|||
|
|||
# shellcheck source=./_/husky.sh |
|||
. "$(dirname "$0")/_/husky.sh" |
|||
|
|||
npx --no-install commitlint --edit "$1" |
@ -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 |
@ -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"] |
|||
}; |
@ -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 |
@ -0,0 +1,7 @@ |
|||
module.exports = { |
|||
bracketSpacing: true, |
|||
jsxBracketSameLine: true, |
|||
singleQuote: false, |
|||
arrowParens: "avoid", |
|||
trailingComma: "none" |
|||
}; |
@ -0,0 +1,3 @@ |
|||
/dist/* |
|||
/public/* |
|||
public/* |
@ -0,0 +1 @@ |
|||
<h1>vue-pure-admin精简版</h1> |
@ -1,2 +1 @@ |
|||
# pure-admin-thin |
|||
vue-pure-admin精简版 |
|||
<h1>vue-pure-admin精简版</h1> |
@ -0,0 +1,5 @@ |
|||
import { http } from "/@/utils/http"; |
|||
|
|||
export const getAsyncRoutes = (data?: object) => { |
|||
return http.request("get", "/getAsyncRoutes", data); |
|||
}; |
@ -0,0 +1,6 @@ |
|||
const productPlugins = []; |
|||
process.env.NODE_ENV === "production" && |
|||
productPlugins.push("transform-remove-console"); |
|||
module.exports = { |
|||
plugins: [...productPlugins] |
|||
}; |
@ -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; |
|||
} |
@ -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 }; |
@ -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" |
|||
] |
|||
] |
|||
} |
|||
}; |
@ -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> |
@ -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[]; |
@ -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" |
|||
} |
@ -0,0 +1,3 @@ |
|||
module.exports = { |
|||
plugins: [require("autoprefixer"), require("postcss-import")] |
|||
}; |
@ -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; |
|||
} |
@ -0,0 +1,9 @@ |
|||
{ |
|||
"Version": "2.1.0", |
|||
"Title": "PureAdmin", |
|||
"FixedHeader": false, |
|||
"HiddenSideBar": false, |
|||
"KeepAlive": true, |
|||
"Locale": "zh", |
|||
"Layout": "vertical-dark" |
|||
} |
@ -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> |
After Width: 313 | Height: 428 | Size: 160 KiB |
After Width: 1014 | Height: 556 | Size: 96 KiB |
After Width: 152 | Height: 138 | Size: 4.7 KiB |
After Width: 1920 | Height: 1080 | Size: 1.1 MiB |
@ -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
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View 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 |
|||
} |
|||
] |
|||
} |
After Width: 607 | Height: 716 | Size: 9.9 KiB |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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 |
|||
}; |
@ -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> |
@ -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> |
@ -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 }; |
@ -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); |
|||
} |
|||
}; |
@ -0,0 +1,2 @@ |
|||
export * from "./permission"; |
|||
export * from "./elResizeDetector"; |
@ -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']\""); |
|||
} |
|||
} |
|||
}; |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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; |
|||
}; |
|||
}; |
|||
}; |
@ -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"); |
|||
}); |
@ -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); |
|||
} |
@ -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); |
|||
}); |
|||
} |
@ -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 |
|||
} |
|||
}; |
@ -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); |
|||
} |
@ -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; |
@ -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; |
@ -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; |
@ -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; |
@ -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 }; |
@ -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); |
|||
} |
@ -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); |
|||
} |
@ -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); |
|||
} |
@ -0,0 +1,6 @@ |
|||
import { RouteRecordName } from "vue-router"; |
|||
|
|||
export type cacheType = { |
|||
mode: string; |
|||
name?: RouteRecordName; |
|||
}; |
@ -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; |
|||
} |
@ -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; |
|||
} |
@ -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%; |
|||
} |
@ -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 |
|||
); |
|||
} |
@ -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; |
|||
} |
@ -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(); |
@ -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); |
|||
}; |
|||
}; |
@ -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; |
|||
}; |
@ -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"]; |
@ -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; |
@ -0,0 +1,2 @@ |
|||
import EnclosureHttp from "./core"; |
|||
export const http = new EnclosureHttp(); |
@ -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>; |
|||
} |
@ -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 |
|||
}; |
|||
} |
@ -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); |
|||
} |
@ -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(); |
|||
}; |
@ -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(); |
@ -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 }; |
Some files were not shown because too many files changed in this diff
Write
Preview
Loading…
Cancel
Save
Reference in new issue