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