Compare commits
224 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
836c9e7cab | ||
|
|
4ded2a7a0c | ||
|
|
dc1caecf1c | ||
|
|
8de3e8b37f | ||
|
|
ab93216dbe | ||
|
|
86177e430e | ||
|
|
93ac4fa813 | ||
|
|
0903008ced | ||
|
|
10fa0ee8c8 | ||
|
|
eb0771e7ec | ||
|
|
39159d5e7b | ||
|
|
501891a21c | ||
|
|
cbffe31c70 | ||
|
|
3ef9444bcb | ||
|
|
c81227bb4c | ||
|
|
05ed941638 | ||
|
|
77c798eaed | ||
|
|
6ab9997a56 | ||
|
|
b961659c2f | ||
|
|
81bf66eca9 | ||
|
|
b251f8ff79 | ||
|
|
bae16008db | ||
|
|
a0c54a6ac9 | ||
|
|
438aab9bfc | ||
|
|
e97bd9c8c4 | ||
|
|
653bafaa2b | ||
|
|
b82a3d3a2e | ||
|
|
b8c8251c64 | ||
|
|
00cc5a88e0 | ||
|
|
5d6ed8da33 | ||
|
|
d57e0e379e | ||
|
|
7811f6bdeb | ||
|
|
113e5f9db2 | ||
|
|
1758711174 | ||
|
|
12879f9553 | ||
|
|
d6a358e851 | ||
|
|
9e7d78fd80 | ||
|
|
570154a4f1 | ||
|
|
5564250e7d | ||
|
|
8d65f8ee92 | ||
|
|
11bf711838 | ||
|
|
a845d4f237 | ||
|
|
02c3a88ed6 | ||
|
|
a8a3e5b303 | ||
|
|
cec5af55d9 | ||
|
|
d04ba7563a | ||
|
|
0450f004d3 | ||
|
|
24a899bba0 | ||
|
|
6c75296a02 | ||
|
|
6cca0d3ab2 | ||
|
|
3d34663eda | ||
|
|
4bbf4c8548 | ||
|
|
8685092260 | ||
|
|
622464a8a4 | ||
|
|
be3a8a6949 | ||
|
|
3acb65d42c | ||
|
|
6d3a8c5a88 | ||
|
|
b65b972353 | ||
|
|
8c31ca1bad | ||
|
|
8cb21b6321 | ||
|
|
6d6eb98562 | ||
|
|
aca6a667f3 | ||
|
|
d79e63f673 | ||
|
|
e67d2df677 | ||
|
|
d44da67dc4 | ||
|
|
9d45e80856 | ||
|
|
be66c8bfb9 | ||
|
|
e26a0f949d | ||
|
|
3e991e6e43 | ||
|
|
0337a0300c | ||
|
|
ee8e0eb733 | ||
|
|
39cca9ac25 | ||
|
|
95140986b9 | ||
|
|
034f1577c2 | ||
|
|
c3645fd760 | ||
|
|
b1b236f736 | ||
|
|
0b79b65575 | ||
|
|
067ed96de4 | ||
|
|
f25e5d19a4 | ||
|
|
0380d4a17a | ||
|
|
89dc4e5052 | ||
|
|
12492a522f | ||
|
|
5d9638758b | ||
|
|
6b064bdef9 | ||
|
|
7aa895a2b7 | ||
|
|
35f2f9e93f | ||
|
|
f0a5f02588 | ||
|
|
c4a6a337a3 | ||
|
|
44a4c9346b | ||
|
|
bcf533af62 | ||
|
|
aa8005a982 | ||
|
|
2d6ad99f6f | ||
|
|
1e1747a355 | ||
|
|
6ba0bd7739 | ||
|
|
b4088f4612 | ||
|
|
3c4619d071 | ||
|
|
10e8b296e3 | ||
|
|
b702703472 | ||
|
|
7590dc308c | ||
|
|
2f457ac6b2 | ||
|
|
cbc6743b5a | ||
|
|
af01d80db2 | ||
|
|
b45a10b98f | ||
|
|
cd668c11d8 | ||
|
|
753491a7eb | ||
|
|
fd4706f2d3 | ||
|
|
db580366fd | ||
|
|
487920375b | ||
|
|
42eb52bd88 | ||
|
|
3943164ad6 | ||
|
|
e98dee9f0a | ||
|
|
782cea9995 | ||
|
|
41a0fde036 | ||
|
|
7d453fdca7 | ||
|
|
c935fce27b | ||
|
|
ff4b18b73c | ||
|
|
cf78f6aa6a | ||
|
|
0ad3418239 | ||
|
|
d501339274 | ||
|
|
ad2f7358db | ||
|
|
a7ca161271 | ||
|
|
3a1f28667b | ||
|
|
ef1c47bac5 | ||
|
|
aa7a073b4a | ||
|
|
544e474e98 | ||
|
|
4b16bac5dc | ||
|
|
ea7266c04c | ||
|
|
c18dbea3cd | ||
|
|
f19f27510b | ||
|
|
57e32bab16 | ||
|
|
e63fe92cd9 | ||
|
|
c95126adf5 | ||
|
|
54a0e457b4 | ||
|
|
8436f964e4 | ||
|
|
709cb87c2b | ||
|
|
e0551251d3 | ||
|
|
dae0b5dc08 | ||
|
|
077276463f | ||
|
|
43a8baee3d | ||
|
|
91fe227e8c | ||
|
|
3f526873d8 | ||
|
|
def4c60395 | ||
|
|
6c02e97157 | ||
|
|
c25b130b7d | ||
|
|
c266a675a2 | ||
|
|
6b151eae1b | ||
|
|
0615902632 | ||
|
|
b773fb582d | ||
|
|
9ddf449ea7 | ||
|
|
8a14519467 | ||
|
|
321b9da9a8 | ||
|
|
594efc6204 | ||
|
|
c819554e54 | ||
|
|
3e12372d9c | ||
|
|
0fb7b5e983 | ||
|
|
9d061c6406 | ||
|
|
5f349775a8 | ||
|
|
4e04fdd657 | ||
|
|
c418ab03a2 | ||
|
|
8ffd341443 | ||
|
|
72224717f9 | ||
|
|
d4237aa596 | ||
|
|
ae72cccbb9 | ||
|
|
3389f3118a | ||
|
|
15ccd5db5b | ||
|
|
a28eb4b203 | ||
|
|
f921996563 | ||
|
|
71bd7e89d4 | ||
|
|
02f2cd9747 | ||
|
|
af6a1b03cd | ||
|
|
b248d9ea31 | ||
|
|
3851d284bf | ||
|
|
b520e234a1 | ||
|
|
b40cd2db74 | ||
|
|
fc4bfc35d7 | ||
|
|
769e684c61 | ||
|
|
25c37bb2fa | ||
|
|
ab06aa00ed | ||
|
|
1fa5793ec4 | ||
|
|
5a78685ac9 | ||
|
|
ae161e8b48 | ||
|
|
8d2824fe82 | ||
|
|
822261c922 | ||
|
|
3e4b49b4be | ||
|
|
c5d6d2c76e | ||
|
|
2d9c185b4e | ||
|
|
70af35dbf5 | ||
|
|
7dcf98a178 | ||
|
|
089dea8441 | ||
|
|
c7b61febc3 | ||
|
|
8b8cd52f64 | ||
|
|
eb8d3ca96e | ||
|
|
7e6768208d | ||
|
|
3980c9a876 | ||
|
|
ca849c94e2 | ||
|
|
430e5d75b0 | ||
|
|
5f294356a9 | ||
|
|
4a6fcdb8e5 | ||
|
|
52394cc550 | ||
|
|
f7036143a4 | ||
|
|
7d0367dc58 | ||
|
|
f872ea1a23 | ||
|
|
016948d852 | ||
|
|
7724d6443e | ||
|
|
7013e57d1e | ||
|
|
639a69d579 | ||
|
|
b2c05e4397 | ||
|
|
f76f43ca0e | ||
|
|
a322b8497e | ||
|
|
b75eecabd2 | ||
|
|
4aaffc53a8 | ||
|
|
a2a793f84e | ||
|
|
d551f372e4 | ||
|
|
04f4815f04 | ||
|
|
cec2ffd3eb | ||
|
|
1b56c86e07 | ||
|
|
8b1b37b727 | ||
|
|
f45588e811 | ||
|
|
7f34b63569 | ||
|
|
db4855737b | ||
|
|
ee7241da3f | ||
|
|
3cd4f340a1 | ||
|
|
a7c12d93d3 | ||
|
|
e22e19552a |
14
.env
@@ -1,14 +1,2 @@
|
||||
# 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" ] ]
|
||||
|
||||
@@ -1,14 +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" ] ]
|
||||
# 开发环境代理
|
||||
VITE_PROXY_DOMAIN = /api
|
||||
|
||||
# 开发环境路由历史模式
|
||||
VITE_ROUTER_HISTORY = "hash"
|
||||
|
||||
# 开发环境后端地址
|
||||
VITE_PROXY_DOMAIN_REAL = "http://127.0.0.1:3000"
|
||||
@@ -1,2 +1,11 @@
|
||||
# public path
|
||||
VITE_PUBLIC_PATH = /manages/
|
||||
# 线上环境项目打包路径
|
||||
VITE_PUBLIC_PATH = /
|
||||
|
||||
# 线上环境路由历史模式
|
||||
VITE_ROUTER_HISTORY = "hash"
|
||||
|
||||
# 线上环境后端地址
|
||||
VITE_PROXY_DOMAIN_REAL = ""
|
||||
|
||||
# 是否为打包后的文件提供传统浏览器兼容性支持 支持 true 不支持 false
|
||||
VITE_LEGACY = false
|
||||
@@ -70,6 +70,12 @@ module.exports = {
|
||||
argsIgnorePattern: "^_",
|
||||
varsIgnorePattern: "^_"
|
||||
}
|
||||
],
|
||||
"prettier/prettier": [
|
||||
"error",
|
||||
{
|
||||
endOfLine: "auto"
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
16
.github/workflows/linter.yml
vendored
@@ -45,11 +45,21 @@ jobs:
|
||||
# Full git history is needed to get a proper list of changed files within `super-linter`
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: "16"
|
||||
registry-url: https://registry.npmjs.com/
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: latest
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
yarn install
|
||||
yarn lint
|
||||
yarn build
|
||||
pnpm install
|
||||
pnpm lint
|
||||
env:
|
||||
VALIDATE_ALL_CODEBASE: false
|
||||
DEFAULT_BRANCH: main
|
||||
|
||||
7
.gitignore
vendored
@@ -5,9 +5,10 @@ dist-ssr
|
||||
*.local
|
||||
.eslintcache
|
||||
|
||||
yarn.lock
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-error.log*
|
||||
.pnpm-debug.log
|
||||
tests/**/coverage/
|
||||
|
||||
# Editor directories and files
|
||||
@@ -15,4 +16,4 @@ tests/**/coverage/
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sln
|
||||
|
||||
@@ -3,7 +3,7 @@ command_exists () {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Workaround for Windows 10, Git Bash and Yarn
|
||||
# Workaround for Windows 10, Git Bash and Pnpm
|
||||
if command_exists winpty && test -t 1; then
|
||||
exec < /dev/tty
|
||||
fi
|
||||
|
||||
11
.markdownlint.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"default": true,
|
||||
"MD003": false,
|
||||
"MD033": false,
|
||||
"MD013": false,
|
||||
"MD001": false,
|
||||
"MD025": false,
|
||||
"MD024": false,
|
||||
"MD007": { "indent": 4 },
|
||||
"no-hard-tabs": false
|
||||
}
|
||||
6
.vscode/settings.json
vendored
@@ -9,7 +9,6 @@
|
||||
"terminal.integrated.rendererType": "dom",
|
||||
"editor.formatOnType": true,
|
||||
"editor.formatOnSave": true,
|
||||
"window.zoomLevel": 1,
|
||||
"javascript.updateImportsOnFileMove.enabled": "always",
|
||||
"[vue]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
@@ -43,5 +42,8 @@
|
||||
"cSpell.userWords": ["sourcemap", "vite"],
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
}
|
||||
},
|
||||
"volar.tsPlugin": true,
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"i18n-ally.localesPaths": ["src/plugins/i18n"]
|
||||
}
|
||||
|
||||
20
.vscode/vue3.2.setup-snippets
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"Vue3.2快速生成模板": {
|
||||
"prefix": "Vue3.2",
|
||||
"body": [
|
||||
"<!-- $1 -->",
|
||||
"<script setup lang='ts'>",
|
||||
"\t$2",
|
||||
"</script>\n",
|
||||
"<template>",
|
||||
"\t<div>",
|
||||
"\t\t$3",
|
||||
"\t</div>",
|
||||
"</template>\n",
|
||||
"<style scoped>",
|
||||
"\t$4",
|
||||
"</style>"
|
||||
],
|
||||
"description": "Vue3.2"
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,36 @@
|
||||
# 2.7.0(2021-12-18)
|
||||
|
||||
### 🎫 Feat
|
||||
|
||||
- New tab reuse
|
||||
- New message reminder template
|
||||
- Added front-end menu tree structure example
|
||||
- Refactor routing, optimize permissions modules, and bring a more convenient experience
|
||||
- Refactor the env environment and http request to bring a more convenient experience
|
||||
- Currently, the tabs of the platform are forced to associate with local storage. The next step is to put the tabs in the memory by default and support configurable persistent tabs
|
||||
- Navigation menu icons support fontawesome, iconfont, remixicon, element-plus/icons, custom svg
|
||||
- Update font-awesome to version 5.0, because versions below 5.0 are no longer officially maintained, but the platform will still be compatible with font-awesome4 version
|
||||
|
||||
### 🍏 Perf
|
||||
|
||||
- Optimize the tab page to bring a better interactive experience
|
||||
- Routing title supports direct writing in Chinese, which can be separated from internationalization
|
||||
- Route history mode is read from env and supports base parameter
|
||||
- Packaged files provide traditional browser compatibility support, configure VITE_LEGACY to true
|
||||
|
||||
# 2.6.0(2021-11-10)
|
||||
|
||||
### 🎫 Feat
|
||||
|
||||
- Refactored navigation theme color, supports multiple color schemes
|
||||
- Refactored login page, illustration style
|
||||
|
||||
### 🍏 Perf
|
||||
|
||||
- Optimize the navigation style
|
||||
- Eliminate strong navigation dependence on vxe-table
|
||||
- Synchronously update element-plus, replace Font Icon with SVG Icon
|
||||
|
||||
# 2.1.0(2021-10-14)
|
||||
|
||||
### 🎫 Feat
|
||||
|
||||
33
CHANGELOG.md
@@ -1,3 +1,36 @@
|
||||
# 2.7.0(2021-12-18)
|
||||
|
||||
### 🎫 Feat
|
||||
|
||||
- New tab reuse
|
||||
- New message reminder template
|
||||
- Added front-end menu tree structure example
|
||||
- Refactor routing, optimize permissions modules, and bring a more convenient experience
|
||||
- Refactor the env environment and http request to bring a more convenient experience
|
||||
- Currently, the tabs of the platform are forced to associate with local storage. The next step is to put the tabs in the memory by default and support configurable persistent tabs
|
||||
- Navigation menu icons support fontawesome, iconfont, remixicon, element-plus/icons, custom svg
|
||||
- Update font-awesome to version 5.0, because versions below 5.0 are no longer officially maintained, but the platform will still be compatible with font-awesome4 version
|
||||
|
||||
### 🍏 Perf
|
||||
|
||||
- Optimize the tab page to bring a better interactive experience
|
||||
- Routing title supports direct writing in Chinese, which can be separated from internationalization
|
||||
- Route history mode is read from env and supports base parameter
|
||||
- Packaged files provide traditional browser compatibility support, configure VITE_LEGACY to true
|
||||
|
||||
# 2.6.0(2021-11-10)
|
||||
|
||||
### 🎫 Feat
|
||||
|
||||
- Refactored navigation theme color, supports multiple color schemes
|
||||
- Refactored login page, illustration style
|
||||
|
||||
### 🍏 Perf
|
||||
|
||||
- Optimize the navigation style
|
||||
- Eliminate strong navigation dependence on vxe-table
|
||||
- Synchronously update element-plus, replace Font Icon with SVG Icon
|
||||
|
||||
# 2.1.0(2021-10-14)
|
||||
|
||||
### 🎫 Feat
|
||||
|
||||
@@ -1,3 +1,36 @@
|
||||
# 2.7.0(2021-12-18)
|
||||
|
||||
### 🎫 Feat
|
||||
|
||||
- 新增标签页复用
|
||||
- 新增消息提醒模版
|
||||
- 新增前端菜单树结构例子
|
||||
- 重构路由,优化权限模块,带来更方便的体验
|
||||
- 重构 env 环境和 http 请求,带来更方便的体验
|
||||
- 目前平台的标签页强制关联了本地存储,下一步标签页默认放到内存中并支持可配置持久化标签页
|
||||
- 导航菜单图标支持 fontawesome、iconfont、remixicon、element-plus/icons、自定义 svg
|
||||
- 更新 font-awesome 到 5.0 版本,因为 5.0 以下的版本官方不再维护,但平台依旧会兼容 font-awesome4 版本
|
||||
|
||||
### 🍏 Perf
|
||||
|
||||
- 优化标签页,带来更好的交互体验
|
||||
- 路由 title 支持直接写中文,可脱离国际化
|
||||
- 路由历史模式从 env 读取并支持 base 参数
|
||||
- 打包后的文件提供传统浏览器兼容性支持,配置 VITE_LEGACY 为 true
|
||||
|
||||
# 2.6.0(2021-11-10)
|
||||
|
||||
### 🎫 Feat
|
||||
|
||||
- 重构导航主题色,支持多种配色
|
||||
- 重构登录页,插画风格
|
||||
|
||||
### 🍏 Perf
|
||||
|
||||
- 优化导航样式
|
||||
- 剔除导航强依赖 vxe-table
|
||||
- 同步更新 element-plus,使用 SVG Icon 替换 Font Icon
|
||||
|
||||
# 2.1.0(2021-10-14)
|
||||
|
||||
### 🎫 Feat
|
||||
|
||||
@@ -6,32 +6,28 @@
|
||||
|
||||
## Introduction
|
||||
|
||||
vue-pure-admin is a free and open source middle and back-end template. Using the latest `vue3`, `vite2`, `TypeScript`, `Element-Plus` and other mainstream technology development, the out-of-the-box middle and back-end front-end solutions can also be used for learning reference.
|
||||
vue-pure-admin is a free and open source middle and back-end template. Using the latest `vue3` `vite2` `Element-Plus` `TypeScript` and other mainstream technology development, the out-of-the-box middle and back-end front-end solutions can also be used for learning reference.
|
||||
|
||||
## Supporting Video
|
||||
|
||||
Tutorial: <https://www.bilibili.com/video/BV1534y1S7HV/>
|
||||
UI Design: <https://www.bilibili.com/video/BV17g411T7rq/>
|
||||
|
||||
## Docs
|
||||
|
||||
<https://pure-admin-doc.vercel.app/>
|
||||
|
||||
## Thin
|
||||
|
||||
Github Address: <https://github.com/xiaoxian521/pure-admin-thin>
|
||||
|
||||
## Preview
|
||||
|
||||
- [vue-pure-admin](http://yiming_chang.gitee.io/manages)
|
||||
|
||||
Click to log in without password
|
||||
|
||||
<p align="center">
|
||||
<img alt="PureAdmin Logo" width="100%" src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f5ee80eee1014fb4a53c5bb37574a5f5~tplv-k3u1fbpfcp-watermark.image">
|
||||
<img alt="PureAdmin Logo" width="100%" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dec0672a62e141f3b7f626c22ff6c7ef~tplv-k3u1fbpfcp-watermark.image">
|
||||
<img alt="PureAdmin Logo" width="100%" src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f586f1353de74b1b88cc9f89fce2146e~tplv-k3u1fbpfcp-watermark.image">
|
||||
<img alt="PureAdmin Logo" width="100%" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a28fc0af7ac44e3b8f30469cba4a9993~tplv-k3u1fbpfcp-watermark.image">
|
||||
<img alt="PureAdmin Logo" width="100%" src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a17e54329cda4d76aa9c1c4f2a4715d3~tplv-k3u1fbpfcp-watermark.image">
|
||||
<img alt="PureAdmin Logo" width="100%" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d884fb611da74ee0bdc17c29014d0260~tplv-k3u1fbpfcp-watermark.image">
|
||||
<img alt="PureAdmin Logo" width="100%" src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/abed44ac1f2744e897c28d1689bcb517~tplv-k3u1fbpfcp-watermark.image">
|
||||
<img alt="PureAdmin Logo" width="100%" src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cf2f068650f44a0787c699f5a20c75a6~tplv-k3u1fbpfcp-watermark.image">
|
||||
<img alt="PureAdmin Logo" width="100%" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/73c575dd06474731ad8ab9d853f1ddfd~tplv-k3u1fbpfcp-watermark.image">
|
||||
<img alt="PureAdmin Logo" width="100%" src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/99389b90f5ac4db9b0d61d99dd9a1454~tplv-k3u1fbpfcp-watermark.image">
|
||||
<img alt="PureAdmin Logo" width="100%" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f1546f1c6014446db6c9983934aedc86~tplv-k3u1fbpfcp-watermark.image">
|
||||
<img alt="PureAdmin Logo" width="100%" src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cfb0093b77c34e87b094daaa4304bc2d~tplv-k3u1fbpfcp-watermark.image">
|
||||
<img alt="PureAdmin Logo" width="100%" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fe133cc6db3245f9b1b37f231d040550~tplv-k3u1fbpfcp-watermark.image">
|
||||
<img alt="PureAdmin Logo" width="100%" src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d3110a12d63e4f6fb314e60bf18bdb66~tplv-k3u1fbpfcp-watermark.image">
|
||||
<img alt="PureAdmin Logo" width="100%" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/45b93ec453e3406a939affe65ddcc803~tplv-k3u1fbpfcp-watermark.image">
|
||||
<img alt="PureAdmin Logo" width="100%" src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cdc2dc88c1ef4aafbaaade820442c986~tplv-k3u1fbpfcp-watermark.image">
|
||||
<img alt="PureAdmin Logo" width="100%" src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f4c91b162206485b88cc58a72ff54a01~tplv-k3u1fbpfcp-watermark.image">
|
||||
<img alt="PureAdmin Logo" width="100%" src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b4857fc7eb7d4c0f8deeefc644c1f7dd~tplv-k3u1fbpfcp-watermark.awebp?">
|
||||
<img alt="PureAdmin Logo" width="100%" src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/549c3184697f4d268a78c9833e5ec2ea~tplv-k3u1fbpfcp-watermark.awebp?">
|
||||
</p>
|
||||
|
||||
### Use Gitpod
|
||||
@@ -55,20 +51,20 @@ git clone https://github.com.cnpmjs.org/xiaoxian521/vue-pure-admin.git
|
||||
```bash
|
||||
cd vue-pure-admin
|
||||
|
||||
yarn install
|
||||
pnpm install
|
||||
|
||||
```
|
||||
|
||||
- run
|
||||
|
||||
```bash
|
||||
yarn serve
|
||||
pnpm serve
|
||||
```
|
||||
|
||||
- build
|
||||
|
||||
```bash
|
||||
yarn build
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## Change Log
|
||||
@@ -123,14 +119,24 @@ Support modern browsers, not IE
|
||||
|
||||
If you think this project is helpful to you, you can help the author buy a cup of coffee to show your support!
|
||||
|
||||

|
||||
|
||||
## Exchange Group
|
||||
|
||||
Please scan the code to join the WeChat exchange group, if you have any questions, you can communicate in the group!
|
||||
|
||||

|
||||
<img src="http://yiming_chang.gitee.io/manages/pay.png" width="360px" height="480px" />
|
||||
|
||||
## License
|
||||
|
||||
In principle, no fees and copyrights are charged, so you can use it with confidence
|
||||
|
||||
[MIT © xiaoxian521-2020](./LICENSE)
|
||||
|
||||
## Backers
|
||||
|
||||
Thank you very much for your support, I believe the project will get better and better! ! ! :heart:
|
||||
|
||||
| xueyuheng | taolei1990 | hang-kim | madwolfcrazy | limuen |
|
||||
| :--------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://github.com/xueyuheng"><img src="https://avatars.githubusercontent.com/u/48202935?v=4" width="60px" height="60px" /></a> | <a href="https://github.com/taolei1990"><img src="https://avatars.githubusercontent.com/u/23173640?v=4" width="60px" height="60px" /></a> | <a href="https://github.com/hang-kim"><img src="https://avatars.githubusercontent.com/u/52914259?v=4" width="60px" height="60px" /></a> | <a href="https://github.com/madwolfcrazy"><img src="https://avatars.githubusercontent.com/u/223671?v=4" width="60px" height="60px" /></a> | <a href="https://github.com/limuen"><img src="https://avatars.githubusercontent.com/u/31790606?v=4" width="60px" height="60px" /></a> |
|
||||
|
||||
## Contributors
|
||||
|
||||
This project exists thanks to all the people who contribute!!! :heart:
|
||||
|
||||
<a href="https://github.com/xiaoxian521/vue-pure-admin/graphs/contributors"><img src="https://contrib.rocks/image?repo=xiaoxian521/vue-pure-admin" /></a>
|
||||
|
||||
70
README.md
@@ -6,32 +6,28 @@
|
||||
|
||||
## 简介
|
||||
|
||||
vue-pure-admin 是一个免费开源的中后台模版。使用了最新的`vue3`,`vite2`,`TypeScript`,`Element-Plus`等主流技术开发,开箱即用的中后台前端解决方案,也可用于学习参考。
|
||||
vue-pure-admin 是一个免费开源的中后台模版。使用了最新的`vue3` `vite2` `Element-Plus` `TypeScript`等主流技术开发,开箱即用的中后台前端解决方案,也可用于学习参考。
|
||||
|
||||
## 配套视频
|
||||
|
||||
教程:<https://www.bilibili.com/video/BV1534y1S7HV/>
|
||||
UI 设计:<https://www.bilibili.com/video/BV17g411T7rq/>
|
||||
|
||||
## 配套文档
|
||||
|
||||
<https://pure-admin-doc.vercel.app/>
|
||||
|
||||
## 精简版
|
||||
|
||||
仓库地址:<https://github.com/xiaoxian521/pure-admin-thin>
|
||||
|
||||
## 预览
|
||||
|
||||
- [vue-pure-admin](http://yiming_chang.gitee.io/manages)
|
||||
|
||||
点击免密登录
|
||||
|
||||
<p align="center">
|
||||
<img alt="PureAdmin Logo" width="100%" src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f5ee80eee1014fb4a53c5bb37574a5f5~tplv-k3u1fbpfcp-watermark.image">
|
||||
<img alt="PureAdmin Logo" width="100%" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dec0672a62e141f3b7f626c22ff6c7ef~tplv-k3u1fbpfcp-watermark.image">
|
||||
<img alt="PureAdmin Logo" width="100%" src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f586f1353de74b1b88cc9f89fce2146e~tplv-k3u1fbpfcp-watermark.image">
|
||||
<img alt="PureAdmin Logo" width="100%" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a28fc0af7ac44e3b8f30469cba4a9993~tplv-k3u1fbpfcp-watermark.image">
|
||||
<img alt="PureAdmin Logo" width="100%" src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a17e54329cda4d76aa9c1c4f2a4715d3~tplv-k3u1fbpfcp-watermark.image">
|
||||
<img alt="PureAdmin Logo" width="100%" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d884fb611da74ee0bdc17c29014d0260~tplv-k3u1fbpfcp-watermark.image">
|
||||
<img alt="PureAdmin Logo" width="100%" src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/abed44ac1f2744e897c28d1689bcb517~tplv-k3u1fbpfcp-watermark.image">
|
||||
<img alt="PureAdmin Logo" width="100%" src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cf2f068650f44a0787c699f5a20c75a6~tplv-k3u1fbpfcp-watermark.image">
|
||||
<img alt="PureAdmin Logo" width="100%" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/73c575dd06474731ad8ab9d853f1ddfd~tplv-k3u1fbpfcp-watermark.image">
|
||||
<img alt="PureAdmin Logo" width="100%" src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/99389b90f5ac4db9b0d61d99dd9a1454~tplv-k3u1fbpfcp-watermark.image">
|
||||
<img alt="PureAdmin Logo" width="100%" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f1546f1c6014446db6c9983934aedc86~tplv-k3u1fbpfcp-watermark.image">
|
||||
<img alt="PureAdmin Logo" width="100%" src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cfb0093b77c34e87b094daaa4304bc2d~tplv-k3u1fbpfcp-watermark.image">
|
||||
<img alt="PureAdmin Logo" width="100%" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fe133cc6db3245f9b1b37f231d040550~tplv-k3u1fbpfcp-watermark.image">
|
||||
<img alt="PureAdmin Logo" width="100%" src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d3110a12d63e4f6fb314e60bf18bdb66~tplv-k3u1fbpfcp-watermark.image">
|
||||
<img alt="PureAdmin Logo" width="100%" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/45b93ec453e3406a939affe65ddcc803~tplv-k3u1fbpfcp-watermark.image">
|
||||
<img alt="PureAdmin Logo" width="100%" src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cdc2dc88c1ef4aafbaaade820442c986~tplv-k3u1fbpfcp-watermark.image">
|
||||
<img alt="PureAdmin Logo" width="100%" src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f4c91b162206485b88cc58a72ff54a01~tplv-k3u1fbpfcp-watermark.image">
|
||||
<img alt="PureAdmin Logo" width="100%" src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b4857fc7eb7d4c0f8deeefc644c1f7dd~tplv-k3u1fbpfcp-watermark.awebp?">
|
||||
<img alt="PureAdmin Logo" width="100%" src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/549c3184697f4d268a78c9833e5ec2ea~tplv-k3u1fbpfcp-watermark.awebp?">
|
||||
</p>
|
||||
|
||||
### 使用 Gitpod
|
||||
@@ -55,20 +51,20 @@ git clone https://github.com.cnpmjs.org/xiaoxian521/vue-pure-admin.git
|
||||
```bash
|
||||
cd vue-pure-admin
|
||||
|
||||
yarn install
|
||||
pnpm install
|
||||
|
||||
```
|
||||
|
||||
- 运行
|
||||
|
||||
```bash
|
||||
yarn serve
|
||||
pnpm serve
|
||||
```
|
||||
|
||||
- 打包
|
||||
|
||||
```bash
|
||||
yarn build
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## 更新日志
|
||||
@@ -121,16 +117,32 @@ yarn build
|
||||
|
||||
## 捐赠
|
||||
|
||||
如果你觉得这个项目对你有帮助,你可以帮作者买一杯咖啡表示支持!
|
||||
如果你觉得这个项目对你有帮助,你可以帮作者买一杯咖啡表示支持
|
||||
|
||||

|
||||
<img src="http://yiming_chang.gitee.io/manages/pay.jpg" width="150px" height="150px" />
|
||||
|
||||
## 交流群
|
||||
## 付费咨询、需求定制
|
||||
|
||||
请扫码加入微信交流群,有问题可以在群里沟通!
|
||||
作者精力有限,需要提供技术服务的可扫下面的二维码加微信,添加请备注来意
|
||||
|
||||

|
||||
<img src="http://yiming_chang.gitee.io/manages/wechat.jpg" width="150px" height="150px" />
|
||||
|
||||
## License
|
||||
## 许可证
|
||||
|
||||
原则上不收取任何费用及版权,可以放心使用
|
||||
|
||||
[MIT © xiaoxian521-2020](./LICENSE)
|
||||
|
||||
## 捐赠者
|
||||
|
||||
非常感谢你们的支持,相信项目会越来越好!!!:heart:
|
||||
|
||||
| xueyuheng | taolei1990 | hang-kim | madwolfcrazy | limuen |
|
||||
| :--------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://github.com/xueyuheng"><img src="https://avatars.githubusercontent.com/u/48202935?v=4" width="60px" height="60px" /></a> | <a href="https://github.com/taolei1990"><img src="https://avatars.githubusercontent.com/u/23173640?v=4" width="60px" height="60px" /></a> | <a href="https://github.com/hang-kim"><img src="https://avatars.githubusercontent.com/u/52914259?v=4" width="60px" height="60px" /></a> | <a href="https://github.com/madwolfcrazy"><img src="https://avatars.githubusercontent.com/u/223671?v=4" width="60px" height="60px" /></a> | <a href="https://github.com/limuen"><img src="https://avatars.githubusercontent.com/u/31790606?v=4" width="60px" height="60px" /></a> |
|
||||
|
||||
## 贡献者
|
||||
|
||||
这个项目的存在感谢所有做出贡献的人!!!:heart:
|
||||
|
||||
<a href="https://github.com/xiaoxian521/vue-pure-admin/graphs/contributors"><img src="https://contrib.rocks/image?repo=xiaoxian521/vue-pure-admin" /></a>
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
// 处理环境变量
|
||||
const warpperEnv = (envConf: Recordable): ViteEnv => {
|
||||
const ret: any = {};
|
||||
// 此处为默认值,无需修改
|
||||
const ret: ViteEnv = {
|
||||
VITE_PORT: 8848,
|
||||
VITE_PUBLIC_PATH: "",
|
||||
VITE_PROXY_DOMAIN: "",
|
||||
VITE_PROXY_DOMAIN_REAL: "",
|
||||
VITE_ROUTER_HISTORY: "",
|
||||
VITE_LEGACY: false
|
||||
};
|
||||
|
||||
for (const envName of Object.keys(envConf)) {
|
||||
let realName = envConf[envName].replace(/\\n/g, "\n");
|
||||
@@ -9,13 +18,6 @@ const warpperEnv = (envConf: Recordable): ViteEnv => {
|
||||
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;
|
||||
@@ -25,8 +27,15 @@ const warpperEnv = (envConf: Recordable): ViteEnv => {
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
// 跨域代理重写
|
||||
const regExps = (value: string, reg: string): string => {
|
||||
return value.replace(new RegExp(reg, "g"), "");
|
||||
};
|
||||
|
||||
// 环境变量
|
||||
const loadEnv = (): ViteEnv => {
|
||||
return import.meta.env;
|
||||
};
|
||||
|
||||
export { loadEnv, warpperEnv };
|
||||
export { warpperEnv, regExps, loadEnv };
|
||||
@@ -1,19 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="stylesheet" href="/iconfont.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>后台管理系统</title>
|
||||
<title>vue-pure-admin</title>
|
||||
<script src="/sortable.min.js"></script>
|
||||
<script>
|
||||
window.process = {};
|
||||
|
||||
@@ -7,8 +7,9 @@ const systemRouter = {
|
||||
name: "system",
|
||||
redirect: "/system/user",
|
||||
meta: {
|
||||
icon: "el-icon-setting",
|
||||
icon: "Setting",
|
||||
title: "message.hssysManagement",
|
||||
i18n: true,
|
||||
showLink: true,
|
||||
rank: 6
|
||||
},
|
||||
@@ -18,6 +19,7 @@ const systemRouter = {
|
||||
name: "user",
|
||||
meta: {
|
||||
title: "message.hsBaseinfo",
|
||||
i18n: true,
|
||||
showLink: true
|
||||
}
|
||||
},
|
||||
@@ -26,7 +28,9 @@ const systemRouter = {
|
||||
name: "dict",
|
||||
meta: {
|
||||
title: "message.hsDict",
|
||||
showLink: true
|
||||
i18n: true,
|
||||
showLink: true,
|
||||
keepAlive: true
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -38,7 +42,8 @@ const permissionRouter = {
|
||||
redirect: "/permission/page",
|
||||
meta: {
|
||||
title: "message.permission",
|
||||
icon: "el-icon-lollipop",
|
||||
icon: "Lollipop",
|
||||
i18n: true,
|
||||
showLink: true,
|
||||
rank: 3
|
||||
},
|
||||
@@ -48,6 +53,7 @@ const permissionRouter = {
|
||||
name: "permissionPage",
|
||||
meta: {
|
||||
title: "message.permissionPage",
|
||||
i18n: true,
|
||||
showLink: true
|
||||
}
|
||||
},
|
||||
@@ -56,6 +62,7 @@ const permissionRouter = {
|
||||
name: "permissionButton",
|
||||
meta: {
|
||||
title: "message.permissionButton",
|
||||
i18n: true,
|
||||
showLink: true,
|
||||
authority: []
|
||||
}
|
||||
@@ -63,6 +70,42 @@ const permissionRouter = {
|
||||
]
|
||||
};
|
||||
|
||||
const tabsRouter = {
|
||||
path: "/tabs",
|
||||
name: "reTabs",
|
||||
redirect: "/tabs/index",
|
||||
meta: {
|
||||
icon: "IF-team-icontabs",
|
||||
title: "message.hstabs",
|
||||
i18n: true,
|
||||
showLink: true,
|
||||
rank: 8
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "/tabs/index",
|
||||
name: "reTabs",
|
||||
meta: {
|
||||
title: "message.hstabs",
|
||||
showLink: true,
|
||||
i18n: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/tabs/detail",
|
||||
name: "tabDetail",
|
||||
meta: {
|
||||
title: "",
|
||||
showLink: false,
|
||||
i18n: false,
|
||||
dynamicLevel: 3,
|
||||
realPath: "/tabs/detail",
|
||||
refreshRedirect: "/tabs/index"
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// 添加不同按钮权限到/permission/button页面中
|
||||
function setDifAuthority(authority, routes) {
|
||||
routes.children[1].meta.authority = [authority];
|
||||
@@ -77,12 +120,16 @@ export default [
|
||||
if (query.name === "admin") {
|
||||
return {
|
||||
code: 0,
|
||||
info: [systemRouter, setDifAuthority("v-admin", permissionRouter)]
|
||||
info: [
|
||||
tabsRouter,
|
||||
systemRouter,
|
||||
setDifAuthority("v-admin", permissionRouter)
|
||||
]
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
code: 0,
|
||||
info: [setDifAuthority("v-test", permissionRouter)]
|
||||
info: [tabsRouter, setDifAuthority("v-test", permissionRouter)]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { MockMethod } from "vite-plugin-mock";
|
||||
|
||||
// http://mockjs.com/examples.html#Object
|
||||
const echartsList = (): any => {
|
||||
const result: any[] = [];
|
||||
const echartsList = () => {
|
||||
const result = [];
|
||||
for (let index = 0; index < 200; index++) {
|
||||
result.push(["@date", Math.floor(Math.random() * 300)]);
|
||||
}
|
||||
|
||||
12
mock/map.ts
@@ -1,8 +1,16 @@
|
||||
import { MockMethod } from "vite-plugin-mock";
|
||||
|
||||
type mapType = {
|
||||
plateNumber: string;
|
||||
driver: string;
|
||||
"orientation|1-360": number;
|
||||
"lng|113-114.1-10": number;
|
||||
"lat|34-35.1-10": number;
|
||||
};
|
||||
|
||||
// http://mockjs.com/examples.html#Object
|
||||
const mapList = (): any => {
|
||||
const result: any[] = [];
|
||||
const mapList = (): Array<mapType> => {
|
||||
const result: Array<mapType> = [];
|
||||
for (let index = 0; index < 200; index++) {
|
||||
result.push({
|
||||
plateNumber: "豫A@natural(11111, 99999)@character('upper')",
|
||||
|
||||
136
package.json
@@ -1,36 +1,52 @@
|
||||
{
|
||||
"name": "vue-pure-admin",
|
||||
"version": "2.1.0",
|
||||
"version": "2.7.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">= 16",
|
||||
"pnpm": ">= 6"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "cross-env --max_old_space_size=4096 vite",
|
||||
"serve": "yarn dev",
|
||||
"serve": "pnpm 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",
|
||||
"preview:build": "pnpm build && vite preview",
|
||||
"clean:cache": "rm -rf node_modules && rm -rf .eslintcache && pnpm install",
|
||||
"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"
|
||||
"lint": "pnpm lint:eslint && pnpm lint:prettier && pnpm lint:stylelint && pnpm lint:pretty",
|
||||
"prepare": "husky install",
|
||||
"preinstall": "npx only-allow pnpm"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"not ie 11",
|
||||
"not op_mini all"
|
||||
],
|
||||
"dependencies": {
|
||||
"@amap/amap-jsapi-loader": "^1.0.1",
|
||||
"@logicflow/core": "^0.4.6",
|
||||
"@logicflow/extension": "^0.4.6",
|
||||
"@vueuse/core": "^6.5.3",
|
||||
"@element-plus/icons-vue": "^0.2.4",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.36",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
||||
"@fortawesome/vue-fontawesome": "^3.0.0-5",
|
||||
"@logicflow/core": "0.7.1",
|
||||
"@logicflow/extension": "0.7.1",
|
||||
"@vueuse/core": "^6.7.1",
|
||||
"@vueuse/motion": "^2.0.0-beta.4",
|
||||
"animate.css": "^4.1.1",
|
||||
"await-to-js": "^3.0.0",
|
||||
"axios": "^0.21.1",
|
||||
"cropperjs": "^1.5.11",
|
||||
"dayjs": "^1.10.7",
|
||||
"echarts": "^5.2.1",
|
||||
"element-plus": "1.1.0-beta.20",
|
||||
"element-plus": "1.2.0-beta.6",
|
||||
"element-resize-detector": "^1.2.3",
|
||||
"font-awesome": "^4.7.0",
|
||||
"js-cookie": "^3.0.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lowdb": "^3.0.0",
|
||||
"mitt": "^3.0.0",
|
||||
@@ -38,65 +54,71 @@
|
||||
"nprogress": "^0.2.0",
|
||||
"path": "^0.12.7",
|
||||
"path-to-regexp": "^6.2.0",
|
||||
"pinia": "2.0.0-rc.10",
|
||||
"pinia": "^2.0.0-rc.14",
|
||||
"qs": "^6.10.1",
|
||||
"remixicon": "^2.5.0",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"responsive-storage": "^1.0.11",
|
||||
"sortablejs": "1.13.0",
|
||||
"typescript-cookie": "^1.0.0",
|
||||
"v-contextmenu": "^3.0.0",
|
||||
"vue": "^3.2.20",
|
||||
"v-contextmenu": "3.0.0",
|
||||
"vue": "^3.2.24",
|
||||
"vue-i18n": "^9.2.0-beta.3",
|
||||
"vue-json-pretty": "^2.0.2",
|
||||
"vue-router": "^4.0.11",
|
||||
"vue-router": "^4.0.12",
|
||||
"vue-types": "^4.1.0",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"vxe-table": "^4.0.27",
|
||||
"wangeditor": "^4.7.7",
|
||||
"xe-ajax": "^4.0.5",
|
||||
"xe-utils": "^3.3.1",
|
||||
"xgplayer": "^2.28.0"
|
||||
"vuedraggable": "4.1.0",
|
||||
"vxe-table": "4.0.30",
|
||||
"wangeditor": "^4.7.9",
|
||||
"xe-ajax": "4.0.5",
|
||||
"xe-utils": "3.4.0",
|
||||
"xgplayer": "2.28.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",
|
||||
"@commitlint/cli": "13.1.0",
|
||||
"@commitlint/config-conventional": "13.1.0",
|
||||
"@types/element-resize-detector": "1.1.3",
|
||||
"@types/js-cookie": "^3.0.1",
|
||||
"@types/mockjs": "1.0.3",
|
||||
"@types/node": "14.14.14",
|
||||
"@types/nprogress": "0.2.0",
|
||||
"@types/qs": "^6.9.7",
|
||||
"@typescript-eslint/eslint-plugin": "4.31.0",
|
||||
"@typescript-eslint/parser": "4.31.0",
|
||||
"@vitejs/plugin-legacy": "^1.6.4",
|
||||
"@vitejs/plugin-vue": "^1.10.2",
|
||||
"@vitejs/plugin-vue-jsx": "^1.3.1",
|
||||
"@vue/compiler-sfc": "^3.2.24",
|
||||
"@vue/eslint-config-prettier": "6.0.0",
|
||||
"@vue/eslint-config-typescript": "7.0.0",
|
||||
"@zougt/vite-plugin-theme-preprocessor": "^1.4.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.45.0",
|
||||
"sass-loader": "^12.3.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.3",
|
||||
"vite": "2.6.14",
|
||||
"vite-plugin-mock": "^2.9.6",
|
||||
"vite-plugin-style-import": "^1.2.1",
|
||||
"vite-svg-loader": "^2.2.0",
|
||||
"vue-eslint-parser": "^7.10.0"
|
||||
"vue-eslint-parser": "7.10.0"
|
||||
},
|
||||
"repository": "git@github.com:xiaoxian521/vue-pure-admin.git",
|
||||
"author": "xiaoxian521",
|
||||
|
||||
7806
pnpm-lock.yaml
generated
Normal file
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -1,11 +1,18 @@
|
||||
{
|
||||
"Version": "2.0.0",
|
||||
"Version": "2.7.0",
|
||||
"Title": "PureAdmin",
|
||||
"FixedHeader": true,
|
||||
"HiddenSideBar": false,
|
||||
"MultiTagsCache": false,
|
||||
"KeepAlive": true,
|
||||
"Locale": "zh",
|
||||
"Layout": "vertical-dark",
|
||||
"Layout": "vertical",
|
||||
"Theme": "default",
|
||||
"Grey": false,
|
||||
"Weak": false,
|
||||
"HideTabs": false,
|
||||
"MapConfigure": {
|
||||
"amapKey": "97b3248d1553172e81f168cf94ea667e",
|
||||
"baiduKey": "wTHbkkEweiFqZLKunMIjcrb2RcqNXkhc",
|
||||
"options": {
|
||||
"resizeEnable": true,
|
||||
"center": [113.6401, 34.72468],
|
||||
|
||||
@@ -14,14 +14,8 @@ export default {
|
||||
[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;
|
||||
}
|
||||
return this.$storage.locale?.locale === "zh" ? zhCn : en;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { http } from "../utils/http";
|
||||
|
||||
// 地图数据
|
||||
export const mapJson = (data?: object) => {
|
||||
return http.request("get", "/getMapInfo", data);
|
||||
export const mapJson = (params?: object) => {
|
||||
return http.request("get", "/getMapInfo", { params });
|
||||
};
|
||||
|
||||
// echarts数据
|
||||
export const echartsJson = (data?: object) => {
|
||||
return http.request("get", "/getEchartsInfo", data);
|
||||
export const echartsJson = (params?: object) => {
|
||||
return http.request("get", "/getEchartsInfo", { params });
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { http } from "../utils/http";
|
||||
|
||||
export const getAsyncRoutes = (data?: object) => {
|
||||
return http.request("get", "/getAsyncRoutes", data);
|
||||
export const getAsyncRoutes = (params?: object) => {
|
||||
return http.request("get", "/getAsyncRoutes", { params });
|
||||
};
|
||||
|
||||
@@ -1,16 +1,26 @@
|
||||
import { http } from "../utils/http";
|
||||
|
||||
interface userType extends Promise<any> {
|
||||
svg?: string;
|
||||
code?: number;
|
||||
info?: object;
|
||||
}
|
||||
|
||||
// 获取验证码
|
||||
export const getVerify = () => {
|
||||
export const getVerify = (): userType => {
|
||||
return http.request("get", "/captcha");
|
||||
};
|
||||
|
||||
// 登录
|
||||
export const getLogin = (data: object) => {
|
||||
return http.request("post", "/login", data);
|
||||
return http.request("post", "/login", { data });
|
||||
};
|
||||
|
||||
// 注册
|
||||
export const getRegist = (data: object) => {
|
||||
return http.request("post", "/register", data);
|
||||
// 刷新token
|
||||
export const refreshToken = (data: object) => {
|
||||
return http.request("post", "/refreshToken", { data });
|
||||
};
|
||||
|
||||
// export const searchVague = (data: object) => {
|
||||
// return http.request("post", "/searchVague", { data });
|
||||
// };
|
||||
|
||||
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 680 KiB |
BIN
src/assets/avatars.jpg
Normal file
|
After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -1,8 +1,8 @@
|
||||
@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");
|
||||
src: url("iconfont.woff2?t=1638023560828") format("woff2"),
|
||||
url("iconfont.woff?t=1638023560828") format("woff"),
|
||||
url("iconfont.ttf?t=1638023560828") format("truetype");
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
@@ -13,8 +13,12 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.team-iconzuixinlianzai::before {
|
||||
content: "\e6da";
|
||||
.team-icontabs::before {
|
||||
content: "\e63e";
|
||||
}
|
||||
|
||||
.team-iconlogo::before {
|
||||
content: "\e620";
|
||||
}
|
||||
|
||||
.team-iconxinpin::before {
|
||||
@@ -25,22 +29,6 @@
|
||||
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";
|
||||
}
|
||||
|
||||
@@ -6,11 +6,18 @@
|
||||
"description": "pure-admin",
|
||||
"glyphs": [
|
||||
{
|
||||
"icon_id": "2508809",
|
||||
"name": "最新连载",
|
||||
"font_class": "zuixinlianzai",
|
||||
"unicode": "e6da",
|
||||
"unicode_decimal": 59098
|
||||
"icon_id": "20594647",
|
||||
"name": "标签页",
|
||||
"font_class": "tabs",
|
||||
"unicode": "e63e",
|
||||
"unicode_decimal": 58942
|
||||
},
|
||||
{
|
||||
"icon_id": "22129506",
|
||||
"name": "水能",
|
||||
"font_class": "logo",
|
||||
"unicode": "e620",
|
||||
"unicode_decimal": 58912
|
||||
},
|
||||
{
|
||||
"icon_id": "7795613",
|
||||
@@ -26,34 +33,6 @@
|
||||
"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": "全屏缩小",
|
||||
|
||||
|
Before Width: | Height: | Size: 9.9 KiB |
1
src/assets/login/avatar.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1636193306629" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1847" width="32" height="32"><path d="M410.558481 0.10861C410.558481 211.083075 109.682285 361.860579 109.682285 633.656511c0 174.943176 134.703259 316.787527 300.876196 316.787527s300.876197-141.817198 300.876197-316.787527C711.407525 361.751969 410.558481 210.974465 410.558481 0.10861z" fill="#386BF3" p-id="1848"></path><path d="M613.468671 73.664572c0 211.055922-300.876197 361.914883-300.876196 633.547901 0 174.943176 134.703259 316.787527 300.876196 316.787527s300.876197-141.817198 300.876197-316.787527c-0.054305-271.633018-300.876197-422.491979-300.876197-633.547901z" fill="#C3D2FB" p-id="1849"></path><path d="M312.592475 707.212473c0-183.713414 137.635722-312.171612 226.72288-441.390078 81.701694 106.111739 172.119322 218.740063 172.119323 367.725506a309.755045 309.755045 0 0 1-291.074166 316.516003 323.114046 323.114046 0 0 1-107.768037-242.851431z" fill="#303F5B" p-id="1850"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
BIN
src/assets/login/bg.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
1
src/assets/login/illustration0.svg
Normal file
|
After Width: | Height: | Size: 20 KiB |
1
src/assets/login/illustration1.svg
Normal file
|
After Width: | Height: | Size: 17 KiB |
1
src/assets/login/illustration2.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" data-name="Layer 1" width="500" height="380" viewBox="0 0 896 529.1129" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M832.06729,623.22778s-26.37759,9.89441-38.806,32.94348S787.06,706.69574,787.06,706.69574s26.37759-9.89447,38.806-32.94348S832.06729,623.22778,832.06729,623.22778Z" transform="translate(-158 -185.8871)" fill="#3f3d56"/><path d="M867.5,657.59637s-8.64182,26.814-31.0802,40.31373-50.17651,8.57293-50.17651,8.57293,8.64175-26.81408,31.08017-40.31378S867.5,657.59637,867.5,657.59637Z" transform="translate(-158 -185.8871)" fill="#5392f0"/><rect y="527.1129" width="896" height="2" fill="#2f2e41"/><path d="M519.87238,620.97461a95.44448,95.44448,0,0,1-35.748-14.44629L485.306,604.915a93.36283,93.36283,0,0,0,34.999,14.10547c18.93164,3.40137,47.26075,1.73144,74.707-25.52735,53.41358-53.04785,104.39307-58.39062,104.90186-58.43847l.18652,1.99219c-.50146.04687-50.76806,5.31738-103.67822,57.86621-21.61328,21.46386-43.792,27.40234-61.71777,27.40234A83.49962,83.49962,0,0,1,519.87238,620.97461Z" transform="translate(-158 -185.8871)" fill="#2f2e41"/><circle cx="515.15271" cy="381.1129" r="12" fill="#2f2e41"/><circle cx="430.15271" cy="437.1129" r="12" fill="#2f2e41"/><path d="M841.5,714s-17.46191-5.41315-52.26129-10.84192L790,692.5c6-60-34-150-34-150a401.561,401.561,0,0,1,21.4693,139.0246C772.13214,672.2124,761.82056,662.16638,742,656c0,0,25.77765,22.106,33.15918,45.10175a997.84042,997.84042,0,0,0-102.02258-8.21589L682,672.5l-17,17s-7-51-22-53l11,50s-13-10-16-9l7.39746,14.79486c-49.819-.51654-109.08453,1.7356-177.76581,8.95227L476,682l-17,17s-7-51-22-53l11,50s-13-10-16-9l8.64288,17.28583Q406.9763,708.2897,370.5,714Z" transform="translate(-158 -185.8871)" fill="#3f3d56"/><path d="M565.64813,230.37817c-10.89964,11.74783,17.59745,40.25959,17.59745,40.25959s-57.70662,9.73051-53.12783,9.14083,2.20622-49.13151,2.20622-49.13151S576.54777,218.63035,565.64813,230.37817Z" transform="translate(-158 -185.8871)" fill="#a0616a"/><path d="M605.81236,356.10945l-50.139,25.6141-27.22969,15.6059s-32.09862,40.43116-38.08709,64.39234,25.92963,68.247,29.54371,72.82286a54.36088,54.36088,0,0,1,4.98908,7.42355c1.24727,1.85589,12.02944-.541,23.80342-3.06554S547.13,518.93875,547.13,518.93875s-15.02732-38.39505-16.14686-39.25912c-1.04554-.807-4.60093-7.44631-2.04309-10.35234a25.94993,25.94993,0,0,0,5.44489-8.89825,30.09064,30.09064,0,0,1,4.18709-7.94151s45.361-36.83645,59.52776-49.37835,51.82952-4.65839,51.82952-4.65839-17.78167,68.20027-22.22979,72.80616-4.929,8.70085-2.91535,16.50759,28.28157.39078,28.28157.39078L662.766,461.6996s15.74879-34.2925,24.29946-69.67451c4.27533-17.691-3.88828-28.23462-13.12073-34.35549a41.39094,41.39094,0,0,0-30.02983-5.97766l-46.34848,8.1308,32.14706-13.84923Z" transform="translate(-158 -185.8871)" fill="#2f2e41"/><path d="M420.87777,290.19133,361.02366,271.685s-24.179-31.16689-12.78824-36.6669,25.65172,26.94419,25.65172,26.94419l41.686,2.69751Z" transform="translate(-158 -185.8871)" fill="#a0616a"/><path d="M672.49431,257.78673l53.2121-33.06768s15.49333-36.27612,3.0807-38.71059-17.98787,32.56435-17.98787,32.56435l-39.64232,13.17143Z" transform="translate(-158 -185.8871)" fill="#a0616a"/><path d="M682.45318,220.40023l1.01427,39.19147-89.68779,16.025c13.19231,28.22441,9.84118,60.34675,43.04725,74.4259L524.9027,404.78717c4.9871-43.03806-15.81748-75.456-35.263-115.75876-23.68547-8.58589-51.19594-2.29078-80.33649,10.34619l-5.237-40.66416,123.87841-8.896,20.34848,7.77932,21.81842-9.17677C602.17891,238.88953,648.22076,220.77584,682.45318,220.40023Z" transform="translate(-158 -185.8871)" fill="#5392f0"/><path d="M626.64006,486.51727c-2.72,2.36681-5.25213,21.84984-5.34982,28.92023s9.21178,8.89624,14.29855,9.2494,4.47816,3.45631,7.83678,6.04854,14.39625,2.179,28.89019-2.71238-9.75274-20.92568-11.86409-21.662-11-22.78156-11-22.78156S629.36,484.15046,626.64006,486.51727Z" transform="translate(-158 -185.8871)" fill="#2f2e41"/><path d="M547.368,531.00717c3.23089,1.60043,10.61681,19.80614,12.50274,26.62107s-6.65716,10.93994-11.48848,12.5704-3.45631,4.47816-6.04855,7.83678-13.3744,5.75546-28.63472,4.696,4.13258-22.71391,5.98847-23.96118,4.86893-24.82526,4.86893-24.82526S544.13715,529.40674,547.368,531.00717Z" transform="translate(-158 -185.8871)" fill="#2f2e41"/><circle cx="389.47074" cy="35.42904" r="23.99585" fill="#a0616a"/><path d="M519.73448,218.90923a22.82668,22.82668,0,0,1-.83378-18.59281c2.35891-5.8153,7.59174-11.65569,18.87309-13.4,24.61387-3.80572,37.71267,13.43543,37.02452,19.07449s-3.99294,19.27051-3.99294,19.27051,1.47587-12.90619-4.85883-13.362-30.90178-2.37835-37.12217,4.145a14.23268,14.23268,0,0,0-3.71042,13.82977Z" transform="translate(-158 -185.8871)" fill="#2f2e41"/></svg>
|
||||
|
After Width: | Height: | Size: 4.6 KiB |
1
src/assets/login/illustration3.svg
Normal file
|
After Width: | Height: | Size: 11 KiB |
1
src/assets/login/illustration4.svg
Normal file
|
After Width: | Height: | Size: 12 KiB |
1
src/assets/login/illustration5.svg
Normal file
|
After Width: | Height: | Size: 29 KiB |
1
src/assets/login/illustration6.svg
Normal file
|
After Width: | Height: | Size: 11 KiB |
1
src/assets/svg/back_top.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M2.88 18.054a35.897 35.897 0 0 1 8.531-16.32.8.8 0 0 1 1.178 0c.166.18.304.332.413.455a35.897 35.897 0 0 1 8.118 15.865c-2.141.451-4.34.747-6.584.874l-2.089 4.178a.5.5 0 0 1-.894 0l-2.089-4.178a44.019 44.019 0 0 1-6.584-.874zm6.698-1.123l1.157.066L12 19.527l1.265-2.53 1.157-.066a42.137 42.137 0 0 0 4.227-.454A33.913 33.913 0 0 0 12 4.09a33.913 33.913 0 0 0-6.649 12.387c1.395.222 2.805.374 4.227.454zM12 15a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0-2a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></svg>
|
||||
|
After Width: | Height: | Size: 608 B |
@@ -1 +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>
|
||||
<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" color="#00000073" 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>
|
||||
|
Before Width: | Height: | Size: 434 B After Width: | Height: | Size: 452 B |
@@ -1 +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>
|
||||
<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" color="#00000073" 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>
|
||||
|
Before Width: | Height: | Size: 403 B After Width: | Height: | Size: 421 B |
@@ -111,7 +111,7 @@ let classOption = reactive({
|
||||
|
||||
.warp {
|
||||
width: 95%;
|
||||
height: 230px;
|
||||
height: 215px;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
|
||||
|
||||
@@ -2,9 +2,15 @@
|
||||
import { ref, unref } from "vue";
|
||||
import { LogicFlow } from "@logicflow/core";
|
||||
|
||||
type nodeListType = {
|
||||
text: string;
|
||||
class: string;
|
||||
type: string;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
lf: LogicFlow;
|
||||
nodeList: ForDataType<undefined>;
|
||||
nodeList: Array<nodeListType>;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
|
||||
@@ -1,5 +1,79 @@
|
||||
import { App } from "vue";
|
||||
import { App, defineComponent } from "vue";
|
||||
import icon from "./src/Icon.vue";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { iconComponents } from "/@/plugins/element-plus";
|
||||
|
||||
/**
|
||||
* find icon component
|
||||
* @param icon icon图标
|
||||
* @returns component
|
||||
*/
|
||||
export function findIconReg(icon: string) {
|
||||
// fontawesome
|
||||
const faReg = /^FA-/;
|
||||
// iconfont
|
||||
const iFReg = /^IF-/;
|
||||
// remixicon
|
||||
const riReg = /^RI-/;
|
||||
// typeof icon === "function" 属于SVG
|
||||
if (faReg.test(icon)) {
|
||||
const text = icon.split(faReg)[1];
|
||||
return findIcon(
|
||||
text.slice(0, text.indexOf(" ")),
|
||||
"FA",
|
||||
text.slice(text.indexOf(" ") + 1, text.length)
|
||||
);
|
||||
} else if (iFReg.test(icon)) {
|
||||
return findIcon(icon.split(iFReg)[1], "IF");
|
||||
} else if (typeof icon === "function") {
|
||||
return findIcon(icon, "SVG");
|
||||
} else if (riReg.test(icon)) {
|
||||
return findIcon(icon.split(riReg)[1], "RI");
|
||||
} else {
|
||||
return findIcon(icon, "EL");
|
||||
}
|
||||
}
|
||||
|
||||
// 支持fontawesome、iconfont、remixicon、element-plus/icons、自定义svg
|
||||
export function findIcon(icon: String, type = "EL", property?: string) {
|
||||
if (type === "FA") {
|
||||
return defineComponent({
|
||||
name: "FaIcon",
|
||||
setup() {
|
||||
return { icon, property };
|
||||
},
|
||||
components: { FontAwesomeIcon },
|
||||
template: `<font-awesome-icon :icon="icon" v-bind:[property]="true" />`
|
||||
});
|
||||
} else if (type === "IF") {
|
||||
return defineComponent({
|
||||
name: "IfIcon",
|
||||
data() {
|
||||
return { icon: `iconfont ${icon}` };
|
||||
},
|
||||
template: `<i :class="icon" />`
|
||||
});
|
||||
} else if (type === "RI") {
|
||||
return defineComponent({
|
||||
name: "RIIcon",
|
||||
data() {
|
||||
return { icon: `ri-${icon}` };
|
||||
},
|
||||
template: `<i :class="icon" />`
|
||||
});
|
||||
} else if (type === "EL") {
|
||||
const components = iconComponents.filter(
|
||||
component => component.name === icon
|
||||
);
|
||||
if (components.length > 0) {
|
||||
return components[0];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else if (type === "SVG") {
|
||||
return icon;
|
||||
}
|
||||
}
|
||||
|
||||
export const Icon = Object.assign(icon, {
|
||||
install(app: App) {
|
||||
|
||||
@@ -1,195 +0,0 @@
|
||||
<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>
|
||||
@@ -3,7 +3,7 @@ import AMapLoader from "@amap/amap-jsapi-loader";
|
||||
import { reactive, getCurrentInstance, onBeforeMount, onUnmounted } from "vue";
|
||||
import { mapJson } from "/@/api/mock";
|
||||
import { deviceDetection } from "/@/utils/deviceDetection";
|
||||
import greenCar from "/@/assets/green.png";
|
||||
import car from "/@/assets/car.png";
|
||||
|
||||
export interface MapConfigureInter {
|
||||
on: Fn;
|
||||
@@ -15,6 +15,10 @@ export interface MapConfigureInter {
|
||||
plugin?: Fn;
|
||||
}
|
||||
|
||||
type resultType = {
|
||||
info: Array<undefined>;
|
||||
};
|
||||
|
||||
export interface mapInter {
|
||||
loading: boolean;
|
||||
}
|
||||
@@ -24,7 +28,7 @@ let map: MapConfigureInter;
|
||||
|
||||
const instance = getCurrentInstance();
|
||||
|
||||
const mapSet: mapInter = reactive({
|
||||
const mapSet = reactive({
|
||||
loading: deviceDetection() ? false : true
|
||||
});
|
||||
|
||||
@@ -72,7 +76,7 @@ onBeforeMount(() => {
|
||||
var { driver, plateNumber, orientation } = data[0];
|
||||
var content = `<img style="transform: scale(1) rotate(${
|
||||
360 - Number(orientation)
|
||||
}deg);" src='${greenCar}' />`;
|
||||
}deg);" src='${car}' />`;
|
||||
marker.setContent(content);
|
||||
marker.setLabel({
|
||||
direction: "bottom",
|
||||
@@ -92,7 +96,7 @@ onBeforeMount(() => {
|
||||
|
||||
// 获取模拟车辆信息
|
||||
mapJson()
|
||||
.then(res => {
|
||||
.then((res: resultType) => {
|
||||
let points: object = res.info.map((v: any) => {
|
||||
return {
|
||||
lnglat: [v.lng, v.lat],
|
||||
@@ -122,19 +126,12 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
id="mapview"
|
||||
ref="mapview"
|
||||
v-loading="mapSet.loading"
|
||||
element-loading-text="地图加载中"
|
||||
element-loading-spinner="el-icon-loading"
|
||||
element-loading-background="rgba(0, 0, 0, 0.8)"
|
||||
></div>
|
||||
<div id="mapview" ref="mapview" v-loading="mapSet.loading"></div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#mapview {
|
||||
height: 100%;
|
||||
height: calc(100vh - 86px);
|
||||
}
|
||||
|
||||
:deep(.amap-marker-label) {
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { App } from "vue";
|
||||
import axios from "axios";
|
||||
import { loadEnv } from "@build/index";
|
||||
|
||||
let config: object = {};
|
||||
const { VITE_PUBLIC_PATH } = loadEnv();
|
||||
|
||||
const setConfig = (cfg?: unknown) => {
|
||||
config = Object.assign(config, cfg);
|
||||
};
|
||||
|
||||
const getConfig = (key?: string) => {
|
||||
const getConfig = (key?: string): ServerConfigs => {
|
||||
if (typeof key === "string") {
|
||||
const arr = key.split(".");
|
||||
if (arr && arr.length) {
|
||||
@@ -30,10 +33,7 @@ export const getServerConfig = async (app: App): Promise<undefined> => {
|
||||
return axios({
|
||||
baseURL: "",
|
||||
method: "get",
|
||||
url:
|
||||
process.env.NODE_ENV === "production"
|
||||
? "/manages/serverConfig.json"
|
||||
: "/serverConfig.json"
|
||||
url: `${VITE_PUBLIC_PATH}serverConfig.json`
|
||||
})
|
||||
.then(({ data: config }) => {
|
||||
let $config = app.config.globalProperties.$config;
|
||||
|
||||
@@ -9,7 +9,7 @@ export const auth: Directive = {
|
||||
const authRoles = value;
|
||||
const hasAuth = usePermissionStoreHook().buttonAuth.includes(authRoles);
|
||||
if (!hasAuth) {
|
||||
el.style.display = "none";
|
||||
el.parentNode.removeChild(el);
|
||||
}
|
||||
} else {
|
||||
throw new Error("need roles! Like v-auth=\"['admin','test']\"");
|
||||
|
||||
@@ -1,48 +1,128 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, getCurrentInstance } from "vue";
|
||||
import {
|
||||
h,
|
||||
ref,
|
||||
computed,
|
||||
Transition,
|
||||
defineComponent,
|
||||
getCurrentInstance
|
||||
} from "vue";
|
||||
import { RouterView } from "vue-router";
|
||||
import backTop from "/@/assets/svg/back_top.svg";
|
||||
import { usePermissionStoreHook } from "/@/store/modules/permission";
|
||||
|
||||
const props = defineProps({
|
||||
fixedHeader: Boolean
|
||||
});
|
||||
const keepAlive: Boolean = ref(
|
||||
getCurrentInstance().appContext.config.globalProperties.$config?.KeepAlive
|
||||
);
|
||||
const instance =
|
||||
getCurrentInstance().appContext.app.config.globalProperties.$storage;
|
||||
|
||||
const transition = computed(() => {
|
||||
const transitions = computed(() => {
|
||||
return route => {
|
||||
return route.meta.transition;
|
||||
};
|
||||
});
|
||||
|
||||
const hideTabs = computed(() => {
|
||||
return instance?.sets.hideTabs;
|
||||
});
|
||||
const layout = computed(() => {
|
||||
return instance?.layout.layout === "vertical";
|
||||
});
|
||||
|
||||
const transitionMain = defineComponent({
|
||||
render() {
|
||||
return h(
|
||||
Transition,
|
||||
{
|
||||
name:
|
||||
transitions.value(this.route) &&
|
||||
this.route.meta.transition.enterTransition
|
||||
? "pure-classes-transition"
|
||||
: (transitions.value(this.route) &&
|
||||
this.route.meta.transition.name) ||
|
||||
"fade-transform",
|
||||
enterActiveClass:
|
||||
transitions.value(this.route) &&
|
||||
`animate__animated ${this.route.meta.transition.enterTransition}`,
|
||||
leaveActiveClass:
|
||||
transitions.value(this.route) &&
|
||||
`animate__animated ${this.route.meta.transition.leaveTransition}`,
|
||||
mode: "out-in",
|
||||
appear: true
|
||||
},
|
||||
{
|
||||
default: () => [this.$slots.default()]
|
||||
}
|
||||
);
|
||||
},
|
||||
props: {
|
||||
route: {
|
||||
type: undefined,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="app-main">
|
||||
<section
|
||||
:class="[props.fixedHeader ? 'app-main' : 'app-main-nofixed-header']"
|
||||
:style="[
|
||||
hideTabs && layout ? 'padding-top: 48px;' : '',
|
||||
!hideTabs && layout ? 'padding-top: 85px;' : '',
|
||||
hideTabs && !layout ? 'padding-top: 48px' : '',
|
||||
!hideTabs && !layout ? 'padding-top: 85px;' : ''
|
||||
]"
|
||||
>
|
||||
<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>
|
||||
<el-scrollbar v-if="props.fixedHeader">
|
||||
<el-backtop title="回到顶部" target=".app-main .el-scrollbar__wrap">
|
||||
<backTop />
|
||||
</el-backtop>
|
||||
<transitionMain :route="route">
|
||||
<keep-alive
|
||||
v-if="keepAlive"
|
||||
:include="usePermissionStoreHook().cachePageList"
|
||||
>
|
||||
<component
|
||||
:is="Component"
|
||||
:key="route.fullPath"
|
||||
class="main-content"
|
||||
/>
|
||||
</keep-alive>
|
||||
<component
|
||||
v-else
|
||||
:is="Component"
|
||||
:key="route.fullPath"
|
||||
class="main-content"
|
||||
/>
|
||||
</transitionMain>
|
||||
</el-scrollbar>
|
||||
<div v-else>
|
||||
<transitionMain :route="route">
|
||||
<keep-alive
|
||||
v-if="keepAlive"
|
||||
:include="usePermissionStoreHook().cachePageList"
|
||||
>
|
||||
<component
|
||||
:is="Component"
|
||||
:key="route.fullPath"
|
||||
class="main-content"
|
||||
/>
|
||||
</keep-alive>
|
||||
<component
|
||||
v-else
|
||||
:is="Component"
|
||||
:key="route.fullPath"
|
||||
class="main-content"
|
||||
/>
|
||||
</transitionMain>
|
||||
</div>
|
||||
</template>
|
||||
</router-view>
|
||||
</section>
|
||||
@@ -50,22 +130,19 @@ const transition = computed(() => {
|
||||
|
||||
<style scoped>
|
||||
.app-main {
|
||||
min-height: calc(100vh - 70px);
|
||||
width: 100%;
|
||||
height: 90vh;
|
||||
height: 100vh;
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.fixed-header + .app-main {
|
||||
padding-top: 50px;
|
||||
.app-main-nofixed-header {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.el-popup-parent--hidden {
|
||||
.fixed-header {
|
||||
padding-right: 15px;
|
||||
}
|
||||
.main-content {
|
||||
margin: 24px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { emitter } from "/@/utils/mitt";
|
||||
import Notice from "./notice/index.vue";
|
||||
import avatars from "/@/assets/avatars.jpg";
|
||||
import { transformI18n } from "/@/plugins/i18n";
|
||||
import Hamburger from "./sidebar/hamBurger.vue";
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
import { storageSession } from "/@/utils/storage";
|
||||
@@ -17,13 +20,17 @@ const pureApp = useAppStoreHook();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
let usename = storageSession.getItem("info")?.username;
|
||||
const { locale, t } = useI18n();
|
||||
const { locale } = useI18n();
|
||||
|
||||
watch(
|
||||
() => locale.value,
|
||||
() => {
|
||||
//@ts-ignore
|
||||
document.title = t(unref(route.meta.title)); // 动态title
|
||||
document.title = transformI18n(
|
||||
//@ts-ignore
|
||||
unref(route.meta.title),
|
||||
unref(route.meta.i18n)
|
||||
); // 动态title
|
||||
}
|
||||
);
|
||||
|
||||
@@ -65,6 +72,8 @@ function translationEn() {
|
||||
<Breadcrumb class="breadcrumb-container" />
|
||||
|
||||
<div class="vertical-header-right">
|
||||
<!-- 通知 -->
|
||||
<Notice />
|
||||
<!-- 全屏 -->
|
||||
<screenfull v-show="!deviceDetection()" />
|
||||
<!-- 国际化 -->
|
||||
@@ -78,6 +87,8 @@ function translationEn() {
|
||||
color: locale === 'zh' ? '#f4f4f5' : '#000'
|
||||
}"
|
||||
@click="translationCh"
|
||||
><el-icon class="check-zh" v-show="locale === 'zh'"
|
||||
><check /></el-icon
|
||||
>简体中文</el-dropdown-item
|
||||
>
|
||||
<el-dropdown-item
|
||||
@@ -86,6 +97,8 @@ function translationEn() {
|
||||
color: locale === 'en' ? '#f4f4f5' : '#000'
|
||||
}"
|
||||
@click="translationEn"
|
||||
><el-icon class="check-en" v-show="locale === 'en'"
|
||||
><check /></el-icon
|
||||
>English</el-dropdown-item
|
||||
>
|
||||
</el-dropdown-menu>
|
||||
@@ -94,24 +107,25 @@ function translationEn() {
|
||||
<!-- 退出登陆 -->
|
||||
<el-dropdown trigger="click">
|
||||
<span class="el-dropdown-link">
|
||||
<img
|
||||
src="https://avatars.githubusercontent.com/u/44761321?s=400&u=30907819abd29bb3779bc247910873e7c7f7c12f&v=4"
|
||||
/>
|
||||
<img :src="avatars" />
|
||||
<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-item @click="logout">
|
||||
<i class="ri-logout-circle-r-line"></i
|
||||
>{{ $t("message.hsLoginOut") }}</el-dropdown-item
|
||||
>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<i
|
||||
<el-icon
|
||||
class="el-icon-setting"
|
||||
:title="$t('message.hssystemSet')"
|
||||
@click="onPanel"
|
||||
></i>
|
||||
>
|
||||
<Setting />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -145,6 +159,12 @@ function translationEn() {
|
||||
color: #000000d9;
|
||||
justify-content: flex-end;
|
||||
|
||||
:deep(.dropdown-badge) {
|
||||
&:hover {
|
||||
background: #f6f6f6;
|
||||
}
|
||||
}
|
||||
|
||||
.screen-full {
|
||||
cursor: pointer;
|
||||
|
||||
@@ -191,8 +211,8 @@ function translationEn() {
|
||||
|
||||
.el-icon-setting {
|
||||
height: 48px;
|
||||
width: 40px;
|
||||
padding: 11px;
|
||||
width: 38px;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
@@ -218,10 +238,27 @@ function translationEn() {
|
||||
color: #606266;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.check-zh {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
top: 13px;
|
||||
}
|
||||
|
||||
.check-en {
|
||||
position: absolute;
|
||||
bottom: 13px;
|
||||
left: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.logout {
|
||||
max-width: 120px;
|
||||
|
||||
.el-dropdown-menu__item {
|
||||
min-width: 100%;
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 0 18px !important;
|
||||
}
|
||||
|
||||
|
||||
146
src/layout/components/notice/data.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
export interface ListItem {
|
||||
avatar: string;
|
||||
title: string;
|
||||
datetime: string;
|
||||
type: string;
|
||||
description: string;
|
||||
status?: "" | "success" | "warning" | "info" | "danger";
|
||||
extra?: string;
|
||||
}
|
||||
|
||||
export interface TabItem {
|
||||
key: string;
|
||||
name: string;
|
||||
list: ListItem[];
|
||||
}
|
||||
|
||||
export const noticesData: TabItem[] = [
|
||||
{
|
||||
key: "1",
|
||||
name: "通知",
|
||||
list: [
|
||||
{
|
||||
avatar:
|
||||
"https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png",
|
||||
title: "你收到了 12 份新周报",
|
||||
datetime: "一年前",
|
||||
description: "",
|
||||
type: "1"
|
||||
},
|
||||
{
|
||||
avatar:
|
||||
"https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png",
|
||||
title: "你推荐的 前端高手 已通过第三轮面试",
|
||||
datetime: "一年前",
|
||||
description: "",
|
||||
type: "1"
|
||||
},
|
||||
{
|
||||
avatar:
|
||||
"https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png",
|
||||
title: "这种模板可以区分多种通知类型",
|
||||
datetime: "一年前",
|
||||
description: "",
|
||||
type: "1"
|
||||
},
|
||||
{
|
||||
avatar:
|
||||
"https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png",
|
||||
title:
|
||||
"展示标题内容超过一行后的处理方式,如果内容超过1行将自动截断并支持tooltip显示完整标题。",
|
||||
datetime: "一年前",
|
||||
description: "",
|
||||
type: "1"
|
||||
},
|
||||
{
|
||||
avatar:
|
||||
"https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png",
|
||||
title: "左侧图标用于区分不同的类型",
|
||||
datetime: "一年前",
|
||||
description: "",
|
||||
type: "1"
|
||||
},
|
||||
{
|
||||
avatar:
|
||||
"https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png",
|
||||
title: "左侧图标用于区分不同的类型",
|
||||
datetime: "一年前",
|
||||
description: "",
|
||||
type: "1"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: "2",
|
||||
name: "消息",
|
||||
list: [
|
||||
{
|
||||
avatar:
|
||||
"https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg",
|
||||
title: "李白 评论了你",
|
||||
description: "长风破浪会有时,直挂云帆济沧海",
|
||||
datetime: "一年前",
|
||||
type: "2"
|
||||
},
|
||||
{
|
||||
avatar:
|
||||
"https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg",
|
||||
title: "李白 回复了你",
|
||||
description: "行路难,行路难,多歧路,今安在。",
|
||||
datetime: "一年前",
|
||||
type: "2"
|
||||
},
|
||||
{
|
||||
avatar:
|
||||
"https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg",
|
||||
title: "标题",
|
||||
description:
|
||||
"请将鼠标移动到此处,以便测试超长的消息在此处将如何处理。本例中设置的描述最大行数为2,超过2行的描述内容将被省略并且可以通过tooltip查看完整内容",
|
||||
datetime: "一年前",
|
||||
type: "2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: "3",
|
||||
name: "代办",
|
||||
list: [
|
||||
{
|
||||
avatar: "",
|
||||
title: "任务名称",
|
||||
description: "任务需要在 2021-11-16 20:00 前启动",
|
||||
datetime: "",
|
||||
extra: "未开始",
|
||||
status: "info",
|
||||
type: "3"
|
||||
},
|
||||
{
|
||||
avatar: "",
|
||||
title: "第三方紧急代码变更",
|
||||
description:
|
||||
"一拳提交于 2021-11-16,需在 2021-11-18 前完成代码变更任务",
|
||||
datetime: "",
|
||||
extra: "马上到期",
|
||||
status: "danger",
|
||||
type: "3"
|
||||
},
|
||||
{
|
||||
avatar: "",
|
||||
title: "信息安全考试",
|
||||
description: "指派小仙于 2021-12-12 前完成更新并发布",
|
||||
datetime: "",
|
||||
extra: "已耗时 8 天",
|
||||
status: "warning",
|
||||
type: "3"
|
||||
},
|
||||
{
|
||||
avatar: "",
|
||||
title: "vue-pure-admin 版本发布",
|
||||
description: "vue-pure-admin 版本发布",
|
||||
datetime: "",
|
||||
extra: "进行中",
|
||||
type: "3"
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
80
src/layout/components/notice/index.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import NoticeList from "./noticeList.vue";
|
||||
import { noticesData } from "./data";
|
||||
|
||||
const activeName = ref(noticesData[0].name);
|
||||
const notices = ref(noticesData);
|
||||
|
||||
let noticesNum = ref(0);
|
||||
notices.value.forEach(notice => {
|
||||
noticesNum.value += notice.list.length;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dropdown trigger="click" placement="bottom-end">
|
||||
<span class="dropdown-badge">
|
||||
<el-badge :value="noticesNum" :max="99">
|
||||
<el-icon class="header-notice-icon"><bell /></el-icon>
|
||||
</el-badge>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-tabs v-model="activeName" class="dropdown-tabs">
|
||||
<template v-for="item in notices" :key="item.key">
|
||||
<el-tab-pane
|
||||
:label="`${item.name}(${item.list.length})`"
|
||||
:name="item.name"
|
||||
>
|
||||
<el-scrollbar max-height="330px">
|
||||
<div class="noticeList-container">
|
||||
<NoticeList :list="item.list" />
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-tab-pane>
|
||||
</template>
|
||||
</el-tabs>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dropdown-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 48px;
|
||||
width: 60px;
|
||||
cursor: pointer;
|
||||
|
||||
.header-notice-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-tabs {
|
||||
width: 336px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 2px 8px rgb(0 0 0 / 15%);
|
||||
border-radius: 4px;
|
||||
|
||||
:deep(.el-tabs__header) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__nav-scroll) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__nav-wrap)::after {
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
:deep(.noticeList-container) {
|
||||
padding: 15px 24px 0 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
168
src/layout/components/notice/noticeItem.vue
Normal file
@@ -0,0 +1,168 @@
|
||||
<script setup lang="ts">
|
||||
import { ListItem } from "./data";
|
||||
import { ref, PropType, nextTick } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
noticeItem: {
|
||||
type: Object as PropType<ListItem>,
|
||||
default: () => {}
|
||||
}
|
||||
});
|
||||
|
||||
const titleRef = ref(null);
|
||||
const descriptionRef = ref(null);
|
||||
const titleTooltip = ref(false);
|
||||
const descriptionTooltip = ref(false);
|
||||
|
||||
function hoverTitle() {
|
||||
nextTick(() => {
|
||||
titleRef.value?.scrollWidth > titleRef.value?.clientWidth
|
||||
? (titleTooltip.value = true)
|
||||
: (titleTooltip.value = false);
|
||||
});
|
||||
}
|
||||
|
||||
function hoverDescription(event, description) {
|
||||
// currentWidth 为文本在页面中所占的宽度,创建标签,加入到页面,获取currentWidth ,最后在移除
|
||||
let tempTag = document.createElement("span");
|
||||
tempTag.innerText = description;
|
||||
tempTag.className = "getDescriptionWidth";
|
||||
document.querySelector("body").appendChild(tempTag);
|
||||
let currentWidth = (
|
||||
document.querySelector(".getDescriptionWidth") as HTMLSpanElement
|
||||
).offsetWidth;
|
||||
document.querySelector(".getDescriptionWidth").remove();
|
||||
|
||||
// cellWidth为容器的宽度
|
||||
const cellWidth = event.target.offsetWidth;
|
||||
|
||||
// 当文本宽度大于容器宽度两倍时,代表文本显示超过两行
|
||||
currentWidth > 2 * cellWidth
|
||||
? (descriptionTooltip.value = true)
|
||||
: (descriptionTooltip.value = false);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="notice-container">
|
||||
<el-avatar
|
||||
v-if="props.noticeItem.avatar"
|
||||
:size="30"
|
||||
:src="props.noticeItem.avatar"
|
||||
class="notice-container-avatar"
|
||||
></el-avatar>
|
||||
<div class="notice-container-text">
|
||||
<div class="notice-text-title">
|
||||
<el-tooltip
|
||||
popper-class="notice-title-popper"
|
||||
:disabled="!titleTooltip"
|
||||
:content="props.noticeItem.title"
|
||||
placement="top-start"
|
||||
>
|
||||
<div
|
||||
ref="titleRef"
|
||||
class="notice-title-content"
|
||||
@mouseover="hoverTitle"
|
||||
>
|
||||
{{ props.noticeItem.title }}
|
||||
</div>
|
||||
</el-tooltip>
|
||||
<el-tag
|
||||
v-if="props.noticeItem?.extra"
|
||||
:type="props.noticeItem?.status"
|
||||
size="small"
|
||||
class="notice-title-extra"
|
||||
>{{ props.noticeItem?.extra }}
|
||||
</el-tag>
|
||||
</div>
|
||||
|
||||
<el-tooltip
|
||||
popper-class="notice-title-popper"
|
||||
:disabled="!descriptionTooltip"
|
||||
:content="props.noticeItem.description"
|
||||
placement="top-start"
|
||||
>
|
||||
<div
|
||||
ref="descriptionRef"
|
||||
class="notice-text-description"
|
||||
@mouseover="hoverDescription($event, props.noticeItem.description)"
|
||||
>
|
||||
{{ props.noticeItem.description }}
|
||||
</div>
|
||||
</el-tooltip>
|
||||
<div class="notice-text-datetime">
|
||||
{{ props.noticeItem.datetime }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.notice-title-popper {
|
||||
max-width: 238px;
|
||||
}
|
||||
</style>
|
||||
<style scoped lang="scss">
|
||||
.notice-container {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
.notice-container-avatar {
|
||||
margin-right: 16px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.notice-container-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
flex: 1;
|
||||
|
||||
.notice-text-title {
|
||||
display: flex;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 1.5715;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
cursor: pointer;
|
||||
|
||||
.notice-title-content {
|
||||
flex: 1;
|
||||
width: 200px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.notice-title-extra {
|
||||
float: right;
|
||||
margin-top: -1.5px;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
.notice-text-description,
|
||||
.notice-text-datetime {
|
||||
font-size: 12px;
|
||||
line-height: 1.5715;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.notice-text-description {
|
||||
display: -webkit-box;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.notice-text-datetime {
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
23
src/layout/components/notice/noticeList.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { PropType } from "vue";
|
||||
import NoticeItem from "./noticeItem.vue";
|
||||
import { ListItem } from "./data";
|
||||
|
||||
const props = defineProps({
|
||||
list: {
|
||||
type: Array as PropType<Array<ListItem>>,
|
||||
default: () => []
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="props.list.length">
|
||||
<NoticeItem
|
||||
v-for="(item, index) in props.list"
|
||||
:noticeItem="item"
|
||||
:key="index"
|
||||
></NoticeItem>
|
||||
</div>
|
||||
<el-empty v-else description="暂无数据"></el-empty>
|
||||
</template>
|
||||
@@ -1,33 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { useEventListener, onClickOutside } from "@vueuse/core";
|
||||
import { onClickOutside } from "@vueuse/core";
|
||||
import { emitter } from "/@/utils/mitt";
|
||||
|
||||
let show = ref<Boolean>(false);
|
||||
const target = ref(null);
|
||||
onClickOutside(target, () => {
|
||||
onClickOutside(target, event => {
|
||||
if (event.clientX > target.value.offsetLeft) return;
|
||||
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>
|
||||
@@ -37,7 +22,9 @@ defineExpose({
|
||||
<div class="right-panel-items">
|
||||
<div class="project-configuration">
|
||||
<h3>项目配置</h3>
|
||||
<i class="el-icon-close" @click="show = !show"></i>
|
||||
<el-icon title="关闭配置" class="el-icon-close" @click="show = !show">
|
||||
<Close />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div style="border-bottom: 1px solid #dcdfe6"></div>
|
||||
<slot />
|
||||
|
||||
@@ -23,7 +23,7 @@ const { isFullscreen, toggle } = useFullscreen();
|
||||
<style lang="scss" scoped>
|
||||
.screen-full {
|
||||
width: 36px;
|
||||
height: 62px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
|
||||
@@ -1,52 +1,87 @@
|
||||
<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,
|
||||
computed,
|
||||
nextTick,
|
||||
useCssModule,
|
||||
getCurrentInstance
|
||||
} from "vue";
|
||||
import panel from "../panel/index.vue";
|
||||
import { getConfig } from "/@/config";
|
||||
import { useRouter } from "vue-router";
|
||||
import { emitter } from "/@/utils/mitt";
|
||||
import { templateRef } from "@vueuse/core";
|
||||
import { debounce } from "/@/utils/debounce";
|
||||
import { themeColorsType } from "../../types";
|
||||
import { useAppStoreHook } from "/@/store/modules/app";
|
||||
import { storageLocal, storageSession } from "/@/utils/storage";
|
||||
import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
|
||||
import { toggleTheme } from "@zougt/vite-plugin-theme-preprocessor/dist/browser-utils";
|
||||
|
||||
const router = useRouter();
|
||||
const { isSelect } = useCssModule();
|
||||
|
||||
const instance =
|
||||
getCurrentInstance().appContext.app.config.globalProperties.$storage;
|
||||
|
||||
const instanceConfig =
|
||||
getCurrentInstance().appContext.app.config.globalProperties.$config;
|
||||
|
||||
let themeColors = ref<Array<themeColorsType>>([
|
||||
// 道奇蓝(默认)
|
||||
{ rgb: "27, 42, 71", themeColor: "default" },
|
||||
// 亮白色
|
||||
{ rgb: "255, 255, 255", themeColor: "light" },
|
||||
// 猩红色
|
||||
{ rgb: "245, 34, 45", themeColor: "dusk" },
|
||||
// 橙红色
|
||||
{ rgb: "250, 84, 28", themeColor: "volcano" },
|
||||
// 金色
|
||||
{ rgb: "250, 219, 20", themeColor: "yellow" },
|
||||
// 绿宝石
|
||||
{ rgb: "19, 194, 194", themeColor: "mingQing" },
|
||||
// 酸橙绿
|
||||
{ rgb: "82, 196, 26", themeColor: "auroraGreen" },
|
||||
// 深粉色
|
||||
{ rgb: "235, 47, 150", themeColor: "pink" },
|
||||
// 深紫罗兰色
|
||||
{ rgb: "114, 46, 209", themeColor: "saucePurple" }
|
||||
]);
|
||||
|
||||
const verticalRef = templateRef<HTMLElement | null>("verticalRef", null);
|
||||
const horizontalRef = templateRef<HTMLElement | null>("horizontalRef", null);
|
||||
|
||||
let layoutTheme =
|
||||
ref(storageLocal.getItem("responsive-layout")) ||
|
||||
ref({
|
||||
layout: instanceConfig?.Layout ?? "vertical",
|
||||
theme: instanceConfig?.Theme ?? "default"
|
||||
});
|
||||
|
||||
// body添加layout属性,作用于src/style/sidebar.scss
|
||||
if (unref(layoutTheme)) {
|
||||
let layout = unref(layoutTheme).layout;
|
||||
let theme = unref(layoutTheme).theme;
|
||||
toggleTheme({
|
||||
scopeName: `layout-theme-${theme}`
|
||||
});
|
||||
setLayoutModel(layout);
|
||||
}
|
||||
|
||||
// 默认灵动模式
|
||||
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")
|
||||
greyVal: instance.sets.grey,
|
||||
weakVal: instance.sets.weak,
|
||||
tabsVal: instance.sets.hideTabs,
|
||||
multiTagsCache: instance.sets.multiTagsCache
|
||||
});
|
||||
|
||||
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;
|
||||
@@ -55,76 +90,89 @@ function toggleClass(flag: boolean, clsName: string, target?: HTMLElement) {
|
||||
}
|
||||
|
||||
// 灰色模式设置
|
||||
const greyChange = ({ value }): void => {
|
||||
const greyChange = (value): void => {
|
||||
toggleClass(settings.greyVal, "html-grey", document.querySelector("html"));
|
||||
value
|
||||
? localOperate("greyVal", true, "set")
|
||||
: localOperate("greyVal", false, "set");
|
||||
instance.sets = {
|
||||
grey: value,
|
||||
weak: instance.sets.weak,
|
||||
hideTabs: instance.sets.hideTabs,
|
||||
multiTagsCache: instance.sets.multiTagsCache
|
||||
};
|
||||
};
|
||||
|
||||
// 色弱模式设置
|
||||
const weekChange = ({ value }): void => {
|
||||
const weekChange = (value): void => {
|
||||
toggleClass(
|
||||
settings.weekVal,
|
||||
settings.weakVal,
|
||||
"html-weakness",
|
||||
document.querySelector("html")
|
||||
);
|
||||
value
|
||||
? localOperate("weekVal", true, "set")
|
||||
: localOperate("weekVal", false, "set");
|
||||
instance.sets = {
|
||||
grey: instance.sets.grey,
|
||||
weak: value,
|
||||
hideTabs: instance.sets.hideTabs,
|
||||
multiTagsCache: instance.sets.multiTagsCache
|
||||
};
|
||||
};
|
||||
|
||||
const tagsChange = () => {
|
||||
let showVal = settings.tagsVal;
|
||||
showVal
|
||||
? storageLocal.setItem("tagsVal", true)
|
||||
: storageLocal.setItem("tagsVal", false);
|
||||
let showVal = settings.tabsVal;
|
||||
instance.sets = {
|
||||
grey: instance.sets.grey,
|
||||
weak: instance.sets.weak,
|
||||
hideTabs: showVal,
|
||||
multiTagsCache: instance.sets.multiTagsCache
|
||||
};
|
||||
emitter.emit("tagViewsChange", showVal);
|
||||
};
|
||||
|
||||
const multiTagsCacheChange = () => {
|
||||
let multiTagsCache = settings.multiTagsCache;
|
||||
instance.sets = {
|
||||
grey: instance.sets.grey,
|
||||
weak: instance.sets.weak,
|
||||
hideTabs: instance.sets.hideTabs,
|
||||
multiTagsCache: multiTagsCache
|
||||
};
|
||||
useMultiTagsStoreHook().multiTagsCacheChange(multiTagsCache);
|
||||
};
|
||||
|
||||
//初始化项目配置
|
||||
nextTick(() => {
|
||||
settings.greyVal &&
|
||||
document.querySelector("html")?.setAttribute("class", "html-grey");
|
||||
settings.weakVal &&
|
||||
document.querySelector("html")?.setAttribute("class", "html-weakness");
|
||||
settings.tabsVal && tagsChange();
|
||||
});
|
||||
|
||||
// 清空缓存并返回登录页
|
||||
function onReset() {
|
||||
storageLocal.clear();
|
||||
storageSession.clear();
|
||||
toggleClass(false, "html-grey", document.querySelector("html"));
|
||||
toggleClass(false, "html-weakness", document.querySelector("html"));
|
||||
useMultiTagsStoreHook().handleTags("equal", [
|
||||
{
|
||||
path: "/welcome",
|
||||
parentPath: "/",
|
||||
meta: {
|
||||
title: "message.hshome",
|
||||
icon: "el-icon-s-home",
|
||||
i18n: true,
|
||||
showLink: true
|
||||
}
|
||||
}
|
||||
]);
|
||||
useMultiTagsStoreHook().multiTagsCacheChange(getConfig().MultiTagsCache);
|
||||
router.push("/login");
|
||||
}
|
||||
|
||||
function onChange({ label }) {
|
||||
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"
|
||||
@@ -141,175 +189,182 @@ function setFalse(Doms): any {
|
||||
|
||||
watch(instance, ({ layout }) => {
|
||||
switch (layout["layout"]) {
|
||||
case "vertical-dark":
|
||||
toggleClass(true, isSelect, unref(verticalDarkDom));
|
||||
debounce(
|
||||
setFalse([verticalLightDom, horizontalDarkDom, horizontalLightDom]),
|
||||
50
|
||||
);
|
||||
case "vertical":
|
||||
toggleClass(true, isSelect, unref(verticalRef));
|
||||
debounce(setFalse([horizontalRef]), 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
|
||||
);
|
||||
case "horizontal":
|
||||
toggleClass(true, isSelect, unref(horizontalRef));
|
||||
debounce(setFalse([verticalRef]), 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}` };
|
||||
// 主题色 激活选择项
|
||||
const getThemeColor = computed(() => {
|
||||
return current => {
|
||||
if (
|
||||
current === layoutTheme.value.theme &&
|
||||
layoutTheme.value.theme !== "light"
|
||||
) {
|
||||
return "#fff";
|
||||
} else if (
|
||||
current === layoutTheme.value.theme &&
|
||||
layoutTheme.value.theme === "light"
|
||||
) {
|
||||
return "#1d2b45";
|
||||
} else {
|
||||
return "transparent";
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// 设置导航模式
|
||||
function setLayoutModel(layout: string) {
|
||||
layoutTheme.value.layout = layout;
|
||||
window.document.body.setAttribute("layout", layout);
|
||||
instance.layout = { layout, theme: layoutTheme.value.theme };
|
||||
useAppStoreHook().setLayout(layout);
|
||||
}
|
||||
|
||||
// 设置导航主题色
|
||||
function setLayoutThemeColor(theme: string) {
|
||||
layoutTheme.value.theme = theme;
|
||||
toggleTheme({
|
||||
scopeName: `layout-theme-${theme}`
|
||||
});
|
||||
instance.layout = { layout: useAppStoreHook().layout, theme };
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<panel>
|
||||
<el-divider>主题风格</el-divider>
|
||||
<ul class="theme-stley">
|
||||
<el-tooltip
|
||||
class="item"
|
||||
effect="dark"
|
||||
content="左侧菜单暗色模式"
|
||||
placement="bottom"
|
||||
>
|
||||
<ul class="pure-theme">
|
||||
<el-tooltip class="item" content="左侧菜单模式" placement="bottom">
|
||||
<li
|
||||
:class="dataTheme.layout === 'vertical-dark' ? $style.isSelect : ''"
|
||||
ref="verticalDarkDom"
|
||||
@click="setTheme('vertical', 'dark')"
|
||||
:class="layoutTheme.layout === 'vertical' ? $style.isSelect : ''"
|
||||
ref="verticalRef"
|
||||
@click="setLayoutModel('vertical')"
|
||||
>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</li>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip
|
||||
class="item"
|
||||
effect="dark"
|
||||
content="左侧菜单亮色模式"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-tooltip class="item" content="顶部菜单模式" placement="bottom">
|
||||
<li
|
||||
:class="dataTheme.layout === 'vertical-light' ? $style.isSelect : ''"
|
||||
ref="verticalLightDom"
|
||||
@click="setTheme('vertical', 'light')"
|
||||
:class="layoutTheme.layout === 'horizontal' ? $style.isSelect : ''"
|
||||
ref="horizontalRef"
|
||||
@click="setLayoutModel('horizontal')"
|
||||
>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</li>
|
||||
</el-tooltip>
|
||||
</ul>
|
||||
|
||||
<el-tooltip
|
||||
class="item"
|
||||
effect="dark"
|
||||
content="顶部菜单暗色模式"
|
||||
placement="bottom"
|
||||
<el-divider>主题色</el-divider>
|
||||
<ul class="theme-color">
|
||||
<li
|
||||
v-for="(item, index) in themeColors"
|
||||
:key="index"
|
||||
:style="{ background: `rgb(${item.rgb})` }"
|
||||
@click="setLayoutThemeColor(item.themeColor)"
|
||||
>
|
||||
<li
|
||||
:class="dataTheme.layout === 'horizontal-dark' ? $style.isSelect : ''"
|
||||
ref="horizontalDarkDom"
|
||||
@click="setTheme('horizontal', 'dark')"
|
||||
<el-icon
|
||||
style="margin: 0.1em 0.1em 0 0"
|
||||
:size="17"
|
||||
:color="getThemeColor(item.themeColor)"
|
||||
>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</li>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip
|
||||
class="item"
|
||||
effect="dark"
|
||||
content="顶部菜单亮色模式"
|
||||
placement="bottom"
|
||||
>
|
||||
<li
|
||||
:class="
|
||||
dataTheme.layout === 'horizontal-light' ? $style.isSelect : ''
|
||||
"
|
||||
ref="horizontalLightDom"
|
||||
@click="setTheme('horizontal', 'light')"
|
||||
>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</li>
|
||||
</el-tooltip>
|
||||
<Check />
|
||||
</el-icon>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<el-divider>界面显示</el-divider>
|
||||
<ul class="setting">
|
||||
<li>
|
||||
<span>灰色模式</span>
|
||||
<vxe-switch
|
||||
<el-switch
|
||||
v-model="settings.greyVal"
|
||||
open-label="开"
|
||||
close-label="关"
|
||||
inline-prompt
|
||||
inactive-color="#a6a6a6"
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
@change="greyChange"
|
||||
></vxe-switch>
|
||||
>
|
||||
</el-switch>
|
||||
</li>
|
||||
<li>
|
||||
<span>色弱模式</span>
|
||||
<vxe-switch
|
||||
v-model="settings.weekVal"
|
||||
open-label="开"
|
||||
close-label="关"
|
||||
<el-switch
|
||||
v-model="settings.weakVal"
|
||||
inline-prompt
|
||||
inactive-color="#a6a6a6"
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
@change="weekChange"
|
||||
></vxe-switch>
|
||||
>
|
||||
</el-switch>
|
||||
</li>
|
||||
<li>
|
||||
<span>隐藏标签页</span>
|
||||
<vxe-switch
|
||||
v-model="settings.tagsVal"
|
||||
open-label="开"
|
||||
close-label="关"
|
||||
<el-switch
|
||||
v-model="settings.tabsVal"
|
||||
inline-prompt
|
||||
inactive-color="#a6a6a6"
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
@change="tagsChange"
|
||||
></vxe-switch>
|
||||
>
|
||||
</el-switch>
|
||||
</li>
|
||||
<li>
|
||||
<span>侧边栏Logo</span>
|
||||
<vxe-switch
|
||||
<el-switch
|
||||
v-model="logoVal"
|
||||
open-value="1"
|
||||
close-value="-1"
|
||||
open-label="开"
|
||||
close-label="关"
|
||||
inline-prompt
|
||||
active-value="1"
|
||||
inactive-value="-1"
|
||||
inactive-color="#a6a6a6"
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
@change="logoChange"
|
||||
></vxe-switch>
|
||||
>
|
||||
</el-switch>
|
||||
</li>
|
||||
<li>
|
||||
<span>标签页持久化</span>
|
||||
<el-switch
|
||||
v-model="settings.multiTagsCache"
|
||||
inline-prompt
|
||||
inactive-color="#a6a6a6"
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
@change="multiTagsCacheChange"
|
||||
>
|
||||
</el-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>
|
||||
<el-radio-group v-model="markValue" size="small" @change="onChange">
|
||||
<el-radio label="card">卡片</el-radio>
|
||||
<el-radio label="smart">灵动</el-radio>
|
||||
</el-radio-group>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<el-divider />
|
||||
<vxe-button
|
||||
status="danger"
|
||||
<el-button
|
||||
type="danger"
|
||||
style="width: 90%; margin: 24px 15px"
|
||||
content="清空缓存并返回登录页"
|
||||
icon="fa fa-sign-out"
|
||||
@click="onReset"
|
||||
></vxe-button>
|
||||
>
|
||||
<i class="fa fa-sign-out"></i>
|
||||
清空缓存并返回登录页</el-button
|
||||
>
|
||||
</panel>
|
||||
</template>
|
||||
|
||||
@@ -336,10 +391,10 @@ function setTheme(layout: string, theme: string) {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.theme-stley {
|
||||
.pure-theme {
|
||||
margin-top: 25px;
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
height: 100px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-around;
|
||||
@@ -376,28 +431,6 @@ function setTheme(layout: string, theme: string) {
|
||||
}
|
||||
|
||||
&: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%;
|
||||
@@ -407,16 +440,29 @@ function setTheme(layout: string, theme: string) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(4) {
|
||||
div {
|
||||
&:nth-child(1) {
|
||||
width: 100%;
|
||||
height: 30%;
|
||||
background: #fff;
|
||||
box-shadow: 0 0 1px #888;
|
||||
}
|
||||
}
|
||||
.theme-color {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
li {
|
||||
float: left;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-top: 8px;
|
||||
margin-right: 8px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
|
||||
&:nth-child(2) {
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from "vue";
|
||||
import { isEqual } from "lodash-es";
|
||||
import { transformI18n } from "/@/plugins/i18n";
|
||||
import { getParentPaths, findRouteByPath } from "/@/router/utils";
|
||||
import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
|
||||
import { useRoute, useRouter, RouteLocationMatched } from "vue-router";
|
||||
|
||||
const levelList = ref([]);
|
||||
const route = useRoute();
|
||||
const levelList = ref([]);
|
||||
const router = useRouter();
|
||||
const routes = router.options.routes;
|
||||
const multiTags = useMultiTagsStoreHook().multiTags;
|
||||
|
||||
const isDashboard = (route: RouteLocationMatched): boolean | string => {
|
||||
const name = route && (route.name as string);
|
||||
@@ -15,19 +21,57 @@ const isDashboard = (route: RouteLocationMatched): boolean | string => {
|
||||
};
|
||||
|
||||
const getBreadcrumb = (): void => {
|
||||
let matched = route.matched.filter(item => item.meta && item.meta.title);
|
||||
// 当前路由信息
|
||||
let currentRoute;
|
||||
if (Object.keys(route.query).length > 0) {
|
||||
multiTags.forEach(item => {
|
||||
if (isEqual(route.query, item?.query)) {
|
||||
currentRoute = item;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
currentRoute = findRouteByPath(router.currentRoute.value.path, multiTags);
|
||||
}
|
||||
// 当前路由的父级路径组成的数组
|
||||
const parentRoutes = getParentPaths(router.currentRoute.value.path, routes);
|
||||
// 存放组成面包屑的数组
|
||||
let matched = [];
|
||||
// 获取每个父级路径对应的路由信息
|
||||
parentRoutes.forEach(path => {
|
||||
if (path !== "/") {
|
||||
matched.push(findRouteByPath(path, routes));
|
||||
}
|
||||
});
|
||||
if (router.currentRoute.value.meta?.refreshRedirect) {
|
||||
matched.unshift(
|
||||
findRouteByPath(
|
||||
router.currentRoute.value.meta.refreshRedirect as string,
|
||||
routes
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// 过滤与子级相同标题的父级路由
|
||||
matched = matched.filter(item => {
|
||||
return !item.redirect || (item.redirect && item.children.length !== 1);
|
||||
});
|
||||
}
|
||||
if (currentRoute?.path !== "/welcome") {
|
||||
matched.push(currentRoute);
|
||||
}
|
||||
|
||||
const first = matched[0];
|
||||
if (!isDashboard(first)) {
|
||||
matched = [
|
||||
{
|
||||
path: "/welcome",
|
||||
parentPath: "/",
|
||||
meta: { title: "message.hshome" }
|
||||
meta: { title: "message.hshome", i18n: true }
|
||||
} as unknown as RouteLocationMatched
|
||||
].concat(matched);
|
||||
}
|
||||
|
||||
levelList.value = matched.filter(
|
||||
item => item.meta && item.meta.title && item.meta.breadcrumb !== false
|
||||
item => item?.meta && item?.meta.title !== false
|
||||
);
|
||||
};
|
||||
|
||||
@@ -38,6 +82,11 @@ watch(
|
||||
() => getBreadcrumb()
|
||||
);
|
||||
|
||||
watch(
|
||||
() => route.query,
|
||||
() => getBreadcrumb()
|
||||
);
|
||||
|
||||
const handleLink = (item: RouteLocationMatched): any => {
|
||||
const { redirect, path } = item;
|
||||
if (redirect) {
|
||||
@@ -55,10 +104,10 @@ const handleLink = (item: RouteLocationMatched): any => {
|
||||
<span
|
||||
v-if="item.redirect === 'noRedirect' || index == levelList.length - 1"
|
||||
class="no-redirect"
|
||||
>{{ $t(item.meta.title) }}</span
|
||||
>{{ transformI18n(item.meta.title, item.meta.i18n) }}</span
|
||||
>
|
||||
<a v-else @click.prevent="handleLink(item)">
|
||||
{{ $t(item.meta.title) }}
|
||||
{{ transformI18n(item.meta.title, item.meta.i18n) }}
|
||||
</a>
|
||||
</el-breadcrumb-item>
|
||||
</transition-group>
|
||||
|
||||
@@ -17,7 +17,11 @@ const toggleClick = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="classes.container" @click="toggleClick">
|
||||
<div
|
||||
:class="classes.container"
|
||||
:title="props.isActive ? '点击折叠' : '点击展开'"
|
||||
@click="toggleClick"
|
||||
>
|
||||
<svg
|
||||
:class="['hamburger', props.isActive ? 'is-active' : '']"
|
||||
viewBox="0 0 1024 1024"
|
||||
|
||||
@@ -8,22 +8,27 @@ import {
|
||||
getCurrentInstance
|
||||
} from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import settings from "/@/settings";
|
||||
import { emitter } from "/@/utils/mitt";
|
||||
import Notice from "../notice/index.vue";
|
||||
import { templateRef } from "@vueuse/core";
|
||||
import SidebarItem from "./sidebarItem.vue";
|
||||
import avatars from "/@/assets/avatars.jpg";
|
||||
import { algorithm } from "/@/utils/algorithm";
|
||||
import screenfull from "../screenfull/index.vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { storageSession } from "/@/utils/storage";
|
||||
import Icon from "/@/components/ReIcon/src/Icon.vue";
|
||||
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;
|
||||
@@ -49,9 +54,10 @@ function onPanel() {
|
||||
emitter.emit("openPanel");
|
||||
}
|
||||
|
||||
const activeMenu = computed(() => {
|
||||
const activeMenu = computed((): string => {
|
||||
const { meta, path } = route;
|
||||
if (meta.activeMenu) {
|
||||
// @ts-ignore
|
||||
return meta.activeMenu;
|
||||
}
|
||||
return path;
|
||||
@@ -113,8 +119,8 @@ onMounted(() => {
|
||||
<template>
|
||||
<div class="horizontal-header">
|
||||
<div class="horizontal-header-left" @click="backHome">
|
||||
<i class="fa fa-optin-monster"></i>
|
||||
<h4>{{ settings.title }}</h4>
|
||||
<Icon svg :width="35" :height="35" content="team-iconlogo" />
|
||||
<h4>{{ title }}</h4>
|
||||
</div>
|
||||
<el-menu
|
||||
ref="menu"
|
||||
@@ -126,13 +132,15 @@ onMounted(() => {
|
||||
@select="menuSelect"
|
||||
>
|
||||
<sidebar-item
|
||||
v-for="route in routeStore.wholeRoutes"
|
||||
v-for="route in usePermissionStoreHook().wholeMenus"
|
||||
:key="route.path"
|
||||
:item="route"
|
||||
:base-path="route.path"
|
||||
/>
|
||||
</el-menu>
|
||||
<div class="horizontal-header-right">
|
||||
<!-- 通知 -->
|
||||
<Notice />
|
||||
<!-- 全屏 -->
|
||||
<screenfull v-show="!deviceDetection()" />
|
||||
<!-- 国际化 -->
|
||||
@@ -146,6 +154,8 @@ onMounted(() => {
|
||||
color: locale === 'zh' ? '#f4f4f5' : '#000'
|
||||
}"
|
||||
@click="translationCh"
|
||||
><el-icon class="check-zh" v-show="locale === 'zh'"
|
||||
><check /></el-icon
|
||||
>简体中文</el-dropdown-item
|
||||
>
|
||||
<el-dropdown-item
|
||||
@@ -154,6 +164,8 @@ onMounted(() => {
|
||||
color: locale === 'en' ? '#f4f4f5' : '#000'
|
||||
}"
|
||||
@click="translationEn"
|
||||
><el-icon class="check-en" v-show="locale === 'en'"
|
||||
><check /></el-icon
|
||||
>English</el-dropdown-item
|
||||
>
|
||||
</el-dropdown-menu>
|
||||
@@ -162,24 +174,25 @@ onMounted(() => {
|
||||
<!-- 退出登陆 -->
|
||||
<el-dropdown trigger="click">
|
||||
<span class="el-dropdown-link">
|
||||
<img
|
||||
src="https://avatars.githubusercontent.com/u/44761321?s=400&u=30907819abd29bb3779bc247910873e7c7f7c12f&v=4"
|
||||
/>
|
||||
<img :src="avatars" />
|
||||
<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-item @click="logout">
|
||||
<i class="ri-logout-circle-r-line"></i
|
||||
>{{ $t("message.hsLoginOut") }}</el-dropdown-item
|
||||
>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<i
|
||||
<el-icon
|
||||
class="el-icon-setting"
|
||||
:title="$t('message.hssystemSet')"
|
||||
@click="onPanel"
|
||||
></i>
|
||||
>
|
||||
<Setting />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -195,10 +208,27 @@ onMounted(() => {
|
||||
color: #606266;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.check-zh {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
top: 13px;
|
||||
}
|
||||
|
||||
.check-en {
|
||||
position: absolute;
|
||||
bottom: 13px;
|
||||
left: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.logout {
|
||||
max-width: 120px;
|
||||
|
||||
.el-dropdown-menu__item {
|
||||
min-width: 100%;
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 0 18px !important;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import settings from "/@/settings";
|
||||
|
||||
import { getCurrentInstance } from "vue";
|
||||
import Icon from "/@/components/ReIcon/src/Icon.vue";
|
||||
const props = defineProps({
|
||||
collapse: Boolean
|
||||
});
|
||||
|
||||
const title =
|
||||
getCurrentInstance().appContext.config.globalProperties.$config?.Title;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -12,22 +15,22 @@ const props = defineProps({
|
||||
<router-link
|
||||
v-if="props.collapse"
|
||||
key="props.collapse"
|
||||
:title="settings.title"
|
||||
:title="title"
|
||||
class="sidebar-logo-link"
|
||||
to="/"
|
||||
>
|
||||
<i class="fa fa-optin-monster"></i>
|
||||
<h1 class="sidebar-title">{{ settings.title }}</h1>
|
||||
<Icon svg :width="35" :height="35" content="team-iconlogo" />
|
||||
<span class="sidebar-title">{{ title }}</span>
|
||||
</router-link>
|
||||
<router-link
|
||||
v-else
|
||||
key="expand"
|
||||
:title="settings.title"
|
||||
:title="title"
|
||||
class="sidebar-logo-link"
|
||||
to="/"
|
||||
>
|
||||
<i class="fa fa-optin-monster"></i>
|
||||
<h1 class="sidebar-title">{{ settings.title }}</h1>
|
||||
<Icon svg :width="35" :height="35" content="team-iconlogo" />
|
||||
<span class="sidebar-title">{{ title }}</span>
|
||||
</router-link>
|
||||
</transition>
|
||||
</div>
|
||||
@@ -37,28 +40,24 @@ const props = defineProps({
|
||||
.sidebar-logo-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
height: 48px;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
|
||||
.sidebar-logo-link {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
margin-top: 5px;
|
||||
|
||||
.sidebar-title {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
color: #1890ff;
|
||||
font-weight: 600;
|
||||
font-size: 20px;
|
||||
margin-top: 16px;
|
||||
margin-top: 10px;
|
||||
font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
.fa-optin-monster {
|
||||
font-size: 30px;
|
||||
color: #1890ff;
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.collapse {
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import path from "path";
|
||||
import { PropType, ref } from "vue";
|
||||
import { PropType, ref, nextTick, getCurrentInstance } from "vue";
|
||||
import { childrenType } from "../../types";
|
||||
import { useAppStoreHook } from "/@/store/modules/app";
|
||||
import Icon from "/@/components/ReIcon/src/Icon.vue";
|
||||
import { RouteRecordRaw } from "vue-router";
|
||||
import { transformI18n } from "/@/plugins/i18n";
|
||||
import { findIconReg } from "/@/components/ReIcon";
|
||||
const instance = getCurrentInstance().appContext.app.config.globalProperties;
|
||||
const menuMode = instance.$storage.layout?.layout === "vertical";
|
||||
const pureApp = useAppStoreHook();
|
||||
|
||||
const props = defineProps({
|
||||
item: {
|
||||
type: Object as PropType<RouteRecordRaw>
|
||||
type: Object as PropType<childrenType>
|
||||
},
|
||||
isNest: {
|
||||
type: Boolean,
|
||||
@@ -18,21 +24,33 @@ const props = defineProps({
|
||||
}
|
||||
});
|
||||
|
||||
type childrenType = {
|
||||
path?: string;
|
||||
noShowingChildren?: boolean;
|
||||
children?: RouteRecordRaw[];
|
||||
meta?: {
|
||||
icon?: string;
|
||||
title?: string;
|
||||
};
|
||||
};
|
||||
const onlyOneChild: childrenType = ref(null);
|
||||
// 存放菜单是否存在showTooltip属性标识
|
||||
const hoverMenuMap = new WeakMap();
|
||||
// 存储菜单文本dom元素
|
||||
const menuTextRef = ref(null);
|
||||
|
||||
const onlyOneChild = ref<RouteRecordRaw | childrenType>({} as any);
|
||||
function hoverMenu(key) {
|
||||
// 如果当前菜单showTooltip属性已存在,退出计算
|
||||
if (hoverMenuMap.get(key)) return;
|
||||
|
||||
nextTick(() => {
|
||||
// 如果文本内容的整体宽度大于其可视宽度,则文本溢出
|
||||
menuTextRef.value?.scrollWidth > menuTextRef.value?.clientWidth
|
||||
? Object.assign(key, {
|
||||
showTooltip: true
|
||||
})
|
||||
: Object.assign(key, {
|
||||
showTooltip: false
|
||||
});
|
||||
|
||||
hoverMenuMap.set(key, true);
|
||||
});
|
||||
}
|
||||
|
||||
function hasOneShowingChild(
|
||||
children: RouteRecordRaw[] = [],
|
||||
parent: RouteRecordRaw
|
||||
children: childrenType[] = [],
|
||||
parent: childrenType
|
||||
) {
|
||||
const showingChildren = children.filter((item: any) => {
|
||||
onlyOneChild.value = item;
|
||||
@@ -51,7 +69,12 @@ function hasOneShowingChild(
|
||||
}
|
||||
|
||||
function resolvePath(routePath) {
|
||||
return path.resolve(props.basePath, routePath);
|
||||
const httpReg = /^http(s?):\/\//;
|
||||
if (httpReg.test(routePath)) {
|
||||
return props.basePath + "/" + routePath;
|
||||
} else {
|
||||
return path.resolve(props.basePath, routePath);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -59,26 +82,69 @@ function resolvePath(routePath) {
|
||||
<template
|
||||
v-if="
|
||||
hasOneShowingChild(props.item.children, props.item) &&
|
||||
(!onlyOneChild.children || onlyOneChild.noShowingChildren) &&
|
||||
!props.item.alwaysShow
|
||||
(!onlyOneChild.children || onlyOneChild.noShowingChildren)
|
||||
"
|
||||
>
|
||||
<el-menu-item
|
||||
:index="resolvePath(onlyOneChild.path)"
|
||||
:class="{ 'submenu-title-noDropdown': !isNest }"
|
||||
style="display: flex; align-items: center"
|
||||
>
|
||||
<i
|
||||
:class="
|
||||
onlyOneChild.meta.icon || (props.item.meta && props.item.meta.icon)
|
||||
"
|
||||
/>
|
||||
<el-icon v-show="props.item.meta.icon">
|
||||
<component
|
||||
:is="
|
||||
findIconReg(
|
||||
onlyOneChild.meta.icon ||
|
||||
(props.item.meta && props.item.meta.icon)
|
||||
)
|
||||
"
|
||||
></component>
|
||||
</el-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}`"
|
||||
/>
|
||||
<div
|
||||
:style="{
|
||||
width: pureApp.sidebar.opened ? '' : '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
overflow: 'hidden'
|
||||
}"
|
||||
>
|
||||
<span v-if="!menuMode">{{
|
||||
transformI18n(onlyOneChild.meta.title, onlyOneChild.meta.i18n)
|
||||
}}</span>
|
||||
<el-tooltip
|
||||
v-else
|
||||
placement="top"
|
||||
:offset="-10"
|
||||
:disabled="!onlyOneChild.showTooltip"
|
||||
>
|
||||
<template #content>
|
||||
{{
|
||||
transformI18n(onlyOneChild.meta.title, onlyOneChild.meta.i18n)
|
||||
}}
|
||||
</template>
|
||||
<span
|
||||
ref="menuTextRef"
|
||||
:style="{
|
||||
width: pureApp.sidebar.opened ? '125px' : '',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
outline: 'none'
|
||||
}"
|
||||
@mouseover="hoverMenu(onlyOneChild)"
|
||||
>
|
||||
{{
|
||||
transformI18n(onlyOneChild.meta.title, onlyOneChild.meta.i18n)
|
||||
}}
|
||||
</span>
|
||||
</el-tooltip>
|
||||
<Icon
|
||||
v-if="onlyOneChild.meta.extraIcon"
|
||||
:svg="onlyOneChild.meta.extraIcon.svg ? true : false"
|
||||
:content="`${onlyOneChild.meta.extraIcon.name}`"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
@@ -90,8 +156,38 @@ function resolvePath(routePath) {
|
||||
popper-append-to-body
|
||||
>
|
||||
<template #title>
|
||||
<i :class="props.item.meta.icon"></i>
|
||||
<span>{{ $t(props.item.meta.title) }}</span>
|
||||
<el-icon v-show="props.item.meta.icon" :class="props.item.meta.icon">
|
||||
<component
|
||||
:is="findIconReg(props.item.meta && props.item.meta.icon)"
|
||||
></component>
|
||||
</el-icon>
|
||||
<span v-if="!menuMode">{{
|
||||
transformI18n(props.item.meta.title, props.item.meta.i18n)
|
||||
}}</span>
|
||||
<el-tooltip
|
||||
v-else
|
||||
placement="top"
|
||||
:offset="-10"
|
||||
:disabled="!pureApp.sidebar.opened || !props.item.showTooltip"
|
||||
>
|
||||
<template #content>
|
||||
{{ transformI18n(props.item.meta.title, props.item.meta.i18n) }}
|
||||
</template>
|
||||
<div
|
||||
ref="menuTextRef"
|
||||
:style="{
|
||||
width: pureApp.sidebar.opened ? '125px' : '',
|
||||
display: 'inline-block',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis'
|
||||
}"
|
||||
@mouseover="hoverMenu(props.item)"
|
||||
>
|
||||
<span style="overflow: hidden; text-overflow: ellipsis">
|
||||
{{ transformI18n(props.item.meta.title, props.item.meta.i18n) }}
|
||||
</span>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
<Icon
|
||||
v-if="props.item.meta.extraIcon"
|
||||
:svg="props.item.meta.extraIcon.svg ? true : false"
|
||||
|
||||
@@ -12,14 +12,14 @@ 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(() => {
|
||||
const activeMenu = computed((): string => {
|
||||
const { meta, path } = route;
|
||||
if (meta.activeMenu) {
|
||||
// @ts-ignore
|
||||
return meta.activeMenu;
|
||||
}
|
||||
return path;
|
||||
@@ -67,12 +67,14 @@ onBeforeMount(() => {
|
||||
router
|
||||
:collapse-transition="false"
|
||||
mode="vertical"
|
||||
class="outer-most"
|
||||
@select="menuSelect"
|
||||
>
|
||||
<sidebar-item
|
||||
v-for="route in routeStore.wholeRoutes"
|
||||
v-for="route in usePermissionStoreHook().wholeMenus"
|
||||
:key="route.path"
|
||||
:item="route"
|
||||
class="outer-most"
|
||||
:base-path="route.path"
|
||||
/>
|
||||
</el-menu>
|
||||
|
||||
343
src/layout/components/tag/index.scss
Normal file
@@ -0,0 +1,343 @@
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes close {
|
||||
from {
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translate(0, -50%);
|
||||
}
|
||||
}
|
||||
|
||||
.tags-view {
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--el-text-color-regular);
|
||||
background: #fff;
|
||||
position: relative;
|
||||
box-shadow: 0 0 1px #888;
|
||||
|
||||
.scroll-item {
|
||||
border-radius: 3px 3px 0 0;
|
||||
padding: 0 6px 0 6px;
|
||||
box-shadow: 0 0 1px #888;
|
||||
position: relative;
|
||||
margin-right: 4px;
|
||||
height: 28px;
|
||||
display: inline-block;
|
||||
line-height: 28px;
|
||||
transition: all 0.4s;
|
||||
cursor: pointer;
|
||||
|
||||
.el-icon-close {
|
||||
font-size: 10px;
|
||||
color: #1890ff;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
transition: font-size 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-radius: 50%;
|
||||
color: #fff;
|
||||
background: #b4bccc;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-closable:not(:first-child) {
|
||||
&:hover {
|
||||
padding-right: 18px;
|
||||
|
||||
&:not(.is-active) {
|
||||
.el-icon-close {
|
||||
animation: close 200ms ease-in forwards;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #666;
|
||||
padding: 0 4px 0 4px;
|
||||
}
|
||||
|
||||
.scroll-container {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
padding: 5px 0;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
|
||||
.tab {
|
||||
position: relative;
|
||||
float: left;
|
||||
list-style: none;
|
||||
overflow: visible;
|
||||
white-space: nowrap;
|
||||
transition: transform 0.5s ease-in-out;
|
||||
|
||||
.scroll-item {
|
||||
transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
|
||||
&:nth-child(1) {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right-button {
|
||||
display: flex;
|
||||
font-size: 16px;
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 38px;
|
||||
border-right: 1px solid #ccc;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
/* 右键菜单 */
|
||||
.contextmenu {
|
||||
margin: 0;
|
||||
background: #fff;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.scroll-item.is-active {
|
||||
background-color: #eaf4fe;
|
||||
position: relative;
|
||||
color: #fff;
|
||||
|
||||
&:not(:first-child) {
|
||||
padding-right: 18px;
|
||||
}
|
||||
|
||||
.el-icon-close {
|
||||
transform: translate(0, -50%);
|
||||
}
|
||||
|
||||
a {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
.ri-arrow-left-s-line {
|
||||
width: 40px;
|
||||
height: 38px;
|
||||
line-height: 38px;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
color: #00000073;
|
||||
box-shadow: 5px 0 5px -6px #ccc;
|
||||
|
||||
&:hover {
|
||||
cursor: w-resize;
|
||||
}
|
||||
}
|
||||
|
||||
.ri-arrow-right-s-line {
|
||||
width: 40px;
|
||||
height: 38px;
|
||||
line-height: 38px;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
border-right: 1px solid #ccc;
|
||||
color: #00000073;
|
||||
box-shadow: -5px 0 5px -6px #ccc;
|
||||
|
||||
&:hover {
|
||||
cursor: e-resize;
|
||||
}
|
||||
}
|
||||
|
||||
/* 卡片模式下鼠标移入显示蓝色边框 */
|
||||
.card-in {
|
||||
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;
|
||||
}
|
||||
@@ -9,14 +9,6 @@ import {
|
||||
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";
|
||||
@@ -25,27 +17,173 @@ 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();
|
||||
import { emitter } from "/@/utils/mitt";
|
||||
import { isEqual, isEmpty } from "lodash-es";
|
||||
import { transformI18n } from "/@/plugins/i18n";
|
||||
import { storageLocal } from "/@/utils/storage";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { RouteConfigs, tagsViewsType } from "../../types";
|
||||
import { useSettingStoreHook } from "/@/store/modules/settings";
|
||||
import { handleAliveRoute, delAliveRoutes } from "/@/router/utils";
|
||||
import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
|
||||
import { usePermissionStoreHook } from "/@/store/modules/permission";
|
||||
import { toggleClass, removeClass, hasClass } from "/@/utils/operate";
|
||||
|
||||
import { templateRef, useResizeObserver, useDebounceFn } from "@vueuse/core";
|
||||
|
||||
// 响应式storage
|
||||
let relativeStorage: relativeStorageType;
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const translateX = ref<number>(0);
|
||||
const activeIndex = ref<number>(-1);
|
||||
let refreshButton = "refresh-button";
|
||||
const instance = getCurrentInstance();
|
||||
const pureSetting = useSettingStoreHook();
|
||||
const showTags = ref(storageLocal.getItem("tagsVal") || false);
|
||||
const tabDom = templateRef<HTMLElement | null>("tabDom", null);
|
||||
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 scrollbarDom = templateRef<HTMLElement | null>("scrollbarDom", null);
|
||||
|
||||
let multiTags: ComputedRef<Array<RouteConfigs>> = computed(() => {
|
||||
return useMultiTagsStoreHook()?.multiTags;
|
||||
});
|
||||
|
||||
const linkIsActive = computed(() => {
|
||||
return item => {
|
||||
if (Object.keys(route.query).length === 0) {
|
||||
if (route.path === item.path) {
|
||||
return "is-active";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
} else {
|
||||
if (isEqual(route?.query, item?.query)) {
|
||||
return "is-active";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const scheduleIsActive = computed(() => {
|
||||
return item => {
|
||||
if (Object.keys(route.query).length === 0) {
|
||||
if (route.path === item.path) {
|
||||
return "schedule-active";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
} else {
|
||||
if (isEqual(route?.query, item?.query)) {
|
||||
return "schedule-active";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const iconIsActive = computed(() => {
|
||||
return (item, index) => {
|
||||
if (index === 0) return;
|
||||
if (Object.keys(route.query).length === 0) {
|
||||
if (route.path === item.path) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (isEqual(route?.query, item?.query)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const dynamicTagView = () => {
|
||||
const index = multiTags.value.findIndex(item => {
|
||||
return item.path === route.path;
|
||||
});
|
||||
moveToView(index);
|
||||
};
|
||||
|
||||
watch([route], () => {
|
||||
activeIndex.value = -1;
|
||||
dynamicTagView();
|
||||
});
|
||||
|
||||
useResizeObserver(
|
||||
scrollbarDom,
|
||||
useDebounceFn(() => {
|
||||
dynamicTagView();
|
||||
}, 200)
|
||||
);
|
||||
|
||||
const tabNavPadding = 10;
|
||||
const moveToView = (index: number): void => {
|
||||
if (!instance.refs["dynamic" + index]) {
|
||||
return;
|
||||
}
|
||||
const tabItemEl = instance.refs["dynamic" + index];
|
||||
const tabItemElOffsetLeft = (tabItemEl as HTMLElement).offsetLeft;
|
||||
const tabItemOffsetWidth = (tabItemEl as HTMLElement).offsetWidth;
|
||||
// 标签页导航栏可视长度(不包含溢出部分)
|
||||
const scrollbarDomWidth = scrollbarDom.value
|
||||
? scrollbarDom.value.offsetWidth
|
||||
: 0;
|
||||
// 已有标签页总长度(包含溢出部分)
|
||||
const tabDomWidth = tabDom.value ? tabDom.value.offsetWidth : 0;
|
||||
|
||||
if (tabDomWidth < scrollbarDomWidth || tabItemElOffsetLeft === 0) {
|
||||
translateX.value = 0;
|
||||
} else if (tabItemElOffsetLeft < -translateX.value) {
|
||||
// 标签在可视区域左侧
|
||||
translateX.value = -tabItemElOffsetLeft + tabNavPadding;
|
||||
} else if (
|
||||
tabItemElOffsetLeft > -translateX.value &&
|
||||
tabItemElOffsetLeft + tabItemOffsetWidth <
|
||||
-translateX.value + scrollbarDomWidth
|
||||
) {
|
||||
// 标签在可视区域
|
||||
translateX.value = Math.min(
|
||||
0,
|
||||
scrollbarDomWidth -
|
||||
tabItemOffsetWidth -
|
||||
tabItemElOffsetLeft -
|
||||
tabNavPadding
|
||||
);
|
||||
} else {
|
||||
// 标签在可视区域右侧
|
||||
translateX.value = -(
|
||||
tabItemElOffsetLeft -
|
||||
(scrollbarDomWidth - tabNavPadding - tabItemOffsetWidth)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleScroll = (offset: number): void => {
|
||||
const scrollbarDomWidth = scrollbarDom.value
|
||||
? scrollbarDom.value?.offsetWidth
|
||||
: 0;
|
||||
const tabDomWidth = tabDom.value ? tabDom.value.offsetWidth : 0;
|
||||
if (offset > 0) {
|
||||
translateX.value = Math.min(0, translateX.value + offset);
|
||||
} else {
|
||||
if (scrollbarDomWidth < tabDomWidth) {
|
||||
if (translateX.value >= -(tabDomWidth - scrollbarDomWidth)) {
|
||||
translateX.value = Math.max(
|
||||
translateX.value + offset,
|
||||
scrollbarDomWidth - tabDomWidth
|
||||
);
|
||||
}
|
||||
} else {
|
||||
translateX.value = 0;
|
||||
}
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
const tagsViews = ref<Array<tagsViewsType>>([
|
||||
{
|
||||
icon: refresh,
|
||||
@@ -58,41 +196,38 @@ const tagsViews = ref<Array<tagsViewsType>>([
|
||||
icon: close,
|
||||
text: "message.hscloseCurrentTab",
|
||||
divided: false,
|
||||
disabled: routerArrays.length > 1 ? false : true,
|
||||
disabled: multiTags.value.length > 1 ? false : true,
|
||||
show: true
|
||||
},
|
||||
{
|
||||
icon: closeLeft,
|
||||
text: "message.hscloseLeftTabs",
|
||||
divided: true,
|
||||
disabled: routerArrays.length > 1 ? false : true,
|
||||
disabled: multiTags.value.length > 1 ? false : true,
|
||||
show: true
|
||||
},
|
||||
{
|
||||
icon: closeRight,
|
||||
text: "message.hscloseRightTabs",
|
||||
divided: false,
|
||||
disabled: routerArrays.length > 1 ? false : true,
|
||||
disabled: multiTags.value.length > 1 ? false : true,
|
||||
show: true
|
||||
},
|
||||
{
|
||||
icon: closeOther,
|
||||
text: "message.hscloseOtherTabs",
|
||||
divided: true,
|
||||
disabled: routerArrays.length > 2 ? false : true,
|
||||
disabled: multiTags.value.length > 2 ? false : true,
|
||||
show: true
|
||||
},
|
||||
{
|
||||
icon: closeAll,
|
||||
text: "message.hscloseAllTabs",
|
||||
divided: false,
|
||||
disabled: routerArrays.length > 1 ? false : true,
|
||||
disabled: multiTags.value.length > 1 ? false : true,
|
||||
show: true
|
||||
}
|
||||
]);
|
||||
const dynamicTagList: ComputedRef<Array<RouteConfigs>> = computed(() => {
|
||||
return relativeStorage.routesInStorage;
|
||||
});
|
||||
|
||||
// 显示模式,默认灵动模式显示
|
||||
const showModel = ref(storageLocal.getItem("showModel") || "smart");
|
||||
@@ -108,7 +243,7 @@ let buttonTop = ref(0);
|
||||
let currentSelect = ref({});
|
||||
|
||||
function dynamicRouteTag(value: string, parentPath: string): void {
|
||||
const hasValue = relativeStorage.routesInStorage.some((item: any) => {
|
||||
const hasValue = multiTags.value.some(item => {
|
||||
return item.path === value;
|
||||
});
|
||||
|
||||
@@ -117,12 +252,12 @@ function dynamicRouteTag(value: string, parentPath: string): void {
|
||||
arr.forEach((arrItem: any) => {
|
||||
let pathConcat = parentPath + arrItem.path;
|
||||
if (arrItem.path === value || pathConcat === value) {
|
||||
routerArrays.push({
|
||||
useMultiTagsStoreHook().handleTags("push", {
|
||||
path: value,
|
||||
parentPath: `/${parentPath.split("/")[1]}`,
|
||||
meta: arrItem.meta
|
||||
meta: arrItem.meta,
|
||||
name: arrItem.name
|
||||
});
|
||||
relativeStorage.routesInStorage = routerArrays;
|
||||
} else {
|
||||
if (arrItem.children && arrItem.children.length > 0) {
|
||||
concatPath(arrItem.children, value, parentPath);
|
||||
@@ -137,9 +272,10 @@ function dynamicRouteTag(value: string, parentPath: string): void {
|
||||
// 重新加载
|
||||
function onFresh() {
|
||||
toggleClass(true, refreshButton, document.querySelector(".rotate"));
|
||||
const { fullPath } = unref(route);
|
||||
const { fullPath, query } = unref(route);
|
||||
router.replace({
|
||||
path: "/redirect" + fullPath
|
||||
path: "/redirect" + fullPath,
|
||||
query: query
|
||||
});
|
||||
setTimeout(() => {
|
||||
removeClass(document.querySelector(".rotate"), refreshButton);
|
||||
@@ -147,32 +283,44 @@ function onFresh() {
|
||||
}
|
||||
|
||||
function deleteDynamicTag(obj: any, current: any, tag?: string) {
|
||||
let valueIndex: number = routerArrays.findIndex((item: any) => {
|
||||
return item.path === obj.path;
|
||||
// 存放被删除的缓存路由
|
||||
let delAliveRouteList = [];
|
||||
let valueIndex: number = multiTags.value.findIndex((item: any) => {
|
||||
if (item.query) {
|
||||
if (item.path === obj.path) {
|
||||
return item.query === obj.query;
|
||||
}
|
||||
} else {
|
||||
return item.path === obj.path;
|
||||
}
|
||||
});
|
||||
|
||||
const spliceRoute = (start?: number, end?: number, other?: boolean): void => {
|
||||
const spliceRoute = (
|
||||
startIndex?: number,
|
||||
length?: number,
|
||||
other?: boolean
|
||||
): void => {
|
||||
if (other) {
|
||||
relativeStorage.routesInStorage = [
|
||||
useMultiTagsStoreHook().handleTags("equal", [
|
||||
{
|
||||
path: "/welcome",
|
||||
parentPath: "/",
|
||||
meta: {
|
||||
title: "message.hshome",
|
||||
i18n: true,
|
||||
icon: "el-icon-s-home",
|
||||
showLink: true
|
||||
}
|
||||
},
|
||||
obj
|
||||
];
|
||||
routerArrays = relativeStorage.routesInStorage;
|
||||
]);
|
||||
} else {
|
||||
routerArrays.splice(start, end);
|
||||
relativeStorage.routesInStorage = routerArrays;
|
||||
// @ts-ignore
|
||||
delAliveRouteList = useMultiTagsStoreHook().handleTags("splice", "", {
|
||||
startIndex,
|
||||
length
|
||||
});
|
||||
}
|
||||
router.push(obj.path);
|
||||
// 删除缓存路由
|
||||
handleAliveRoute(route.matched, "delete");
|
||||
};
|
||||
|
||||
if (tag === "other") {
|
||||
@@ -180,20 +328,37 @@ function deleteDynamicTag(obj: any, current: any, tag?: string) {
|
||||
} else if (tag === "left") {
|
||||
spliceRoute(1, valueIndex - 1);
|
||||
} else if (tag === "right") {
|
||||
spliceRoute(valueIndex + 1, routerArrays.length);
|
||||
spliceRoute(valueIndex + 1, multiTags.value.length);
|
||||
} else {
|
||||
// 从当前匹配到的路径中删除
|
||||
spliceRoute(valueIndex, 1);
|
||||
}
|
||||
|
||||
if (current === obj.path) {
|
||||
let newRoute = useMultiTagsStoreHook().handleTags("slice");
|
||||
if (current === route.path) {
|
||||
// 删除缓存路由
|
||||
tag
|
||||
? delAliveRoutes(delAliveRouteList)
|
||||
: handleAliveRoute(route.matched, "delete");
|
||||
// 如果删除当前激活tag就自动切换到最后一个tag
|
||||
let newRoute: any = routerArrays.slice(-1);
|
||||
if (tag === "left") return;
|
||||
nextTick(() => {
|
||||
router.push({
|
||||
path: newRoute[0].path
|
||||
path: newRoute[0].path,
|
||||
query: newRoute[0].query
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// 删除缓存路由
|
||||
tag ? delAliveRoutes(delAliveRouteList) : delAliveRoutes([obj]);
|
||||
if (!multiTags.value.length) return;
|
||||
let isHasActiveTag = multiTags.value.some(item => {
|
||||
return item.path === route.path;
|
||||
});
|
||||
!isHasActiveTag &&
|
||||
router.push({
|
||||
path: newRoute[0].path,
|
||||
query: newRoute[0].query
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,6 +368,19 @@ function deleteMenu(item, tag?: string) {
|
||||
|
||||
function onClickDrop(key, item, selectRoute?: RouteConfigs) {
|
||||
if (item && item.disabled) return;
|
||||
|
||||
let selectTagRoute;
|
||||
if (selectRoute) {
|
||||
selectTagRoute = {
|
||||
path: selectRoute.path,
|
||||
meta: selectRoute.meta,
|
||||
name: selectRoute.name,
|
||||
query: selectRoute.query
|
||||
};
|
||||
} else {
|
||||
selectTagRoute = { path: route.path, meta: route.meta };
|
||||
}
|
||||
|
||||
// 当前路由信息
|
||||
switch (key) {
|
||||
case 0:
|
||||
@@ -211,57 +389,32 @@ function onClickDrop(key, item, selectRoute?: RouteConfigs) {
|
||||
break;
|
||||
case 1:
|
||||
// 关闭当前标签页
|
||||
selectRoute
|
||||
? deleteMenu({ path: selectRoute.path, meta: selectRoute.meta })
|
||||
: deleteMenu({ path: route.path, meta: route.meta });
|
||||
deleteMenu(selectTagRoute);
|
||||
break;
|
||||
case 2:
|
||||
// 关闭左侧标签页
|
||||
selectRoute
|
||||
? deleteMenu(
|
||||
{
|
||||
path: selectRoute.path,
|
||||
meta: selectRoute.meta
|
||||
},
|
||||
"left"
|
||||
)
|
||||
: deleteMenu({ path: route.path, meta: route.meta }, "left");
|
||||
deleteMenu(selectTagRoute, "left");
|
||||
break;
|
||||
case 3:
|
||||
// 关闭右侧标签页
|
||||
selectRoute
|
||||
? deleteMenu(
|
||||
{
|
||||
path: selectRoute.path,
|
||||
meta: selectRoute.meta
|
||||
},
|
||||
"right"
|
||||
)
|
||||
: deleteMenu({ path: route.path, meta: route.meta }, "right");
|
||||
deleteMenu(selectTagRoute, "right");
|
||||
break;
|
||||
case 4:
|
||||
// 关闭其他标签页
|
||||
selectRoute
|
||||
? deleteMenu(
|
||||
{
|
||||
path: selectRoute.path,
|
||||
meta: selectRoute.meta
|
||||
},
|
||||
"other"
|
||||
)
|
||||
: deleteMenu({ path: route.path, meta: route.meta }, "other");
|
||||
deleteMenu(selectTagRoute, "other");
|
||||
break;
|
||||
case 5:
|
||||
// 关闭全部标签页
|
||||
routerArrays.splice(1, routerArrays.length);
|
||||
relativeStorage.routesInStorage = routerArrays;
|
||||
useMultiTagsStoreHook().handleTags("splice", "", {
|
||||
startIndex: 1,
|
||||
length: multiTags.value.length
|
||||
});
|
||||
usePermissionStoreHook().clearAllCachePage();
|
||||
router.push("/welcome");
|
||||
|
||||
break;
|
||||
}
|
||||
setTimeout(() => {
|
||||
showMenuModel(route.fullPath);
|
||||
showMenuModel(route.fullPath, route.query);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -274,31 +427,44 @@ function closeMenu() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
function showMenus(value: Boolean) {
|
||||
function showMenus(value: boolean) {
|
||||
Array.of(1, 2, 3, 4, 5).forEach(v => {
|
||||
tagsViews.value[v].show = value;
|
||||
});
|
||||
}
|
||||
|
||||
function disabledMenus(value: Boolean) {
|
||||
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,右侧没有菜单,则不显示关闭右侧标签页
|
||||
function showMenuModel(
|
||||
currentPath: string,
|
||||
query: object = {},
|
||||
refresh = false
|
||||
) {
|
||||
let allRoute = multiTags.value;
|
||||
let routeLength = multiTags.value.length;
|
||||
let currentIndex = -1;
|
||||
if (isEmpty(query)) {
|
||||
currentIndex = allRoute.findIndex(v => v.path === currentPath);
|
||||
} else {
|
||||
currentIndex = allRoute.findIndex(v => isEqual(v.query, query));
|
||||
}
|
||||
|
||||
showMenus(true);
|
||||
|
||||
if (refresh) {
|
||||
tagsViews.value[0].show = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* currentIndex为1时,左侧的菜单是首页,则不显示关闭左侧标签页
|
||||
* 如果currentIndex等于routeLength-1,右侧没有菜单,则不显示关闭右侧标签页
|
||||
*/
|
||||
|
||||
if (currentIndex === 1 && routeLength !== 2) {
|
||||
// 左侧的菜单是首页,右侧存在别的菜单
|
||||
tagsViews.value[2].show = false;
|
||||
@@ -337,10 +503,10 @@ function openMenu(tag, e) {
|
||||
} else if (route.path !== tag.path) {
|
||||
// 右键菜单不匹配当前路由,隐藏刷新
|
||||
tagsViews.value[0].show = false;
|
||||
showMenuModel(tag.path);
|
||||
showMenuModel(tag.path, tag.query);
|
||||
} else if (
|
||||
// eslint-disable-next-line no-dupe-else-if
|
||||
relativeStorage.routesInStorage.length === 2 &&
|
||||
multiTags.value.length === 2 &&
|
||||
route.path !== tag.path
|
||||
) {
|
||||
showMenus(true);
|
||||
@@ -348,7 +514,7 @@ function openMenu(tag, e) {
|
||||
tagsViews.value[4].show = false;
|
||||
} else if (route.path === tag.path) {
|
||||
// 右键当前激活的菜单
|
||||
showMenuModel(tag.path, true);
|
||||
showMenuModel(tag.path, tag.query, true);
|
||||
}
|
||||
|
||||
currentSelect.value = tag;
|
||||
@@ -362,7 +528,9 @@ function openMenu(tag, e) {
|
||||
} else {
|
||||
buttonLeft.value = left;
|
||||
}
|
||||
buttonTop.value = e.clientY + 10;
|
||||
pureSetting.hiddenSideBar
|
||||
? (buttonTop.value = e.clientY)
|
||||
: (buttonTop.value = e.clientY - 40);
|
||||
setTimeout(() => {
|
||||
visible.value = true;
|
||||
}, 10);
|
||||
@@ -370,7 +538,11 @@ function openMenu(tag, e) {
|
||||
|
||||
// 触发tags标签切换
|
||||
function tagOnClick(item) {
|
||||
showMenuModel(item.path);
|
||||
router.push({
|
||||
path: item?.path,
|
||||
query: item?.query
|
||||
});
|
||||
showMenuModel(item?.path, item?.query);
|
||||
}
|
||||
|
||||
// 鼠标移入
|
||||
@@ -414,8 +586,6 @@ watch(
|
||||
|
||||
onBeforeMount(() => {
|
||||
if (!instance) return;
|
||||
relativeStorage = instance.appContext.app.config.globalProperties.$storage;
|
||||
routerArrays = relativeStorage.routesInStorage ?? routerArrays;
|
||||
|
||||
// 根据当前路由初始化操作标签页的禁用状态
|
||||
showMenuModel(route.fullPath);
|
||||
@@ -443,38 +613,51 @@ onBeforeMount(() => {
|
||||
|
||||
<template>
|
||||
<div ref="containerDom" class="tags-view" v-if="!showTags">
|
||||
<el-scrollbar wrap-class="scrollbar-wrapper" class="scroll-container">
|
||||
<i class="ri-arrow-left-s-line" @click="handleScroll(200)"></i>
|
||||
<div ref="scrollbarDom" 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)"
|
||||
class="tab"
|
||||
ref="tabDom"
|
||||
:style="{ transform: `translateX(${translateX}px)` }"
|
||||
>
|
||||
<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>
|
||||
:ref="'dynamic' + index"
|
||||
v-for="(item, index) in multiTags"
|
||||
:key="index"
|
||||
:class="[
|
||||
'scroll-item is-closable',
|
||||
linkIsActive(item),
|
||||
$route.path === item.path && showModel === 'card'
|
||||
? 'card-active'
|
||||
: ''
|
||||
]"
|
||||
@contextmenu.prevent="openMenu(item, $event)"
|
||||
@mouseenter.prevent="onMouseenter(item, index)"
|
||||
@mouseleave.prevent="onMouseleave(item, index)"
|
||||
@click="tagOnClick(item)"
|
||||
>
|
||||
<router-link :to="item.path"
|
||||
>{{ transformI18n(item.meta.title, item.meta.i18n) }}
|
||||
</router-link>
|
||||
<el-icon
|
||||
v-if="
|
||||
iconIsActive(item, index) ||
|
||||
(index === activeIndex && index !== 0)
|
||||
"
|
||||
class="el-icon-close"
|
||||
@click.stop="deleteMenu(item)"
|
||||
>
|
||||
<CloseBold />
|
||||
</el-icon>
|
||||
<div
|
||||
:ref="'schedule' + index"
|
||||
v-if="showModel !== 'card'"
|
||||
:class="[scheduleIsActive(item)]"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
<i class="ri-arrow-right-s-line" @click="handleScroll(-200)"></i>
|
||||
<!-- 右键菜单按钮 -->
|
||||
<transition name="el-zoom-in-top">
|
||||
<ul
|
||||
@@ -498,15 +681,19 @@ onBeforeMount(() => {
|
||||
<!-- 右侧功能按钮 -->
|
||||
<ul class="right-button">
|
||||
<li>
|
||||
<i
|
||||
<el-icon
|
||||
:title="$t('message.hsrefreshRoute')"
|
||||
class="el-icon-refresh-right rotate"
|
||||
@click="onFresh"
|
||||
></i>
|
||||
>
|
||||
<RefreshRight />
|
||||
</el-icon>
|
||||
</li>
|
||||
<li>
|
||||
<el-dropdown trigger="click" placement="bottom-end">
|
||||
<i class="el-icon-arrow-down"></i>
|
||||
<el-icon>
|
||||
<ArrowDown />
|
||||
</el-icon>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
@@ -531,277 +718,5 @@ onBeforeMount(() => {
|
||||
</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;
|
||||
}
|
||||
@import "./index.scss";
|
||||
</style>
|
||||
|
||||
@@ -1,53 +1,23 @@
|
||||
<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,
|
||||
h,
|
||||
reactive,
|
||||
computed,
|
||||
onMounted,
|
||||
watchEffect,
|
||||
useCssModule,
|
||||
onBeforeMount,
|
||||
defineComponent,
|
||||
getCurrentInstance
|
||||
} from "vue";
|
||||
import { setType } from "./types";
|
||||
import options from "/@/settings";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { routerArrays } from "./types";
|
||||
import { emitter } from "/@/utils/mitt";
|
||||
import { toggleClass } from "/@/utils/operate";
|
||||
import { useEventListener } from "@vueuse/core";
|
||||
import { storageLocal } from "/@/utils/storage";
|
||||
import backTop from "/@/assets/svg/back_top.svg";
|
||||
import { useAppStoreHook } from "/@/store/modules/app";
|
||||
import fullScreen from "/@/assets/svg/full_screen.svg";
|
||||
import exitScreen from "/@/assets/svg/exit_screen.svg";
|
||||
import { deviceDetection } from "/@/utils/deviceDetection";
|
||||
import { useSettingStoreHook } from "/@/store/modules/settings";
|
||||
import { useMultiTagsStore } from "/@/store/modules/multiTags";
|
||||
|
||||
import navbar from "./components/navbar.vue";
|
||||
import tag from "./components/tag/index.vue";
|
||||
@@ -56,13 +26,46 @@ import setting from "./components/setting/index.vue";
|
||||
import Vertical from "./components/sidebar/vertical.vue";
|
||||
import Horizontal from "./components/sidebar/horizontal.vue";
|
||||
|
||||
const isMobile = deviceDetection();
|
||||
const pureSetting = useSettingStoreHook();
|
||||
const { hiddenMainContainer } = useCssModule();
|
||||
const instance = getCurrentInstance().appContext.app.config.globalProperties;
|
||||
|
||||
const instance =
|
||||
getCurrentInstance().appContext.app.config.globalProperties.$storage;
|
||||
|
||||
let containerHiddenSideBar = ref(options.hiddenSideBar);
|
||||
// 清空缓存后从serverConfig.json读取默认配置并赋值到storage中
|
||||
const layout = computed(() => {
|
||||
// 路由
|
||||
if (
|
||||
useMultiTagsStore().multiTagsCache &&
|
||||
(!instance.$storage.tags || instance.$storage.tags.length === 0)
|
||||
) {
|
||||
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
|
||||
instance.$storage.tags = routerArrays;
|
||||
}
|
||||
// 国际化
|
||||
if (!instance.$storage.locale) {
|
||||
// eslint-disable-next-line
|
||||
instance.$storage.locale = { locale: instance.$config?.Locale ?? "zh" };
|
||||
useI18n().locale.value = instance.$config?.Locale ?? "zh";
|
||||
}
|
||||
// 导航
|
||||
if (!instance.$storage.layout) {
|
||||
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
|
||||
instance.$storage.layout = {
|
||||
layout: instance.$config?.Layout ?? "vertical",
|
||||
theme: instance.$config?.Theme ?? "default"
|
||||
};
|
||||
}
|
||||
// 灰色模式、色弱模式、隐藏标签页
|
||||
if (!instance.$storage.sets) {
|
||||
// eslint-disable-next-line
|
||||
instance.$storage.sets = {
|
||||
grey: instance.$config?.Grey ?? false,
|
||||
weak: instance.$config?.Weak ?? false,
|
||||
hideTabs: instance.$config?.HideTabs ?? false,
|
||||
multiTagsCache: instance.$config?.MultiTagsCache ?? false
|
||||
};
|
||||
}
|
||||
return instance.$storage?.layout.layout;
|
||||
});
|
||||
|
||||
const set: setType = reactive({
|
||||
sidebar: computed(() => {
|
||||
@@ -84,81 +87,108 @@ const set: setType = reactive({
|
||||
withoutAnimation: set.sidebar.withoutAnimation,
|
||||
mobile: set.device === "mobile"
|
||||
};
|
||||
}),
|
||||
|
||||
hideTabs: computed(() => {
|
||||
return instance.$storage?.sets.hideTabs;
|
||||
})
|
||||
});
|
||||
|
||||
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}` };
|
||||
window.document.body.setAttribute("layout", layoutModel);
|
||||
instance.$storage.layout = {
|
||||
layout: `${layoutModel}`,
|
||||
theme: instance.$storage.layout?.theme
|
||||
};
|
||||
}
|
||||
|
||||
function toggle(device: string, bool: boolean) {
|
||||
useAppStoreHook().toggleDevice(device);
|
||||
useAppStoreHook().toggleSideBar(bool, "resize");
|
||||
}
|
||||
|
||||
// 判断是否可自动关闭菜单栏
|
||||
let isAutoCloseSidebar = true;
|
||||
|
||||
// 监听容器
|
||||
emitter.on("resize", ({ detail }) => {
|
||||
if (isMobile) return;
|
||||
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);
|
||||
/** width app-wrapper类容器宽度
|
||||
* 0 < width <= 760 隐藏侧边栏
|
||||
* 760 < width <= 990 折叠侧边栏
|
||||
* width > 990 展开侧边栏
|
||||
*/
|
||||
if (width > 0 && width <= 760) {
|
||||
toggle("mobile", false);
|
||||
isAutoCloseSidebar = true;
|
||||
} else if (width > 760 && width <= 990) {
|
||||
if (isAutoCloseSidebar) {
|
||||
toggle("desktop", false);
|
||||
isAutoCloseSidebar = false;
|
||||
}
|
||||
} else if (width > 990) {
|
||||
if (!set.sidebar.isClickHamburger) {
|
||||
toggle("desktop", true);
|
||||
isAutoCloseSidebar = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function onFullScreen() {
|
||||
if (unref(containerHiddenSideBar)) {
|
||||
containerHiddenSideBar.value = false;
|
||||
toggleClass(
|
||||
false,
|
||||
hiddenMainContainer,
|
||||
document.querySelector(".main-container")
|
||||
);
|
||||
} else {
|
||||
containerHiddenSideBar.value = true;
|
||||
toggleClass(
|
||||
true,
|
||||
hiddenMainContainer,
|
||||
document.querySelector(".main-container")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const isMobile = $_isMobile();
|
||||
if (isMobile) {
|
||||
useAppStoreHook().toggleDevice("mobile");
|
||||
handleClickOutside(true);
|
||||
}
|
||||
toggleClass(
|
||||
unref(containerHiddenSideBar),
|
||||
hiddenMainContainer,
|
||||
document.querySelector(".main-container")
|
||||
);
|
||||
});
|
||||
|
||||
onBeforeMount(() => {
|
||||
useEventListener("resize", $_resizeHandler);
|
||||
onMounted(() => {
|
||||
if (isMobile) {
|
||||
toggle("mobile", false);
|
||||
}
|
||||
});
|
||||
|
||||
function onFullScreen() {
|
||||
pureSetting.hiddenSideBar
|
||||
? pureSetting.changeSetting({ key: "hiddenSideBar", value: false })
|
||||
: pureSetting.changeSetting({ key: "hiddenSideBar", value: true });
|
||||
}
|
||||
|
||||
const layoutHeader = defineComponent({
|
||||
render() {
|
||||
return h(
|
||||
"div",
|
||||
{
|
||||
class: { "fixed-header": set.fixedHeader },
|
||||
style: [
|
||||
set.hideTabs && layout.value.includes("horizontal")
|
||||
? "box-shadow: 0 1px 4px rgb(0 21 41 / 8%);"
|
||||
: ""
|
||||
]
|
||||
},
|
||||
{
|
||||
default: () => [
|
||||
!pureSetting.hiddenSideBar && layout.value.includes("vertical")
|
||||
? h(navbar)
|
||||
: h("div"),
|
||||
!pureSetting.hiddenSideBar && layout.value.includes("horizontal")
|
||||
? h(Horizontal)
|
||||
: h("div"),
|
||||
h(
|
||||
tag,
|
||||
{},
|
||||
{
|
||||
default: () => [
|
||||
h(
|
||||
"span",
|
||||
{ onClick: onFullScreen },
|
||||
{
|
||||
default: () => [
|
||||
!pureSetting.hiddenSideBar ? h(fullScreen) : h(exitScreen)
|
||||
]
|
||||
}
|
||||
)
|
||||
]
|
||||
}
|
||||
)
|
||||
]
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -170,41 +200,39 @@ onBeforeMount(() => {
|
||||
set.sidebar.opened &&
|
||||
layout.includes('vertical')
|
||||
"
|
||||
class="drawer-bg"
|
||||
@click="handleClickOutside(false)"
|
||||
class="app-mask"
|
||||
@click="useAppStoreHook().toggleSideBar()"
|
||||
/>
|
||||
<Vertical v-show="!containerHiddenSideBar && layout.includes('vertical')" />
|
||||
<div class="main-container">
|
||||
<div :class="{ 'fixed-header': set.fixedHeader }">
|
||||
<!-- 顶部导航栏 -->
|
||||
<navbar
|
||||
v-show="!containerHiddenSideBar && layout.includes('vertical')"
|
||||
/>
|
||||
<!-- tabs标签页 -->
|
||||
<Horizontal
|
||||
v-show="!containerHiddenSideBar && layout.includes('horizontal')"
|
||||
/>
|
||||
<tag>
|
||||
<span @click="onFullScreen">
|
||||
<fullScreen v-if="!containerHiddenSideBar" />
|
||||
<exitScreen v-else />
|
||||
</span>
|
||||
</tag>
|
||||
<Vertical
|
||||
v-show="!pureSetting.hiddenSideBar && layout.includes('vertical')"
|
||||
/>
|
||||
<div
|
||||
:class="[
|
||||
'main-container',
|
||||
pureSetting.hiddenSideBar ? 'main-hidden' : ''
|
||||
]"
|
||||
>
|
||||
<div v-if="set.fixedHeader">
|
||||
<layout-header />
|
||||
<!-- 主体内容 -->
|
||||
<app-main :fixed-header="set.fixedHeader" />
|
||||
</div>
|
||||
<!-- 主体内容 -->
|
||||
<app-main />
|
||||
<el-scrollbar v-else>
|
||||
<el-backtop
|
||||
title="回到顶部"
|
||||
target=".main-container .el-scrollbar__wrap"
|
||||
><backTop />
|
||||
</el-backtop>
|
||||
<layout-header />
|
||||
<!-- 主体内容 -->
|
||||
<app-main :fixed-header="set.fixedHeader" />
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
<!-- 系统设置 -->
|
||||
<setting />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped module>
|
||||
.hiddenMainContainer {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@mixin clearfix {
|
||||
&::after {
|
||||
@@ -227,7 +255,11 @@ onBeforeMount(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.drawer-bg {
|
||||
.main-hidden {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
.app-mask {
|
||||
background: #000;
|
||||
opacity: 0.3;
|
||||
width: 100%;
|
||||
@@ -237,19 +269,6 @@ onBeforeMount(() => {
|
||||
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;
|
||||
}
|
||||
|
||||
15
src/layout/routerView/parent.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<router-view>
|
||||
<template #default="{ Component, route }">
|
||||
<transition appear name="fade-transform" mode="out-in">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</transition>
|
||||
</template>
|
||||
</router-view>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: "layoutParentView"
|
||||
};
|
||||
</script>
|
||||
11
src/layout/theme/auroraGreen-vars.scss
Normal file
@@ -0,0 +1,11 @@
|
||||
/* 酸橙绿 */
|
||||
$subMenuActiveText: #fff;
|
||||
$menuBg: #0b1e15;
|
||||
$menuHover: #60ac80;
|
||||
$subMenuBg: #000;
|
||||
$subMenuActiveBg: #60ac80;
|
||||
$navTextColor: #7a80b4;
|
||||
$menuText: #7a80b4;
|
||||
$sidebarLogo: #112f21;
|
||||
$menuTitleHover: #fff;
|
||||
$menuActiveBefore: #60ac80;
|
||||
29
src/layout/theme/default-vars.scss
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 道奇蓝(默认)
|
||||
* 此scss变量文件作为multipleScopeVars去编译时,会自动移除!default以达到变量提升
|
||||
* 同时此scss变量文件作为默认主题变量文件,被其他.scss通过 @import 时,必需 !default
|
||||
*/
|
||||
|
||||
/* 菜单选中后字体样式 */
|
||||
$subMenuActiveText: #fff !default;
|
||||
|
||||
/* 菜单背景 */
|
||||
$menuBg: #001529 !default;
|
||||
|
||||
/* 鼠标覆盖到菜单时的背景 */
|
||||
$menuHover: #4091f7 !default;
|
||||
|
||||
/* 子菜单背景 */
|
||||
$subMenuBg: #0f0303 !default;
|
||||
|
||||
/* 有无子集的激活菜单背景 */
|
||||
$subMenuActiveBg: #4091f7 !default;
|
||||
$navTextColor: #fff !default;
|
||||
$menuText: rgba(254, 254, 254, 0.65) !default;
|
||||
|
||||
/* logo背景颜色 */
|
||||
$sidebarLogo: #002140 !default;
|
||||
|
||||
/* 鼠标覆盖到菜单时的字体颜色 */
|
||||
$menuTitleHover: #fff !default;
|
||||
$menuActiveBefore: #4091f7 !default;
|
||||
11
src/layout/theme/dusk-vars.scss
Normal file
@@ -0,0 +1,11 @@
|
||||
/* 猩红色 */
|
||||
$subMenuActiveText: #fff;
|
||||
$menuBg: #2a0608;
|
||||
$menuHover: #e13c39;
|
||||
$subMenuBg: #000;
|
||||
$subMenuActiveBg: #e13c39;
|
||||
$navTextColor: red;
|
||||
$menuText: rgba(254, 254, 254, 0.651);
|
||||
$sidebarLogo: #42090c;
|
||||
$menuTitleHover: #fff;
|
||||
$menuActiveBefore: #e13c39;
|
||||
11
src/layout/theme/light-vars.scss
Normal file
@@ -0,0 +1,11 @@
|
||||
/* 亮白色 */
|
||||
$subMenuActiveText: #409eff;
|
||||
$menuBg: #fff;
|
||||
$menuHover: #e0ebf6;
|
||||
$subMenuBg: #fff;
|
||||
$subMenuActiveBg: #e0ebf6;
|
||||
$navTextColor: #7a80b4;
|
||||
$menuText: #7a80b4;
|
||||
$sidebarLogo: #fff;
|
||||
$menuTitleHover: #000;
|
||||
$menuActiveBefore: #4091f7;
|
||||
11
src/layout/theme/mingQing-vars.scss
Normal file
@@ -0,0 +1,11 @@
|
||||
/* 绿宝石 */
|
||||
$subMenuActiveText: #fff;
|
||||
$menuBg: #032121;
|
||||
$menuHover: #59bfc1;
|
||||
$subMenuBg: #000;
|
||||
$subMenuActiveBg: #59bfc1;
|
||||
$navTextColor: #7a80b4;
|
||||
$menuText: #7a80b4;
|
||||
$sidebarLogo: #053434;
|
||||
$menuTitleHover: #fff;
|
||||
$menuActiveBefore: #59bfc1;
|
||||
11
src/layout/theme/pink-vars.scss
Normal file
@@ -0,0 +1,11 @@
|
||||
/* 深粉色 */
|
||||
$subMenuActiveText: #fff;
|
||||
$menuBg: #28081a;
|
||||
$menuHover: #d84493;
|
||||
$subMenuBg: #000;
|
||||
$subMenuActiveBg: #d84493;
|
||||
$navTextColor: #7a80b4;
|
||||
$menuText: #7a80b4;
|
||||
$sidebarLogo: #3f0d29;
|
||||
$menuTitleHover: #fff;
|
||||
$menuActiveBefore: #d84493;
|
||||
11
src/layout/theme/saucePurple-vars.scss
Normal file
@@ -0,0 +1,11 @@
|
||||
/* 深紫罗兰色 */
|
||||
$subMenuActiveText: #fff;
|
||||
$menuBg: #130824;
|
||||
$menuHover: #693ac9;
|
||||
$subMenuBg: #000;
|
||||
$subMenuActiveBg: #693ac9;
|
||||
$navTextColor: #7a80b4;
|
||||
$menuText: #7a80b4;
|
||||
$sidebarLogo: #1f0c38;
|
||||
$menuTitleHover: #fff;
|
||||
$menuActiveBefore: #693ac9;
|
||||
11
src/layout/theme/volcano-vars.scss
Normal file
@@ -0,0 +1,11 @@
|
||||
/* 橙红色 */
|
||||
$subMenuActiveText: #fff;
|
||||
$menuBg: #2b0e05;
|
||||
$menuHover: #e85f33;
|
||||
$subMenuBg: #0f0603;
|
||||
$subMenuActiveBg: #e85f33;
|
||||
$navTextColor: #fff;
|
||||
$menuText: rgba(254, 254, 254, 0.65);
|
||||
$sidebarLogo: #441708;
|
||||
$menuTitleHover: #fff;
|
||||
$menuActiveBefore: #e85f33;
|
||||
11
src/layout/theme/yellow-vars.scss
Normal file
@@ -0,0 +1,11 @@
|
||||
/* 金色 */
|
||||
$subMenuActiveText: #d25f00;
|
||||
$menuBg: #2b2503;
|
||||
$menuHover: #f6da4d;
|
||||
$subMenuBg: #0f0603;
|
||||
$subMenuActiveBg: #f6da4d;
|
||||
$navTextColor: #fff;
|
||||
$menuText: rgba(254, 254, 254, 0.65);
|
||||
$sidebarLogo: #443b05;
|
||||
$menuTitleHover: #fff;
|
||||
$menuActiveBefore: #f6da4d;
|
||||
@@ -1,36 +1,49 @@
|
||||
export const routerArrays: Array<RouteConfigs> = [
|
||||
{
|
||||
path: "/welcome",
|
||||
parentPath: "/",
|
||||
meta: {
|
||||
title: "message.hshome",
|
||||
i18n: true,
|
||||
icon: "el-icon-s-home",
|
||||
showLink: true
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export type RouteConfigs = {
|
||||
path?: string;
|
||||
parentPath?: string;
|
||||
query?: object;
|
||||
meta?: {
|
||||
title?: string;
|
||||
i18n?: boolean;
|
||||
icon?: string;
|
||||
showLink?: boolean;
|
||||
savedPosition?: boolean;
|
||||
authority?: Array<string>;
|
||||
};
|
||||
children?: RouteConfigs[];
|
||||
name?: string;
|
||||
};
|
||||
|
||||
export type relativeStorageType = {
|
||||
routesInStorage: Array<RouteConfigs>;
|
||||
export type multiTagsType = {
|
||||
tags: Array<RouteConfigs>;
|
||||
};
|
||||
|
||||
export type tagsViewsType = {
|
||||
icon: string;
|
||||
text: string;
|
||||
divided: {
|
||||
valueOf: () => boolean;
|
||||
};
|
||||
disabled: {
|
||||
valueOf: () => boolean;
|
||||
};
|
||||
show: {
|
||||
valueOf: () => boolean;
|
||||
};
|
||||
divided: boolean;
|
||||
disabled: boolean;
|
||||
show: boolean;
|
||||
};
|
||||
|
||||
export interface setType {
|
||||
sidebar: {
|
||||
opened: boolean;
|
||||
withoutAnimation: boolean;
|
||||
isClickHamburger: boolean;
|
||||
};
|
||||
device: string;
|
||||
fixedHeader: boolean;
|
||||
@@ -40,16 +53,33 @@ export interface setType {
|
||||
withoutAnimation: boolean;
|
||||
mobile: boolean;
|
||||
};
|
||||
hideTabs: 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;
|
||||
i18n?: boolean;
|
||||
extraIcon?: {
|
||||
svg?: boolean;
|
||||
name?: string;
|
||||
};
|
||||
};
|
||||
showTooltip?: boolean;
|
||||
};
|
||||
|
||||
export type themeColorsType = {
|
||||
rgb: string;
|
||||
themeColor: string;
|
||||
};
|
||||
|
||||
export interface scrollbarDomType extends HTMLElement {
|
||||
wrap?: {
|
||||
offsetWidth: number;
|
||||
};
|
||||
}
|
||||
|
||||
10
src/main.ts
@@ -4,7 +4,9 @@ import { setupStore } from "/@/store";
|
||||
import { getServerConfig } from "./config";
|
||||
import { createApp, Directive } from "vue";
|
||||
import { usI18n } from "../src/plugins/i18n";
|
||||
import { MotionPlugin } from "@vueuse/motion";
|
||||
import { useTable } from "../src/plugins/vxe-table";
|
||||
import { useFontawesome } from "../src/plugins/fontawesome";
|
||||
import { useElementPlus } from "../src/plugins/element-plus";
|
||||
import { injectResponsiveStorage } from "/@/utils/storage/responsive";
|
||||
|
||||
@@ -27,7 +29,13 @@ Object.keys(directives).forEach(key => {
|
||||
getServerConfig(app).then(async config => {
|
||||
injectResponsiveStorage(app, config);
|
||||
setupStore(app);
|
||||
app.use(router).use(useElementPlus).use(useTable).use(usI18n);
|
||||
app
|
||||
.use(router)
|
||||
.use(MotionPlugin)
|
||||
.use(useElementPlus)
|
||||
.use(useTable)
|
||||
.use(usI18n)
|
||||
.use(useFontawesome);
|
||||
await router.isReady();
|
||||
app.mount("#app");
|
||||
});
|
||||
|
||||
@@ -23,19 +23,35 @@ import {
|
||||
ElInput,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElLoading,
|
||||
ElPopover,
|
||||
ElPopper,
|
||||
ElTooltip,
|
||||
ElDrawer,
|
||||
ElPagination,
|
||||
ElAlert,
|
||||
ElRadio,
|
||||
ElRadioButton,
|
||||
ElRadioGroup,
|
||||
ElDescriptions,
|
||||
ElDescriptionsItem
|
||||
ElDescriptionsItem,
|
||||
ElBacktop,
|
||||
ElSwitch,
|
||||
ElBadge,
|
||||
ElTabs,
|
||||
ElTabPane,
|
||||
ElAvatar,
|
||||
ElEmpty,
|
||||
ElCollapse,
|
||||
ElCollapseItem,
|
||||
ElTreeV2,
|
||||
// 指令
|
||||
ElLoading,
|
||||
ElInfiniteScroll
|
||||
} from "element-plus";
|
||||
|
||||
// Directives
|
||||
const plugins = [ElLoading, ElInfiniteScroll];
|
||||
|
||||
const components = [
|
||||
ElTag,
|
||||
ElAffix,
|
||||
@@ -66,19 +82,72 @@ const components = [
|
||||
ElDrawer,
|
||||
ElPagination,
|
||||
ElAlert,
|
||||
ElRadio,
|
||||
ElRadioButton,
|
||||
ElRadioGroup,
|
||||
ElDescriptions,
|
||||
ElDescriptionsItem
|
||||
ElDescriptionsItem,
|
||||
ElBacktop,
|
||||
ElSwitch,
|
||||
ElBadge,
|
||||
ElTabs,
|
||||
ElTabPane,
|
||||
ElAvatar,
|
||||
ElEmpty,
|
||||
ElCollapse,
|
||||
ElCollapseItem,
|
||||
ElTreeV2
|
||||
];
|
||||
|
||||
const plugins = [ElLoading];
|
||||
// https://element-plus.org/zh-CN/component/icon.html
|
||||
import {
|
||||
Check,
|
||||
Menu,
|
||||
HomeFilled,
|
||||
SetUp,
|
||||
Edit,
|
||||
Setting,
|
||||
Lollipop,
|
||||
Link,
|
||||
Position,
|
||||
Histogram,
|
||||
RefreshRight,
|
||||
ArrowDown,
|
||||
Close,
|
||||
CloseBold,
|
||||
Bell
|
||||
} from "@element-plus/icons-vue";
|
||||
|
||||
// Icon
|
||||
export const iconComponents = [
|
||||
Check,
|
||||
Menu,
|
||||
HomeFilled,
|
||||
SetUp,
|
||||
Edit,
|
||||
Setting,
|
||||
Lollipop,
|
||||
Link,
|
||||
Position,
|
||||
Histogram,
|
||||
RefreshRight,
|
||||
ArrowDown,
|
||||
Close,
|
||||
CloseBold,
|
||||
Bell
|
||||
];
|
||||
|
||||
export function useElementPlus(app: App) {
|
||||
// 注册组件
|
||||
components.forEach((component: Component) => {
|
||||
app.component(component.name, component);
|
||||
});
|
||||
// 注册指令
|
||||
plugins.forEach(plugin => {
|
||||
app.use(plugin);
|
||||
});
|
||||
// 注册图标
|
||||
iconComponents.forEach((component: Component) => {
|
||||
app.component(component.name, component);
|
||||
});
|
||||
}
|
||||
|
||||
21
src/plugins/fontawesome/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/** 兼容fontawesome4和5版本
|
||||
* 4版本: www.fontawesome.com.cn/faicons/
|
||||
* 5版本:https://fontawesome.com/v5.15/icons?d=gallery&p=2&m=free
|
||||
* https://github.com/FortAwesome/vue-fontawesome
|
||||
*/
|
||||
import { App } from "vue";
|
||||
import "font-awesome/css/font-awesome.css";
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import {
|
||||
faUserSecret,
|
||||
faCoffee,
|
||||
faSpinner
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
// github.com/Remix-Design/RemixIcon/blob/master/README_CN.md#%E5%AE%89%E8%A3%85%E5%BC%95%E5%85%A5
|
||||
import "remixicon/fonts/remixicon.css";
|
||||
|
||||
export function useFontawesome(app: App) {
|
||||
library.add(faUserSecret, faCoffee, faSpinner);
|
||||
app.component("font-awesome-icon", FontAwesomeIcon);
|
||||
}
|
||||
@@ -42,6 +42,8 @@ export const menusConfig = {
|
||||
permission: "权限管理",
|
||||
permissionPage: "页面权限",
|
||||
permissionButton: "按钮权限",
|
||||
hstabs: "标签页操作",
|
||||
hsMenuTree: "菜单树结构",
|
||||
externalLink: "外链"
|
||||
}
|
||||
},
|
||||
@@ -78,6 +80,8 @@ export const menusConfig = {
|
||||
permission: "Permission Manage",
|
||||
permissionPage: "Page Permission",
|
||||
permissionButton: "Button Permission",
|
||||
hstabs: "Tabs Operate",
|
||||
hsMenuTree: "Menu Tree",
|
||||
externalLink: "External Link"
|
||||
}
|
||||
}
|
||||
@@ -92,14 +96,13 @@ export const buttonConfig = {
|
||||
hsexitfullscreen: "退出全屏",
|
||||
hsrefreshRoute: "刷新路由",
|
||||
hslogin: "登陆",
|
||||
hsregister: "注册",
|
||||
hsadd: "新增",
|
||||
hsmark: "标记/取消",
|
||||
hssave: "保存",
|
||||
hssearch: "搜索",
|
||||
hsexpendAll: "全部展开",
|
||||
hscollapseAll: "全部折叠",
|
||||
hssystemSet: "系统设置",
|
||||
hssystemSet: "打开项目配置",
|
||||
hsdelete: "删除",
|
||||
hsreload: "重新加载",
|
||||
hscloseCurrentTab: "关闭当前标签页",
|
||||
@@ -116,14 +119,13 @@ export const buttonConfig = {
|
||||
hsexitfullscreen: "exitFullscreen",
|
||||
hsrefreshRoute: "refreshRoute",
|
||||
hslogin: "login",
|
||||
hsregister: "register",
|
||||
hsadd: "Add",
|
||||
hsmark: "Mark/Cancel",
|
||||
hssave: "Save",
|
||||
hssearch: "Search",
|
||||
hsexpendAll: "Expand All",
|
||||
hscollapseAll: "Collapse All",
|
||||
hssystemSet: "System Set",
|
||||
hssystemSet: "Open ProjectConfig",
|
||||
hsdelete: "Delete",
|
||||
hsreload: "Reload",
|
||||
hscloseCurrentTab: "Close CurrentTab",
|
||||
|
||||
@@ -13,3 +13,27 @@ export const i18n = createI18n({
|
||||
export function usI18n(app: App) {
|
||||
app.use(i18n);
|
||||
}
|
||||
|
||||
/**
|
||||
* 国际化转换工具函数
|
||||
* @param message message
|
||||
* @param isI18n 如果true,获取对应的消息,否则返回本身
|
||||
* @returns message
|
||||
*/
|
||||
export function transformI18n(message: string | object = "", isI18n = false) {
|
||||
if (!message) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// 处理存储动态路由的title,格式 {zh:"",en:""}
|
||||
if (typeof message === "object") {
|
||||
return message[i18n.global?.locale];
|
||||
}
|
||||
|
||||
if (isI18n) {
|
||||
//@ts-ignore
|
||||
return i18n.global.tc.call(i18n.global, message);
|
||||
} else {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import "xe-utils";
|
||||
import { App } from "vue";
|
||||
import { i18n } from "../i18n/index";
|
||||
import "font-awesome/css/font-awesome.css";
|
||||
import {
|
||||
// 核心
|
||||
VXETable,
|
||||
@@ -51,7 +50,7 @@ import {
|
||||
VXETable.setup({
|
||||
size: "medium",
|
||||
version: 0,
|
||||
zIndex: 100,
|
||||
zIndex: 1002,
|
||||
table: {
|
||||
// 自动监听父元素的变化去重新计算表格
|
||||
autoResize: true,
|
||||
|
||||
@@ -1,129 +1,27 @@
|
||||
import {
|
||||
Router,
|
||||
createRouter,
|
||||
RouteComponent,
|
||||
createWebHashHistory,
|
||||
RouteRecordNormalized
|
||||
} from "vue-router";
|
||||
import { split } from "lodash-es";
|
||||
import { i18n } from "/@/plugins/i18n";
|
||||
import { toRouteType } from "./types";
|
||||
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 editorRouter from "./modules/editor";
|
||||
import nestedRouter from "./modules/nested";
|
||||
import externalLink from "./modules/externalLink";
|
||||
import { constantRoutes } from "./modules";
|
||||
import { split, findIndex } from "lodash-es";
|
||||
import { transformI18n } from "/@/plugins/i18n";
|
||||
import remainingRouter from "./modules/remaining";
|
||||
import flowChartRouter from "./modules/flowchart";
|
||||
import componentsRouter from "./modules/components";
|
||||
// 动态路由
|
||||
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,
|
||||
flowChartRouter,
|
||||
editorRouter,
|
||||
componentsRouter,
|
||||
nestedRouter,
|
||||
externalLink,
|
||||
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
|
||||
).concat(...remainingRouter);
|
||||
|
||||
// 过滤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;
|
||||
};
|
||||
import { storageSession } from "/@/utils/storage";
|
||||
import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
|
||||
import { usePermissionStoreHook } from "/@/store/modules/permission";
|
||||
import { Router, RouteMeta, createRouter, RouteRecordName } from "vue-router";
|
||||
import {
|
||||
initRouter,
|
||||
getHistoryMode,
|
||||
getParentPaths,
|
||||
findRouteByPath,
|
||||
handleAliveRoute
|
||||
} from "./utils";
|
||||
|
||||
// 创建路由实例
|
||||
export const router: Router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: filterTree(ascending(constantRoutes)).concat(...remainingRouter),
|
||||
history: getHistoryMode(),
|
||||
routes: constantRoutes.concat(...remainingRouter),
|
||||
strict: true,
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
return new Promise(resolve => {
|
||||
if (savedPosition) {
|
||||
@@ -139,53 +37,10 @@ export const router: Router = createRouter({
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化路由
|
||||
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"];
|
||||
const whiteList = ["/login"];
|
||||
|
||||
router.beforeEach((to, _from, next) => {
|
||||
router.beforeEach((to: toRouteType, _from, next) => {
|
||||
if (to.meta?.keepAlive) {
|
||||
const newMatched = to.matched;
|
||||
handleAliveRoute(newMatched, "add");
|
||||
@@ -197,10 +52,15 @@ router.beforeEach((to, _from, next) => {
|
||||
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 (!externalLink)
|
||||
to.matched.some(item => {
|
||||
item.meta.title
|
||||
? (document.title = transformI18n(
|
||||
item.meta.title as string,
|
||||
item.meta?.i18n as boolean
|
||||
))
|
||||
: "";
|
||||
});
|
||||
if (name) {
|
||||
if (_from?.name) {
|
||||
// 如果路由包含http 则是超链接 反之是普通路由
|
||||
@@ -212,23 +72,74 @@ router.beforeEach((to, _from, next) => {
|
||||
}
|
||||
} else {
|
||||
// 刷新
|
||||
if (usePermissionStoreHook().wholeRoutes.length === 0)
|
||||
if (usePermissionStoreHook().wholeMenus.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);
|
||||
if (!useMultiTagsStoreHook().getMultiTagsCache) {
|
||||
const handTag = (
|
||||
path: string,
|
||||
parentPath: string,
|
||||
name: RouteRecordName,
|
||||
meta: RouteMeta
|
||||
): void => {
|
||||
useMultiTagsStoreHook().handleTags("push", {
|
||||
path,
|
||||
parentPath,
|
||||
name,
|
||||
meta
|
||||
});
|
||||
};
|
||||
// 未开启标签页缓存,刷新页面重定向到顶级路由(参考标签页操作例子,只针对静态路由)
|
||||
if (to.meta?.realPath) {
|
||||
const routes = router.options.routes;
|
||||
const { refreshRedirect } = to.meta;
|
||||
const { name, meta } = findRouteByPath(refreshRedirect, routes);
|
||||
handTag(
|
||||
refreshRedirect,
|
||||
getParentPaths(refreshRedirect, routes)[1],
|
||||
name,
|
||||
meta
|
||||
);
|
||||
return router.push(refreshRedirect);
|
||||
} else {
|
||||
const { path } = to;
|
||||
const index = findIndex(remainingRouter, v => {
|
||||
return v.path == path;
|
||||
});
|
||||
const routes =
|
||||
index === -1
|
||||
? router.options.routes[0].children
|
||||
: router.options.routes;
|
||||
const route = findRouteByPath(path, routes);
|
||||
const routePartent = getParentPaths(path, routes);
|
||||
// 未开启标签页缓存,刷新页面重定向到顶级路由(参考标签页操作例子,只针对动态路由)
|
||||
if (
|
||||
path !== routes[0].path &&
|
||||
route?.meta?.rank !== 0 &&
|
||||
routePartent.length === 0
|
||||
) {
|
||||
const { name, meta } = findRouteByPath(
|
||||
route?.meta?.refreshRedirect,
|
||||
routes
|
||||
);
|
||||
handTag(
|
||||
route.meta?.refreshRedirect,
|
||||
getParentPaths(route.meta?.refreshRedirect, routes)[0],
|
||||
name,
|
||||
meta
|
||||
);
|
||||
return router.push(route.meta?.refreshRedirect);
|
||||
} else {
|
||||
handTag(
|
||||
route.path,
|
||||
routePartent[routePartent.length - 1],
|
||||
route.name,
|
||||
route.meta
|
||||
);
|
||||
return router.push(path);
|
||||
}
|
||||
});
|
||||
});
|
||||
storageLocal.setItem("responsive-routesInStorage", newLocalRoutes);
|
||||
}
|
||||
}
|
||||
router.push(to.fullPath);
|
||||
});
|
||||
next();
|
||||
}
|
||||
|
||||
@@ -4,10 +4,11 @@ const componentsRouter = {
|
||||
path: "/components",
|
||||
name: "components",
|
||||
component: Layout,
|
||||
redirect: "/components/split-pane",
|
||||
redirect: "/components/video",
|
||||
meta: {
|
||||
icon: "el-icon-menu",
|
||||
icon: "Menu",
|
||||
title: "message.hscomponents",
|
||||
i18n: true,
|
||||
showLink: true,
|
||||
rank: 4
|
||||
},
|
||||
@@ -18,7 +19,8 @@ const componentsRouter = {
|
||||
component: () => import("/@/views/components/video/index.vue"),
|
||||
meta: {
|
||||
title: "message.hsvideo",
|
||||
showLink: true
|
||||
showLink: true,
|
||||
i18n: true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -29,6 +31,7 @@ const componentsRouter = {
|
||||
title: "message.hsmap",
|
||||
showLink: true,
|
||||
keepAlive: true,
|
||||
i18n: true,
|
||||
transition: {
|
||||
name: "fade"
|
||||
}
|
||||
@@ -41,6 +44,7 @@ const componentsRouter = {
|
||||
meta: {
|
||||
title: "message.hsdraggable",
|
||||
showLink: true,
|
||||
i18n: true,
|
||||
transition: {
|
||||
enterTransition: "animate__zoomIn",
|
||||
leaveTransition: "animate__zoomOut"
|
||||
@@ -55,6 +59,7 @@ const componentsRouter = {
|
||||
meta: {
|
||||
title: "message.hssplitPane",
|
||||
showLink: true,
|
||||
i18n: true,
|
||||
extraIcon: {
|
||||
svg: true,
|
||||
name: "team-iconxinpinrenqiwang"
|
||||
@@ -67,6 +72,7 @@ const componentsRouter = {
|
||||
component: () => import("/@/views/components/button/index.vue"),
|
||||
meta: {
|
||||
title: "message.hsbutton",
|
||||
i18n: true,
|
||||
showLink: true
|
||||
}
|
||||
},
|
||||
@@ -76,6 +82,7 @@ const componentsRouter = {
|
||||
component: () => import("/@/views/components/cropping/index.vue"),
|
||||
meta: {
|
||||
title: "message.hscropping",
|
||||
i18n: true,
|
||||
showLink: true
|
||||
}
|
||||
},
|
||||
@@ -85,6 +92,7 @@ const componentsRouter = {
|
||||
component: () => import("/@/views/components/count-to/index.vue"),
|
||||
meta: {
|
||||
title: "message.hscountTo",
|
||||
i18n: true,
|
||||
showLink: true
|
||||
}
|
||||
},
|
||||
@@ -94,6 +102,7 @@ const componentsRouter = {
|
||||
component: () => import("/@/views/components/selector/index.vue"),
|
||||
meta: {
|
||||
title: "message.hsselector",
|
||||
i18n: true,
|
||||
showLink: true
|
||||
}
|
||||
},
|
||||
@@ -103,6 +112,7 @@ const componentsRouter = {
|
||||
component: () => import("/@/views/components/seamless-scroll/index.vue"),
|
||||
meta: {
|
||||
title: "message.hsseamless",
|
||||
i18n: true,
|
||||
showLink: true
|
||||
}
|
||||
},
|
||||
@@ -112,6 +122,7 @@ const componentsRouter = {
|
||||
component: () => import("/@/views/components/contextmenu/index.vue"),
|
||||
meta: {
|
||||
title: "message.hscontextmenu",
|
||||
i18n: true,
|
||||
showLink: true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,9 @@ const editorRouter = {
|
||||
component: Layout,
|
||||
redirect: "/editor/index",
|
||||
meta: {
|
||||
icon: "el-icon-edit-outline",
|
||||
icon: "Edit",
|
||||
title: "message.hseditor",
|
||||
i18n: true,
|
||||
showLink: true,
|
||||
rank: 2
|
||||
},
|
||||
@@ -19,6 +20,7 @@ const editorRouter = {
|
||||
meta: {
|
||||
title: "message.hseditor",
|
||||
showLink: true,
|
||||
i18n: true,
|
||||
keepAlive: true,
|
||||
extraIcon: {
|
||||
svg: true,
|
||||
|
||||
@@ -6,9 +6,10 @@ const errorRouter = {
|
||||
component: Layout,
|
||||
redirect: "/error/401",
|
||||
meta: {
|
||||
icon: "el-icon-position",
|
||||
icon: "Position",
|
||||
title: "message.hserror",
|
||||
showLink: true,
|
||||
i18n: true,
|
||||
rank: 7
|
||||
},
|
||||
children: [
|
||||
@@ -18,6 +19,7 @@ const errorRouter = {
|
||||
component: () => import("/@/views/error/401.vue"),
|
||||
meta: {
|
||||
title: "message.hsfourZeroOne",
|
||||
i18n: true,
|
||||
showLink: true
|
||||
}
|
||||
},
|
||||
@@ -27,6 +29,7 @@ const errorRouter = {
|
||||
component: () => import("/@/views/error/404.vue"),
|
||||
meta: {
|
||||
title: "message.hsfourZeroFour",
|
||||
i18n: true,
|
||||
showLink: true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,18 +5,19 @@ const externalLink = {
|
||||
name: "external",
|
||||
component: Layout,
|
||||
meta: {
|
||||
icon: "el-icon-link",
|
||||
icon: "Link",
|
||||
title: "message.externalLink",
|
||||
showLink: true,
|
||||
i18n: true,
|
||||
rank: 190
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "https://github.com/xiaoxian521/vue-pure-admin",
|
||||
meta: {
|
||||
icon: "el-icon-link",
|
||||
title: "message.externalLink",
|
||||
showLink: true,
|
||||
i18n: true,
|
||||
rank: 191
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,10 @@ const flowChartRouter = {
|
||||
component: Layout,
|
||||
redirect: "/flowChart/index",
|
||||
meta: {
|
||||
icon: "el-icon-set-up",
|
||||
icon: "SetUp",
|
||||
title: "message.hsflowChart",
|
||||
showLink: true,
|
||||
i18n: true,
|
||||
rank: 1
|
||||
},
|
||||
children: [
|
||||
@@ -18,6 +19,7 @@ const flowChartRouter = {
|
||||
component: () => import("/@/views/flow-chart/index.vue"),
|
||||
meta: {
|
||||
title: "message.hsflowChart",
|
||||
i18n: true,
|
||||
showLink: true
|
||||
}
|
||||
}
|
||||
|
||||