Compare commits

18 Commits

Author SHA1 Message Date
Coder慌
4836a770c4 !139 feat(es):增加ES实例中对HTTP/HTTPS协议的支持,默认使用HTTP协议,使用https时默认证书免校验
Merge pull request !139 from davidathena/dev
2025-10-28 11:25:47 +00:00
fudawei
e6c89fad1b feat(es):增加ES实例中对HTTPS协议的支持,默认证书免校验 2025-10-23 15:29:27 +08:00
meilin.huang
dba19b1e66 fix: editor提示被遮挡问题修复等 2025-10-18 11:21:33 +08:00
davidathena
4e30bdb7cc !138 fix: 后端数据连接断开后报空指针异常后程序中断问题
* fix(connection):fix the bug for nil error in connection when connection reset
* fix(sqleditor): fix the spell error in sql editor
2025-10-18 03:15:25 +00:00
meilin.huang
4ac57cd140 refactor: 标签不可移动,资源选择优化等 2025-10-07 15:41:19 +08:00
meilin.huang
c4d52ce47a feat: 资源操作新增右键菜单操作等 2025-09-17 21:23:12 +08:00
meilin.huang
54d0688571 fix: 名称调整等 2025-09-14 20:53:47 +08:00
meilin.huang
66d5fd6ca4 feat: 容器操作优化等 2025-09-06 21:32:48 +08:00
时光似水戏流年
25195b6360 !137 现在执行sql只执行当前光标所在的sql(分号分割的),如果要执行全部sql需要先全选,再执行
* 现在执行sql只执行当前sql(分号分割的),如果要执行全部sql需要先全选,再执行
2025-09-02 11:12:04 +00:00
meilin.huang
e02ecf053f feat: 资源操作统一管理&容器操作 2025-08-31 21:46:10 +08:00
meilin.huang
c86f2ad412 refactor: 样式优化 2025-08-19 19:44:14 +08:00
meilin.huang
82fd97e06a fix: file文件缺失 2025-08-08 12:55:10 +08:00
meilin.huang
614a144f60 refactor: 样式优化 2025-08-04 21:02:27 +08:00
meilin.huang
7d344c71e1 refactor: 消息模块调整 & 样式优化 2025-08-02 22:08:56 +08:00
meilin.huang
6ad6c69660 refactor: 消息模块重构,infra包路径简写等 2025-07-27 21:02:48 +08:00
meilin.huang
e96379b6c0 fix: vite配置调整 2025-07-07 12:05:55 +08:00
meilin.huang
f7480f3bac refactor: cast包替换 2025-06-27 12:17:45 +08:00
meilin.huang
54d3a5b368 fix: sql执行记录根据关键词搜索问题修复等 2025-06-22 10:52:06 +08:00
439 changed files with 13089 additions and 10091 deletions

View File

@@ -51,42 +51,36 @@ http://go.mayfly.run
#### 首页
![首页](https://foruda.gitee.com/images/1714378104294194769/149fd257_1240250.png "屏幕截图")
![首页](https://foruda.gitee.com/images/1757163736351080323/afb6b330_1240250.png "屏幕截图")
#### 机器操作
#### 资源管理
##### 状态查看
![资源树](https://foruda.gitee.com/images/1757163958991119284/83eb2171_1240250.png "屏幕截图")
![机器状态查看](https://foruda.gitee.com/images/1714378556642584686/93c46ec0_1240250.png "屏幕截图")
#### 资源操作
##### ssh 终端
![终端操作](https://foruda.gitee.com/images/1757164093410206293/1c7dda30_1240250.png)
![终端操作](https://foruda.gitee.com/images/1714378353790214943/2864ba66_1240250.png "屏幕截图")
##### 文件操作
![文件操作](https://foruda.gitee.com/images/1714378417206086701/74a188d8_1240250.png "屏幕截图")
![文件操作](https://foruda.gitee.com/images/1757164149388450531/0542398c_1240250.png)
![文件查看](https://foruda.gitee.com/images/1714378482611638688/7753faf6_1240250.png "屏幕截图")
#### 数据库操作
##### sql 编辑器
![sql编辑器](https://foruda.gitee.com/images/1757164386318836686/c3b17a52_1240250.png)
![sql编辑器](https://foruda.gitee.com/images/1714378747473077515/3c9387c0_1240250.png "屏幕截图")
##### 在线增删改查数据
![选表查数据](https://foruda.gitee.com/images/1757164281011401749/5109485f_1240250.png)
![选表查数据](https://foruda.gitee.com/images/1714378625059063750/3951e5a8_1240250.png "屏幕截图")
#### Redis 操作
![redis操作](https://foruda.gitee.com/images/1757164442298752845/4af1b296_1240250.png)
![redis操作](https://foruda.gitee.com/images/1714378855845451114/4c3f0097_1240250.png "屏幕截图")
#### Mongo 操作
![mongo操作](https://foruda.gitee.com/images/1714378916425714642/77fc0ed9_1240250.png "屏幕截图")
![es操作](https://foruda.gitee.com/images/1757164553845346963/b5b70381_1240250.png)
![容器操作](https://foruda.gitee.com/images/1757164625186816754/2b195e25_1240250.png)
#### 工单流程审批
![流程审批](https://foruda.gitee.com/images/1714379057627690037/ad136862_1240250.png "屏幕截图")

View File

@@ -46,40 +46,35 @@ account/passwordtest/test123.
![首页](https://foruda.gitee.com/images/1714378104294194769/149fd257_1240250.png "屏幕截图")
#### Machine Operation
#### Resource Manage
##### Status
![资源树](https://foruda.gitee.com/images/1757163958991119284/83eb2171_1240250.png "屏幕截图")
![机器状态查看](https://foruda.gitee.com/images/1714378556642584686/93c46ec0_1240250.png "屏幕截图")
#### Resource Operation
##### SSH Terminal
![终端操作](https://foruda.gitee.com/images/1757164093410206293/1c7dda30_1240250.png)
![终端操作](https://foruda.gitee.com/images/1714378353790214943/2864ba66_1240250.png "屏幕截图")
##### File Operation
![文件操作](https://foruda.gitee.com/images/1714378417206086701/74a188d8_1240250.png "屏幕截图")
![文件操作](https://foruda.gitee.com/images/1757164149388450531/0542398c_1240250.png)
![文件查看](https://foruda.gitee.com/images/1714378482611638688/7753faf6_1240250.png "屏幕截图")
#### Database Operation
##### SQL Editor
![sql编辑器](https://foruda.gitee.com/images/1757164386318836686/c3b17a52_1240250.png)
![sql编辑器](https://foruda.gitee.com/images/1714378747473077515/3c9387c0_1240250.png "屏幕截图")
##### Add, delete, update and check data online
![选表查数据](https://foruda.gitee.com/images/1757164281011401749/5109485f_1240250.png)
![选表查数据](https://foruda.gitee.com/images/1714378625059063750/3951e5a8_1240250.png "屏幕截图")
#### Redis Operation
![redis操作](https://foruda.gitee.com/images/1757164442298752845/4af1b296_1240250.png)
![redis操作](https://foruda.gitee.com/images/1714378855845451114/4c3f0097_1240250.png "屏幕截图")
#### Mongo Operation
![mongo操作](https://foruda.gitee.com/images/1714378916425714642/77fc0ed9_1240250.png "屏幕截图")
![es操作](https://foruda.gitee.com/images/1757164553845346963/b5b70381_1240250.png)
![容器操作](https://foruda.gitee.com/images/1757164625186816754/2b195e25_1240250.png)
#### Work order process approval
![流程审批](https://foruda.gitee.com/images/1714379057627690037/ad136862_1240250.png "屏幕截图")

View File

@@ -7,4 +7,8 @@ VITE_OPEN = false
# public path 配置线上环境路径(打包)
VITE_PUBLIC_PATH = ''
VITE_EDITOR=idea
VITE_EDITOR=idea
# 路由模式
# Optional: hash | history
VITE_ROUTER_MODE = hash

View File

@@ -4,8 +4,4 @@ ENV = 'development'
VITE_OPEN = true
# 本地环境接口地址
VITE_API_URL = '/api'
# 路由模式
# Optional: hash | history
VITE_ROUTER_MODE = hash
VITE_API_URL = '/api'

View File

@@ -3,7 +3,3 @@ ENV = 'production'
# 线上环境接口地址
VITE_API_URL = '/api'
# 路由模式
# Optional: hash | history
VITE_ROUTER_MODE = hash

View File

@@ -10,63 +10,61 @@
"lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"@logicflow/core": "^2.0.16",
"@logicflow/extension": "^2.0.21",
"@vueuse/core": "^13.3.0",
"@element-plus/icons-vue": "^2.3.2",
"@logicflow/core": "^2.1.3",
"@logicflow/extension": "^2.1.5",
"@vueuse/core": "^13.9.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-search": "^0.15.0",
"@xterm/addon-web-links": "^0.11.0",
"@xterm/xterm": "^5.5.0",
"asciinema-player": "^3.10.0",
"asciinema-player": "^3.11.1",
"axios": "^1.6.2",
"clipboard": "^2.0.11",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.13",
"echarts": "^5.6.0",
"element-plus": "^2.10.2",
"js-base64": "^3.7.7",
"jsencrypt": "^3.3.2",
"mitt": "^3.0.1",
"monaco-editor": "^0.52.2",
"monaco-sql-languages": "^0.15.0",
"monaco-themes": "^0.4.5",
"dayjs": "^1.11.18",
"echarts": "^6.0.0",
"element-plus": "^2.11.4",
"js-base64": "^3.7.8",
"jsencrypt": "^3.5.4",
"monaco-editor": "^0.54.0",
"monaco-sql-languages": "^0.15.1",
"monaco-themes": "^0.4.7",
"nprogress": "^0.2.0",
"pinia": "^3.0.3",
"qrcode.vue": "^3.6.0",
"screenfull": "^6.0.2",
"sortablejs": "^1.15.6",
"sql-formatter": "^15.6.1",
"sql-formatter": "^15.6.8",
"trzsz": "^1.1.5",
"uuid": "^9.0.1",
"vue": "^3.5.16",
"vue-i18n": "^11.1.5",
"vue-router": "^4.5.1",
"uuid": "^13.0.0",
"vue": "^v3.5.22",
"vue-i18n": "^11.1.12",
"vue-router": "^4.6.3",
"vuedraggable": "^4.1.0"
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.9",
"@tailwindcss/vite": "^4.1.14",
"@types/crypto-js": "^4.2.2",
"@types/node": "^18.14.0",
"@types/node": "^22.13.14",
"@types/nprogress": "^0.2.0",
"@types/sortablejs": "^1.15.8",
"@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4",
"@vitejs/plugin-vue": "^5.2.4",
"@vue/compiler-sfc": "^3.5.16",
"@typescript-eslint/eslint-plugin": "^8.35.0",
"@typescript-eslint/parser": "^8.35.0",
"@vitejs/plugin-vue": "^6.0.1",
"@vue/compiler-sfc": "^3.5.18",
"autoprefixer": "^10.4.21",
"code-inspector-plugin": "^0.20.9",
"dotenv": "^16.3.1",
"eslint": "^9.27.0",
"eslint-plugin-vue": "^10.2.0",
"postcss": "^8.5.4",
"prettier": "^3.5.3",
"sass": "^1.89.2",
"tailwindcss": "^4.1.9",
"typescript": "^5.8.2",
"code-inspector-plugin": "^1.0.4",
"eslint": "^9.29.0",
"eslint-plugin-vue": "^10.5.0",
"postcss": "^8.5.6",
"prettier": "^3.6.1",
"sass": "^1.93.2",
"tailwindcss": "^4.1.14",
"typescript": "^5.9.2",
"vite": "npm:rolldown-vite@latest",
"vite-plugin-progress": "0.0.7",
"vue-eslint-parser": "^10.1.3"
"vue-eslint-parser": "^10.2.0"
},
"browserslist": [
"> 1%",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -1,39 +1,34 @@
<template>
<el-config-provider :size="getGlobalComponentSize" :locale="getGlobalI18n">
<div class="h-full">
<el-watermark
:zIndex="10000000"
:width="210"
v-if="themeConfig.isWatermark"
:font="{ color: 'rgba(180, 180, 180, 0.3)' }"
:content="themeConfig.watermarkText"
class="!h-full"
>
<router-view v-show="themeConfig.lockScreenTime !== 0" />
</el-watermark>
<router-view v-if="!themeConfig.isWatermark" v-show="themeConfig.lockScreenTime !== 0" />
<el-watermark
:zIndex="100000"
:width="210"
v-if="themeConfig.isWatermark"
:font="{ color: 'rgba(180, 180, 180, 0.3)' }"
:content="themeConfig.watermarkText"
class="!h-full"
>
<router-view />
</el-watermark>
<router-view v-if="!themeConfig.isWatermark" />
<LockScreen v-if="themeConfig.isLockScreen" />
<Setings ref="setingsRef" v-show="themeConfig.lockScreenTime !== 0" />
</div>
<Setings />
</el-config-provider>
</template>
<script setup lang="ts" name="app">
import { ref, onMounted, onUnmounted, nextTick, watch, computed } from 'vue';
import { onMounted, nextTick, watch, computed, defineAsyncComponent } from 'vue';
import { useRoute } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
import LockScreen from '@/layout/lockScreen/index.vue';
import Setings from '@/layout/navBars/breadcrumb/setings.vue';
import mittBus from '@/common/utils/mitt';
import { useIntervalFn } from '@vueuse/core';
import { useI18n } from 'vue-i18n';
import EnumValue from './common/Enum';
import { I18nEnum } from './common/commonEnum';
import { saveThemeConfig } from './common/utils/storage';
const setingsRef = ref();
const Setings = defineAsyncComponent(() => import('@/layout/navBars/breadcrumb/setings.vue'));
const route = useRoute();
const themeConfigStores = useThemeConfig();
@@ -42,19 +37,9 @@ const { themeConfig } = storeToRefs(themeConfigStores);
// 定义变量内容
const { locale, t } = useI18n();
// 布局配置弹窗打开
const openSetingsDrawer = () => {
setingsRef.value.openDrawer();
};
// 页面加载时
onMounted(() => {
nextTick(() => {
// 监听布局配置弹窗点击打开
mittBus.on('openSetingsDrawer', () => {
openSetingsDrawer();
});
// 初始化系统主题
themeConfigStores.initThemeConfig();
});
@@ -120,11 +105,6 @@ const refreshWatermarkTime = () => {
themeConfigStores.setWatermarkNowTime();
};
// 页面销毁时,关闭监听布局配置
onUnmounted(() => {
mittBus.off('openSetingsDrawer', () => {});
});
// 监听路由的变化,设置网站标题
watch(
() => route.path,

View File

@@ -0,0 +1 @@
<svg t="1756305127175" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="22356" width="48" height="48"><path d="M959.718832 123.963683C872.444401 50.185297 704.593576 0.299912 511.850044 0.299912S151.255687 50.185297 63.981255 123.963683C23.193205 158.453578 0 198.04198 0 240.22962v543.840672c0 132.461193 229.132871 239.929708 511.850044 239.929708s511.850044-107.468515 511.850044-239.929708v-543.840672c0-42.18764-23.193205-81.776042-63.981256-116.265937zM87.774285 189.64444c19.794201-21.893586 50.685151-43.087377 89.373816-61.182075 42.287611-19.794201 92.073025-35.489603 147.956653-46.586352C384.087474 70.17944 446.869081 64.281168 511.850044 64.281168s127.76257 5.898272 186.745289 17.594845c55.883628 11.096749 105.669042 26.792151 147.956654 46.586352 38.688665 18.094699 69.579615 39.28849 89.373816 61.182075 15.795372 17.494875 23.793029 34.489896 23.793029 50.48521 0 16.095285-7.997657 33.090306-23.793029 50.485209-19.794201 21.893586-50.685151 43.087377-89.373816 61.182075-42.287611 19.894172-92.073025 35.489603-147.956654 46.586352-58.98272 11.696573-121.864298 17.594845-186.745289 17.594845s-127.76257-5.898272-186.74529-17.594845c-55.883628-11.096749-105.669042-26.792151-147.956653-46.586352-38.688665-18.094699-69.579615-39.28849-89.373816-61.182075C71.978912 273.319926 63.981255 256.324905 63.981255 240.22962s7.997657-33.090306 23.79303-50.58518zM63.981255 356.495558c87.274431 73.778385 255.125256 123.66377 447.868789 123.66377s360.594357-49.885385 447.868788-123.66377v155.254515c0 16.095285-7.997657 33.090306-23.793029 50.48521-19.794201 21.893586-50.685151 43.087377-89.373816 61.182075-42.287611 19.794201-92.073025 35.489603-147.956654 46.586352-58.98272 11.696573-121.864298 17.594845-186.745289 17.594845s-127.76257-5.898272-186.74529-17.594845c-55.883628-11.096749-105.669042-26.792151-147.956653-46.586352-38.688665-18.094699-69.579615-39.28849-89.373816-61.182075C71.978912 544.740408 63.981255 527.745387 63.981255 511.750073V356.495558z m895.737577 427.574734c0 16.095285-7.997657 33.090306-23.793029 50.485209-19.794201 21.893586-50.685151 43.087377-89.373816 61.182076-42.287611 19.894172-92.073025 35.489603-147.956654 46.586352-58.98272 11.696573-121.864298 17.594845-186.745289 17.594845s-127.76257-5.898272-186.74529-17.594845c-55.883628-11.096749-105.669042-26.792151-147.956653-46.586352-38.688665-18.094699-69.579615-39.28849-89.373816-61.182076C71.978912 817.160597 63.981255 800.165576 63.981255 784.070292V627.91604c87.274431 73.778385 255.125256 123.66377 447.868789 123.663771s360.594357-49.885385 447.868788-123.663771v156.154252z" p-id="22357"></path><path d="M167.950796 519.847701m-39.988285 0a39.988285 39.988285 0 1 0 79.976569 0 39.988285 39.988285 0 1 0-79.976569 0Z" p-id="22358"></path><path d="M167.950796 791.768037m-39.988285 0a39.988285 39.988285 0 1 0 79.976569 0 39.988285 39.988285 0 1 0-79.976569 0Z" p-id="22359"></path></svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -0,0 +1 @@
<svg t="1756305474315" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="24277" width="48" height="48"><path d="M960 0H0v1024h1024V0.146286h-64V0z m-640 960.146286h-256v-192h256v192z m0-256.146286h-256V512.146286h256v191.853714z m320 256.146286h-256v-192h256v192z m0-256.146286h-256V512.146286h256v191.853714z m320 256.146286h-256v-192h256v192z m0-256.146286h-256V512.146286h256v191.853714z m0-256h-256V256.146286H640v192h-256V256.146286h-64v192h-256V256.146286h896v191.853714z" p-id="24278"></path></svg>

After

Width:  |  Height:  |  Size: 547 B

View File

@@ -0,0 +1 @@
<svg t="1756107672203" class="icon" viewBox="0 0 1472 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5144" width="48" height="48"><path d="M1449.66628 358.737a233.848 233.848 0 0 0-166.348-35.445 268.717 268.717 0 0 0-108.127-152.273l-31.158-20.026-22.265 30.455a258.736 258.736 0 0 0-22.01 265.39 177.353 177.353 0 0 1-74.28 21.241h-24.953V309.536H830.08228V0H624.44928v154.768H287.27328v154.704H118.68528V468.08H8.44728L3.26528 504.42a493.032 493.032 0 0 0 95.97 353.3c90.149 110.11 234.232 165.964 428.284 165.964a749.848 749.848 0 0 0 585.42-255.025 804.871 804.871 0 0 0 139.86-226.874c187.718-3.391 213.246-134.359 214.27-139.99l4.863-27.447-22.01-15.61z m-766.291-49.84v-92.068h87.717v92.068h-87.717z m-337.176 154.64v-92.068h87.59v92.068h-87.59z m168.588 0v-92.068h87.589v92.068h-87.589z m168.588 0v-92.068h87.717v92.068h-87.717z m170.38-92.068h87.524v92.068h-87.525v-92.068zM683.37428 62.125h87.717v92.003h-87.717V62.125zM514.78728 216.829h87.589v92.068h-87.525v-92.068z m-168.588 0h87.59v92.068h-87.59v-92.068zM177.61228 371.47h87.525v92.068H177.61228v-92.068zM527.19928 938.4a609.348 609.348 0 0 1-235-40.564 399.493 399.493 0 0 0 151.058-66.092 44.018 44.018 0 0 0 7.87-57.582 39.54 39.54 0 0 0-54.575-11.9 375.18 375.18 0 0 1-215.998 62.508 262.639 262.639 0 0 1-19.194-21.433 392.455 392.455 0 0 1-79.591-249.523h943.9a250.035 250.035 0 0 0 155.216-62.06l4.99-4.671a682.157 682.157 0 0 1-658.42 451.636z m699.432-482.412l-25.144-1.215-15.163-21.178a186.566 186.566 0 0 1-21.626-161.358 145.619 145.619 0 0 1 42.483 100.769l-1.663 60.525 54.83-18.682a205.505 205.505 0 0 1 111.07-1.664 170.123 170.123 0 0 1-144.787 42.803zM544.41028 629.31a69.738 69.738 0 1 1-66.412 69.674 68.139 68.139 0 0 1 66.412-69.674z m0 85.413a15.74 15.74 0 1 0-14.971-15.675 15.291 15.291 0 0 0 14.97 15.675z m0 0" p-id="5145"></path></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1 @@
<svg t="1756286353957" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="19008" width="48" height="48"><path d="M853.333333 554.666667a128 128 0 0 1 128 128v170.666666a128 128 0 0 1-128 128H170.666667a128 128 0 0 1-128-128v-170.666666a128 128 0 0 1 128-128h682.666666z m0 85.333333H170.666667a42.666667 42.666667 0 0 0-42.368 37.674667L128 682.666667v170.666666a42.666667 42.666667 0 0 0 37.674667 42.368L170.666667 896h682.666666a42.666667 42.666667 0 0 0 42.368-37.674667L896 853.333333v-170.666666a42.666667 42.666667 0 0 0-42.666667-42.666667zM256 725.333333a42.666667 42.666667 0 1 1 0 85.333334 42.666667 42.666667 0 0 1 0-85.333334zM853.333333 42.666667a128 128 0 0 1 128 128v170.666666a128 128 0 0 1-128 128H170.666667a128 128 0 0 1-128-128V170.666667a128 128 0 0 1 128-128h682.666666z m0 85.333333H170.666667a42.666667 42.666667 0 0 0-42.368 37.674667L128 170.666667v170.666666a42.666667 42.666667 0 0 0 37.674667 42.368L170.666667 384h682.666666a42.666667 42.666667 0 0 0 42.368-37.674667L896 341.333333V170.666667a42.666667 42.666667 0 0 0-42.666667-42.666667zM256 213.333333a42.666667 42.666667 0 1 1 0 85.333334 42.666667 42.666667 0 0 1 0-85.333334z" p-id="19009"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M475.19999999 84.5568c202.7008 0 362.6496 71.0912 373.50400001 163.6608l0.40959999 4.5568h0.5632v232.2432H795.19999999V364.288c-63.1552 48.3328-175.5648 80.5888-307.5584 82.5088l-12.4416 0.0768c-133.1968 0-247.7312-30.8224-313.93279999-78.08L155.2 364.288v136.7552c0 63.5136 128.6144 126.208 319.99999999 126.208 63.1808 0 119.5264-6.8352 166.656-18.2784-4.9408 23.552-6.4 43.5968-4.4032 60.2112-48.7936 10.6752-103.7056 16.6144-162.2528 16.6144-133.1968 0-247.7312-30.7968-313.93279999-78.08l-6.0672-4.5056v125.824c0 63.5136 128.6144 126.2336 319.99999999 126.2336 74.3168 0 139.1616-9.4464 190.6688-24.7296l15.18080001 55.5008a631.04 631.04 0 0 1-89.6256 19.584 803.8656 803.8656 0 0 1-116.22400001 8.192c-206.7456 0-369.3312-73.984-374.3488-169.1392l-0.128-4.5568V252.7744h0.56320001C107.32799999 158.0032 269.1712 84.5824 475.19999999 84.5824z m335.18080001 637.696c12.3648 0 22.4 10.0608 22.39999999 22.4256l-0.0768 74.112a22.3744 22.3744 0 0 1 8.96-9.3184c15.4112-8.704 27.0336-24.6528 33.408-46.592a22.4 22.4 0 1 1 43.008 12.4928c-9.6 33.024-28.416 58.4704-54.39999999 73.1136a22.4 22.4 0 0 1-30.92480001-9.216v40.7296a22.4 22.4 0 0 1-44.79999999 0V744.704c0-12.3648 10.0608-22.4 22.4256-22.4z m-15.6672-184.7808a22.784 22.784 0 0 1 31.51359999 0.256c9.8816 9.8816 24.6528 26.624 40.06400001 47.36 25.3184 34.048 44.2624 68.4544 53.24799999 101.9136a22.4256 22.4256 0 0 1-43.3664 11.3408c-9.8816-36.6848-35.584-76.3392-65.8432-111.488-39.7824 46.1824-69.76 97.152-69.75999999 138.5984 0 36.992 13.056 67.4048 33.89439999 81.5616l5.632 5.3248a22.4 22.4 0 0 1-30.77119999 31.7696c-33.8432-22.9376-53.5552-67.3792-53.55520001-118.656 0-39.1424 18.1248-81.8944 48.2816-125.8752a461.312 461.312 0 0 1 50.688-62.1056zM475.19999999 143.0016c-187.7504 0.0512-314.7776 60.416-319.53919999 122.7264 4.8128 62.2336 131.7888 122.5984 319.53919999 122.5984s314.7776-60.3648 319.5392-122.6496C789.92639999 203.4176 662.95039999 143.0016 475.19999999 143.0016z" ></path></svg>
<svg t="1756389060526" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="29147" width="48" height="48"><path d="M465.454545 9.402182c245.697939 0 439.575273 86.171152 452.732122 198.376727l0.496485 5.523394h0.682666v281.506909H853.333333V348.470303c-76.551758 58.585212-212.805818 97.683394-372.79806 100.010667l-15.080728 0.093091c-161.450667 0-300.280242-37.360485-380.524606-94.642425L77.575758 348.470303v165.763879c0 76.986182 155.896242 152.979394 387.878787 152.979394 76.582788 0 144.880485-8.285091 202.007273-22.155637-5.988848 28.547879-7.757576 52.844606-5.337212 72.983273-59.143758 12.939636-125.703758 20.138667-196.670061 20.138667-161.450667 0-300.280242-37.329455-380.524606-94.642424l-7.354181-5.461334v152.51394c0 76.986182 155.896242 153.010424 387.878787 153.010424 90.08097 0 168.680727-11.450182 231.113697-29.975273l18.40097 67.273697a764.89697 764.89697 0 0 1-108.637091 23.738182 974.382545 974.382545 0 0 1-140.877576 9.929697c-250.600727 0-447.674182-89.677576-453.756121-205.017212l-0.155151-5.523394V213.302303h0.682666C19.549091 98.428121 215.722667 9.433212 465.454545 9.433212z m406.279758 772.964848c14.987636 0 27.151515 12.194909 27.151515 27.182546l-0.093091 89.832727a27.120485 27.120485 0 0 1 10.860606-11.29503c18.680242-10.550303 32.768-29.882182 40.494546-56.475152a27.151515 27.151515 0 1 1 52.130909 15.142788c-11.636364 40.029091-34.443636 70.873212-65.939394 88.622546a27.151515 27.151515 0 0 1-37.484606-11.17091v49.369213a27.151515 27.151515 0 0 1-54.30303 0V809.580606c0-14.987636 12.194909-27.151515 27.182545-27.151515z m-18.990545-223.976727a27.61697 27.61697 0 0 1 38.198303 0.310303c11.977697 11.977697 29.882182 32.271515 48.562424 57.406061 30.68897 41.270303 53.651394 82.97503 64.54303 123.531636a27.182545 27.182545 0 0 1-52.565333 13.746424c-11.977697-44.466424-43.132121-92.532364-79.80994-135.136969-48.221091 55.978667-84.557576 117.76-84.557575 167.99806 0 44.838788 15.825455 81.702788 41.084121 98.862546l6.826667 6.454303a27.151515 27.151515 0 0 1-37.298425 38.508606c-41.022061-27.803152-64.915394-81.671758-64.915394-143.825455 0-47.445333 21.969455-99.265939 58.523152-152.576a559.166061 559.166061 0 0 1 61.44-75.279515zM465.454545 80.244364C237.878303 80.306424 83.905939 153.475879 78.134303 229.003636 83.968 304.407273 237.878303 377.607758 465.454545 377.607758S847.003152 304.407273 852.774788 228.941576C846.941091 153.475879 693.030788 80.244364 465.454545 80.244364z" p-id="29148"></path></svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M897.8125003 599.75c-0.37500029 8.58750029-11.73750029 18.18749971-35.06250058 30.375-47.99999971 25.01250029-296.84999971 127.35-349.79999942 154.95000029-52.9875 27.60000029-82.38750029 27.3375-124.23750029 7.3125-41.85-19.98749971-306.60000029-126.97499971-354.30000029-149.7375-23.81249971-11.40000029-35.96249971-20.99999971-36.37499942-30.07500029v90.97499971c0 9.07499971 12.52500029 18.71250029 36.37499942 30.11250058 47.7 22.79999971 312.48749971 129.75000029 354.30000029 149.7375 41.85 20.025 71.25000029 20.28750029 124.23750029-7.35000029 52.94999971-27.60000029 301.76250029-129.89999971 349.79999942-154.95000029 24.4125-12.7125 35.25000029-22.6125 35.25000029-31.57499971v-89.70000029l-0.18749971-0.07499971z" fill="" ></path><path d="M897.77500001 451.43749971c-0.37500029 8.58750029-11.73750029 18.15000029-35.02500029 30.33750058-47.99999971 25.01250029-296.84999971 127.35-349.79999942 154.94999942-52.9875 27.60000029-82.38750029 27.3375-124.23750029 7.35000029-41.85-19.98749971-306.60000029-126.97499971-354.30000029-149.77500029-23.81249971-11.3625-35.96249971-20.99999971-36.37499942-30.0375v90.97500058c0 9.07499971 12.52500029 18.675 36.37499942 30.07499942 47.7 22.79999971 312.45000029 129.75000029 354.30000029 149.7375 41.85 20.025 71.25000029 20.28750029 124.23750029-7.3125 52.94999971-27.60000029 301.76250029-129.9375 349.79999942-154.94999942 24.4125-12.75000029 35.25000029-22.65000029 35.25000029-31.6125v-89.70000029l-0.225-0.03750029z" fill="" ></path><path d="M897.77500001 297.61250029c0.45-9.15000029-11.51250029-17.17499971-35.58750029-26.02500029-46.8-17.13750029-294.11250029-115.57500029-341.47499942-132.93749971-47.3625-17.325-66.63750029-16.61249971-122.25000058 3.375C342.7375003 161.93750029 79.41249972 265.24999971 32.5750003 283.55000029c-23.43750029 9.225-34.875 17.73749971-34.50000058 26.81249942V401.37499971c0 9.07499971 12.52500029 18.675 36.37500029 30.07500029 47.7 22.79999971 312.45000029 129.78749971 354.30000029 149.77500029 41.85 19.98749971 71.25000029 20.25 124.23749942-7.35000029 52.94999971-27.60000029 301.76250029-129.9375 349.80000029-154.95000029 24.4125-12.75000029 35.25000029-22.65000029 35.25000029-31.6125V297.61250029h-0.30000058zM320.31250001 383.75l208.53749971-32.02499971-63 92.3625-145.49999942-60.33750029z m461.25-83.17500029l-123.33750029 48.75000029-13.3875 5.24999971-123.26249971-48.74999942 136.575-54 123.37499971 48.74999942z m-362.09999971-89.36249942l-20.17500029-37.20000058 62.92500029 24.60000058 59.32499942-19.42500058-16.04999971 38.43750058 60.45000029 22.64999942-77.9625 8.1-17.47500029 42.00000029-28.19999971-46.83750029-90-8.1 67.1625-24.22499942z m-155.3625 52.49999971c61.57500029 0 111.44999971 19.31249971 111.44999971 43.16249971s-49.87500029 43.2-111.44999971 43.2-111.4875-19.38750029-111.4875-43.2c0-23.85 49.91249971-43.2 111.4875-43.2z" fill="" ></path></svg>
<svg t="1756388835244" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="25729" width="48" height="48"><path d="M1023.786667 611.84c-0.426667 9.770667-13.354667 20.693333-39.893334 34.56-54.613333 28.458667-337.749333 144.896-397.994666 176.298667-60.288 31.402667-93.738667 31.104-141.354667 8.32-47.616-22.741333-348.842667-144.469333-403.114667-170.368-27.093333-12.970667-40.917333-23.893333-41.386666-34.218667v103.509333c0 10.325333 14.250667 21.290667 41.386666 34.261334 54.272 25.941333 355.541333 147.626667 403.114667 170.368 47.616 22.784 81.066667 23.082667 141.354667-8.362667 60.245333-31.402667 343.338667-147.797333 397.994666-176.298667 27.776-14.464 40.106667-25.728 40.106667-35.925333v-102.058667l-0.213333-0.085333z m0-168.746667c-0.512 9.770667-13.397333 20.650667-39.893334 34.517334-54.613333 28.458667-337.749333 144.896-397.994666 176.298666-60.288 31.402667-93.738667 31.104-141.354667 8.362667-47.616-22.741333-348.842667-144.469333-403.114667-170.410667-27.093333-12.928-40.917333-23.893333-41.386666-34.176v103.509334c0 10.325333 14.250667 21.248 41.386666 34.218666 54.272 25.941333 355.498667 147.626667 403.114667 170.368 47.616 22.784 81.066667 23.082667 141.354667-8.32 60.245333-31.402667 343.338667-147.84 397.994666-176.298666 27.776-14.506667 40.106667-25.770667 40.106667-35.968v-102.058667l-0.256-0.042667z m0-175.018666c0.469333-10.410667-13.141333-19.541333-40.533334-29.610667-53.248-19.498667-334.634667-131.498667-388.522666-151.253333-53.888-19.712-75.818667-18.901333-139.093334 3.84C392.234667 113.706667 92.629333 231.253333 39.338667 252.074667c-26.666667 10.496-39.68 20.181333-39.253334 30.506666V386.133333c0 10.325333 14.250667 21.248 41.386667 34.218667 54.272 25.941333 355.498667 147.669333 403.114667 170.410667 47.616 22.741333 81.066667 23.04 141.354666-8.362667 60.245333-31.402667 343.338667-147.84 397.994667-176.298667 27.776-14.506667 40.106667-25.770667 40.106667-35.968V268.074667h-0.341334zM366.677333 366.08l237.269334-36.437333-71.68 105.088-165.546667-68.650667z m524.8-94.634667l-140.330666 55.466667-15.232 5.973333-140.245334-55.466666 155.392-61.44 140.373334 55.466666z m-411.989333-101.674666l-22.954667-42.325334 71.594667 27.989334 67.498667-22.101334-18.261334 43.733334 68.778667 25.770666-88.704 9.216-19.882667 47.786667-32.085333-53.290667-102.4-9.216 76.416-27.562666z m-176.768 59.733333c70.058667 0 126.805333 21.973333 126.805333 49.109333s-56.746667 49.152-126.805333 49.152-126.848-22.058667-126.848-49.152c0-27.136 56.789333-49.152 126.848-49.152z" p-id="25730"></path></svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.4 KiB

View File

@@ -1 +1,9 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1621859009605" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9709" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M820.203922 812.172549H684.67451v-45.176471h112.439215V279.090196H633.47451l-85.333334 277.082353c-3.011765 10.039216-12.047059 16.062745-22.086274 16.062745-10.039216 0-19.07451-7.027451-21.082353-17.066667l-71.278431-280.094117h-180.705883V762.980392h120.470589v45.176471H229.898039c-12.047059 0-22.086275-10.039216-22.086274-22.086275V252.988235c0-12.047059 10.039216-22.086275 22.086274-22.086274H451.764706c10.039216 0 19.07451 7.027451 22.086274 17.066666l55.215687 218.854902L595.32549 250.980392c3.011765-9.035294 12.047059-16.062745 21.082353-16.062745h202.792157c12.047059 0 22.086275 10.039216 22.086275 22.086275v533.082353c1.003922 12.047059-9.035294 22.086275-21.082353 22.086274z m0 0" fill="#e25813" p-id="9710"></path><path d="M731.858824 425.662745c4.015686-12.047059-2.007843-25.098039-14.054902-29.113725-12.047059-4.015686-25.098039 2.007843-29.113726 14.054902L563.2 766.996078h-73.286275L371.45098 410.603922c-4.015686-12.047059-17.066667-18.070588-28.109804-14.054902-12.047059 4.015686-18.070588 17.066667-14.054901 28.109804l123.482352 371.45098c3.011765 9.035294 12.047059 15.058824 21.082353 15.058823h72.282353l-53.207843 160.627451 46.180392 2.007844 192.752942-548.141177z" fill="#2c2c2c" p-id="9711"></path></svg>
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1621859009605" class="icon" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="9709" xmlns:xlink="http://www.w3.org/1999/xlink"
width="200" height="200">
<defs><style type="text/css"></style></defs>
<path d="M820.203922 812.172549H684.67451v-45.176471h112.439215V279.090196H633.47451l-85.333334 277.082353c-3.011765 10.039216-12.047059 16.062745-22.086274 16.062745-10.039216 0-19.07451-7.027451-21.082353-17.066667l-71.278431-280.094117h-180.705883V762.980392h120.470589v45.176471H229.898039c-12.047059 0-22.086275-10.039216-22.086274-22.086275V252.988235c0-12.047059 10.039216-22.086275 22.086274-22.086274H451.764706c10.039216 0 19.07451 7.027451 22.086274 17.066666l55.215687 218.854902L595.32549 250.980392c3.011765-9.035294 12.047059-16.062745 21.082353-16.062745h202.792157c12.047059 0 22.086275 10.039216 22.086275 22.086275v533.082353c1.003922 12.047059-9.035294 22.086275-21.082353 22.086274z m0 0" fill="#e25813" p-id="9710" stroke-width="30" stroke="#e25813"></path>
<path d="M731.858824 425.662745c4.015686-12.047059-2.007843-25.098039-14.054902-29.113725-12.047059-4.015686-25.098039 2.007843-29.113726 14.054902L563.2 766.996078h-73.286275L371.45098 410.603922c-4.015686-12.047059-17.066667-18.070588-28.109804-14.054902-12.047059 4.015686-18.070588 17.066667-14.054901 28.109804l123.482352 371.45098c3.011765 9.035294 12.047059 15.058824 21.082353 15.058823h72.282353l-53.207843 160.627451 46.180392 2.007844 192.752942-548.141177z" fill="#2c2c2c" p-id="9711" stroke-width="30" stroke="#2c2c2c"></path>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,43 +0,0 @@
class SocketBuilder {
websocket: WebSocket;
constructor(url: string) {
if (typeof WebSocket === 'undefined') {
throw new Error('不支持websocket');
}
if (!url) {
throw new Error('websocket url不能为空');
}
this.websocket = new WebSocket(url);
}
static builder(url: string) {
return new SocketBuilder(url);
}
open(onopen: any) {
this.websocket.onopen = onopen;
return this;
}
error(onerror: any) {
this.websocket.onerror = onerror;
return this;
}
message(onmessage: any) {
this.websocket.onmessage = onmessage;
return this;
}
close(onclose: any) {
this.websocket.onclose = onclose;
return this;
}
build() {
return this.websocket;
}
}
export default SocketBuilder;

View File

@@ -9,14 +9,20 @@ export const I18nEnum = {
En: EnumValue.of('en', 'English').setExtra({ icon: 'icon layout/en', el: enLocale }),
};
export const LinkTypeEnum = {
Iframes: EnumValue.of(1, 'ifrmaes'),
Link: EnumValue.of(2, 'link'),
};
// 资源类型
export const ResourceTypeEnum = {
Machine: EnumValue.of(1, '机器').setExtra({ icon: 'Monitor', iconColor: 'var(--el-color-primary)' }).tagTypeSuccess(),
Db: EnumValue.of(2, '数据库实例').setExtra({ icon: 'Coin', iconColor: 'var(--el-color-warning)' }).tagTypeWarning(),
Machine: EnumValue.of(1, 'tag.machine').setExtra({ icon: 'icon machine/machine', iconColor: 'var(--el-color-primary)' }).tagTypeSuccess(),
Db: EnumValue.of(2, 'tag.db').setExtra({ icon: 'icon db/db', iconColor: 'var(--el-color-warning)' }).tagTypeWarning(),
Redis: EnumValue.of(3, 'redis').setExtra({ icon: 'icon redis/redis', iconColor: 'var(--el-color-danger)' }).tagTypeInfo(),
Mongo: EnumValue.of(4, 'mongo').setExtra({ icon: 'icon mongo/mongo', iconColor: 'var(--el-color-success)' }).tagTypeDanger(),
AuthCert: EnumValue.of(5, '授权凭证').setExtra({ icon: 'Ticket', iconColor: 'var(--el-color-success)' }),
Es: EnumValue.of(6, 'ES实例').setExtra({ icon: 'icon es/es-color', iconColor: 'var(--el-color-warning)' }).tagTypeWarning(),
AuthCert: EnumValue.of(5, 'ac.ac').setExtra({ icon: 'Ticket', iconColor: 'var(--el-color-success)' }),
Es: EnumValue.of(6, 'tag.es').setExtra({ icon: 'icon es/es-color', iconColor: 'var(--el-color-warning)' }).tagTypeWarning(),
Container: EnumValue.of(7, 'tag.container').setExtra({ icon: 'icon docker/docker', iconColor: 'var(--el-color-primary)' }),
};
// 标签关联的资源类型
@@ -30,8 +36,9 @@ export const TagResourceTypeEnum = {
Redis: ResourceTypeEnum.Redis,
Mongo: ResourceTypeEnum.Mongo,
AuthCert: ResourceTypeEnum.AuthCert,
Container: ResourceTypeEnum.Container,
Db: EnumValue.of(22, '数据库').setExtra({ icon: 'Coin' }),
Db: EnumValue.of(22, '数据库').setExtra({ icon: 'icon db/db' }),
};
// 标签关联的资源类型路径
@@ -42,3 +49,31 @@ export const TagResourceTypePath = {
Db: `${TagResourceTypeEnum.DbInstance.value}/${TagResourceTypeEnum.AuthCert.value}/${TagResourceTypeEnum.Db.value}`,
Es: `${TagResourceTypeEnum.EsInstance.value}/${TagResourceTypeEnum.AuthCert.value}`,
};
// 消息子类型
export const MsgSubtypeEnum = {
UserLogin: EnumValue.of('user.login', 'login.login').setExtra({
notifyType: 'primary',
}),
MachineFileUploadSuccess: EnumValue.of('machine.file.upload.success', 'machine.fileUploadSuccess').setExtra({
notifyType: 'success',
}),
MachineFileUploadFail: EnumValue.of('machine.file.upload.fail', 'machine.fileUploadFail').setExtra({
notifyType: 'danger',
}),
DbDumpFail: EnumValue.of('db.dump.fail', 'db.dbDumpFail').setExtra({
notifyType: 'danger',
}),
SqlScriptRunSuccess: EnumValue.of('db.sqlscript.run.success', 'db.sqlScriptRunSuccess').setExtra({
notifyType: 'success',
}),
SqlScriptRunFail: EnumValue.of('db.sqlscript.run.fail', 'db.sqlScriptRunFail').setExtra({
notifyType: 'danger',
}),
FlowUserTaskTodo: EnumValue.of('flow.usertask.todo', 'flow.todoTask').setExtra({
notifyType: 'primary',
}),
};

View File

@@ -15,7 +15,7 @@ const config = {
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
// 系统版本
version: 'v1.10.1',
version: 'v1.10.4',
};
export default config;

View File

@@ -1,5 +1,8 @@
import CryptoJS from 'crypto-js';
import { getToken } from '@/common/utils/storage';
import openApi from './openApi';
import JSEncrypt from 'jsencrypt';
import { notBlank } from './assert';
/**
* AES 加密数据
@@ -36,3 +39,36 @@ export function AesDecrypt(word: string, key?: string): string {
return decrypted.toString(CryptoJS.enc.Base64);
}
var encryptor: any = null;
export async function getRsaPublicKey() {
let publicKey = sessionStorage.getItem('RsaPublicKey');
if (publicKey) {
return publicKey;
}
publicKey = (await openApi.getPublicKey()) as string;
sessionStorage.setItem('RsaPublicKey', publicKey);
return publicKey;
}
/**
* 公钥加密指定值
*
* @param value value
* @returns 加密后的值
*/
export async function RsaEncrypt(value: any) {
// 不存在则返回空值
if (!value) {
return '';
}
if (encryptor != null && sessionStorage.getItem('RsaPublicKey') != null) {
return encryptor.encrypt(value);
}
encryptor = new JSEncrypt();
const publicKey = (await getRsaPublicKey()) as string;
notBlank(publicKey, '获取公钥失败');
encryptor.setPublicKey(publicKey); //设置公钥
return encryptor.encrypt(value);
}

View File

@@ -204,6 +204,24 @@ function getApiUrl(url: string) {
return baseUrl + url + '?' + joinClientParams();
}
/**
* 创建 websocket
*/
export const createWebSocket = (url: string): Promise<WebSocket> => {
return new Promise<WebSocket>((resolve, reject) => {
const clientParam = (url.includes('?') ? '&' : '?') + joinClientParams();
const socket = new WebSocket(`${config.baseWsUrl}${url}${clientParam}`);
socket.onopen = () => {
resolve(socket);
};
socket.onerror = (e) => {
reject(e);
};
});
};
// 组装客户端参数,包括 token 和 clientId
export function joinClientParams(): string {
return `token=${getToken()}&clientId=${getClientId()}`;

View File

@@ -1,36 +0,0 @@
import openApi from './openApi';
import JSEncrypt from 'jsencrypt';
import { notBlank } from './assert';
var encryptor: any = null;
export async function getRsaPublicKey() {
let publicKey = sessionStorage.getItem('RsaPublicKey');
if (publicKey) {
return publicKey;
}
publicKey = (await openApi.getPublicKey()) as string;
sessionStorage.setItem('RsaPublicKey', publicKey);
return publicKey;
}
/**
* 公钥加密指定值
*
* @param value value
* @returns 加密后的值
*/
export async function RsaEncrypt(value: any) {
// 不存在则返回空值
if (!value) {
return '';
}
if (encryptor != null && sessionStorage.getItem('RsaPublicKey') != null) {
return encryptor.encrypt(value);
}
encryptor = new JSEncrypt();
const publicKey = (await getRsaPublicKey()) as string;
notBlank(publicKey, '获取公钥失败');
encryptor.setPublicKey(publicKey); //设置公钥
return encryptor.encrypt(value);
}

View File

@@ -4,15 +4,15 @@ import { h, reactive } from 'vue';
import { ElNotification } from 'element-plus';
import ProgressNotify from '@/components/progress-notify/progress-notify.vue';
export function initSysMsgs() {
registerDbSqlExecProgress();
export async function initSysMsgs() {
await registerDbSqlExecProgress();
}
const sqlExecNotifyMap: Map<string, any> = new Map();
function registerDbSqlExecProgress() {
syssocket.registerMsgHandler('execSqlFileProgress', function (message: any) {
const content = JSON.parse(message.msg);
async function registerDbSqlExecProgress() {
await syssocket.registerMsgHandler('sqlScriptRunProgress', function (message: any) {
const content = message.params;
const id = content.id;
let progress = sqlExecNotifyMap.get(id);
if (content.terminated) {
@@ -38,7 +38,7 @@ function registerDbSqlExecProgress() {
duration: 0,
title: message.title,
message: h(ProgressNotify, progress.props),
type: syssocket.getMsgType(message.type),
type: 'info',
showClose: false,
});
sqlExecNotifyMap.set(id, progress);

View File

@@ -1,34 +1,27 @@
import Config from './config';
import SocketBuilder from './SocketBuilder';
import { getToken } from '@/common/utils/storage';
import { joinClientParams } from './request';
import { createWebSocket } from './request';
import { ElNotification } from 'element-plus';
import { MsgSubtypeEnum } from './commonEnum';
import EnumValue from './Enum';
import { h } from 'vue';
import { MessageRenderer } from '@/components/message/message';
class SysSocket {
/**
* socket连接
*/
socket: any;
socket: WebSocket | null = null;
/**
* key -> 消息类别value -> 消息对应的处理器函数
*/
categoryHandlers: Map<string, any> = new Map();
/**
* 消息类型
*/
messageTypes: any = {
0: 'error',
1: 'success',
2: 'info',
};
/**
* 初始化全局系统消息websocket
*/
init() {
async init() {
// 存在则不需要重新建立连接
if (this.socket) {
return;
@@ -38,9 +31,9 @@ class SysSocket {
return null;
}
console.log('init system ws');
const sysMsgUrl = `${Config.baseWsUrl}/sysmsg?${joinClientParams()}`;
this.socket = SocketBuilder.builder(sysMsgUrl)
.message((event: { data: string }) => {
try {
this.socket = await createWebSocket('/sysmsg');
this.socket.onmessage = async (event: { data: string }) => {
let message;
try {
message = JSON.parse(event.data);
@@ -56,23 +49,32 @@ class SysSocket {
return;
}
// 默认通知处理
const type = this.getMsgType(message.type);
let msg = message.msg;
let duration = 0;
const msgSubtype = EnumValue.getEnumByValue(MsgSubtypeEnum, message.subtype);
if (!msgSubtype) {
console.log(`not found msg subtype: ${message.subtype}`);
return;
}
// 动态导入 i18n 或延迟获取 i18n 实例
let title = '';
try {
// 方式1: 动态导入
const { i18n } = await import('@/i18n');
title = i18n.global.t(msgSubtype?.label);
} catch (e) {
console.warn('i18n not ready, using default title');
}
ElNotification({
duration: duration,
title: message.title,
message: msg,
type: type,
duration: 0,
title,
message: h(MessageRenderer, { content: message.msg }),
type: msgSubtype?.extra.notifyType || 'info',
});
})
.open((event: any) => console.log(event))
.close(() => {
console.log('close sys socket');
this.socket = null;
})
.build();
};
} catch (e) {
console.error('open system ws error', e);
}
}
destory() {
@@ -87,8 +89,7 @@ class SysSocket {
* @param category 消息类别
* @param handlerFunc 消息处理函数
*/
registerMsgHandler(category: any, handlerFunc: any) {
this.init();
async registerMsgHandler(category: any, handlerFunc: any) {
if (this.categoryHandlers.has(category)) {
console.log(`${category}该类别消息处理器已存在...`);
return;
@@ -98,10 +99,6 @@ class SysSocket {
}
this.categoryHandlers.set(category, handlerFunc);
}
getMsgType(msgType: any) {
return this.messageTypes[msgType];
}
}
// 全局系统消息websocket;

View File

@@ -1,5 +1,7 @@
import { nextTick } from 'vue';
import '@/theme/loading.scss';
import { useThemeConfig } from '@/store/themeConfig';
import { storeToRefs } from 'pinia';
/**
* 页面全局 Loading
@@ -9,33 +11,57 @@ import '@/theme/loading.scss';
export const NextLoading = {
// 创建 loading
start: () => {
// 如果已经存在loading元素则不重复创建
if (document.querySelector('.loading-next')) {
return;
}
const bodys: Element = document.body;
const div = <HTMLElement>document.createElement('div');
div.setAttribute('class', 'loading-next');
const { themeConfig } = storeToRefs(useThemeConfig());
if (themeConfig.value.isDark) {
div.classList.add('dark');
}
const htmls = `
<div class="loading-next-box">
<div class="loading-next-box-warp">
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
</div>
</div>
`;
<div class="loading-next-box">
<div class="loading-next-box-warp">
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
</div>
</div>
`;
div.innerHTML = htmls;
bodys.insertBefore(div, bodys.childNodes[0]);
// 插入到body的第一个子元素之前避免影响布局
if (bodys.firstChild) {
bodys.insertBefore(div, bodys.firstChild);
} else {
bodys.appendChild(div);
}
},
// 移除 loading
done: (time: number = 1000) => {
done: (time: number = 500) => {
nextTick(() => {
setTimeout(() => {
const el = <HTMLElement>document.querySelector('.loading-next');
el?.parentNode?.removeChild(el);
if (el) {
// 添加淡出效果
el.style.transition = 'opacity 0.3s ease-out';
el.style.opacity = '0';
setTimeout(() => {
el?.parentNode?.removeChild(el);
}, 300);
}
}, time);
});
},

View File

@@ -1,8 +0,0 @@
// https://www.npmjs.com/package/mitt
import mitt, { Emitter } from 'mitt';
// 类型
const emitter: Emitter<any> = mitt<any>();
// 导出
export default emitter;

View File

@@ -12,7 +12,13 @@ import { ElMessage } from 'element-plus';
export function templateResolve(template: string, param: any) {
return template.replace(/\{\w+\}/g, (word) => {
const key = word.substring(1, word.length - 1);
const value = param[key];
let value;
// 兼容FormData类型的参数
if (param instanceof FormData) {
value = param.get(key);
} else {
value = param[key];
}
if (value != null || value != undefined) {
return value;
}

View File

@@ -1,241 +0,0 @@
/**
* 2020.11.29 lyt 整理
* 工具类集合,适用于平时开发
*/
// 小数或整数(不可以负数)
export function verifyNumberIntegerAndFloat(val: string) {
// 匹配空格
let v = val.replace(/(^\s*)|(\s*$)/g, '');
// 只能是数字和小数点,不能是其他输入
v = v.replace(/[^\d.]/g, '');
// 以0开始只能输入一个
v = v.replace(/^0{2}$/g, '0');
// 保证第一位只能是数字,不能是点
v = v.replace(/^\./g, '');
// 小数只能出现1位
v = v.replace('.', '$#$').replace(/\./g, '').replace('$#$', '.');
// 小数点后面保留2位
v = v.replace(/^(\-)*(\d+)\.(\d\d).*$/, '$1$2.$3');
// 返回结果
return v;
}
// 正整数验证
export function verifiyNumberInteger(val: string) {
// 匹配空格
let v = val.replace(/(^\s*)|(\s*$)/g, '');
// 去掉 '.' , 防止贴贴的时候出现问题 如 0.1.12.12
v = v.replace(/[\.]*/g, '');
// 去掉以 0 开始后面的数, 防止贴贴的时候出现问题 如 00121323
v = v.replace(/(^0[\d]*)$/g, '0');
// 首位是0,只能出现一次
v = v.replace(/^0\d$/g, '0');
// 只匹配数字
v = v.replace(/[^\d]/g, '');
// 返回结果
return v;
}
// 去掉中文及空格
export function verifyCnAndSpace(val: string) {
// 匹配中文与空格
let v = val.replace(/[\u4e00-\u9fa5\s]+/g, '');
// 匹配空格
v = v.replace(/(^\s*)|(\s*$)/g, '');
// 返回结果
return v;
}
// 去掉英文及空格
export function verifyEnAndSpace(val: string) {
// 匹配英文与空格
let v = val.replace(/[a-zA-Z]+/g, '');
// 匹配空格
v = v.replace(/(^\s*)|(\s*$)/g, '');
// 返回结果
return v;
}
// 禁止输入空格
export function verifyAndSpace(val: string) {
// 匹配空格
let v = val.replace(/(^\s*)|(\s*$)/g, '');
// 返回结果
return v;
}
// 金额用 `,` 区分开
export function verifyNumberComma(val: string) {
// 调用小数或整数(不可以负数)方法
let v: any = verifyNumberIntegerAndFloat(val);
// 字符串转成数组
v = v.toString().split('.');
// \B 匹配非单词边界,两边都是单词字符或者两边都是非单词字符
v[0] = v[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
// 数组转字符串
v = v.join('.');
// 返回结果
return v;
}
// 匹配文字变色(搜索时)
export function verifyTextColor(val: string, text = '', color = 'red') {
// 返回内容,添加颜色
let v = text.replace(new RegExp(val, 'gi'), `<span style='color: ${color}'>${val}</span>`);
// 返回结果
return v;
}
// 数字转中文大写
export function verifyNumberCnUppercase(val: any, unit = '仟佰拾亿仟佰拾万仟佰拾元角分', v = '') {
// 当前内容字符串添加 2个0为什么??
val += '00';
// 返回某个指定的字符串值在字符串中首次出现的位置,没有出现,则该方法返回 -1
let lookup = val.indexOf('.');
// substring不包含结束下标内容substr包含结束下标内容
if (lookup >= 0) val = val.substring(0, lookup) + val.substr(lookup + 1, 2);
// 根据内容 val 的长度,截取返回对应大写
unit = unit.substr(unit.length - val.length);
// 循环截取拼接大写
for (let i = 0; i < val.length; i++) {
v += '零壹贰叁肆伍陆柒捌玖'.substr(val.substr(i, 1), 1) + unit.substr(i, 1);
}
// 正则处理
v = v
.replace(/零角零分$/, '整')
.replace(/零[仟佰拾]/g, '零')
.replace(/零{2,}/g, '零')
.replace(/零([亿|万])/g, '$1')
.replace(/零+元/, '元')
.replace(/亿零{0,3}万/, '亿')
.replace(/^元/, '零元');
// 返回结果
return v;
}
// 手机号码
export function verifyPhone(val: string) {
// false: 手机号码不正确
if (!/^((12[0-9])|(13[0-9])|(14[5|7])|(15([0-3]|[5-9]))|(18[0,5-9]))\d{8}$/.test(val)) return false;
// true: 手机号码正确
else return true;
}
// 国内电话号码
export function verifyTelPhone(val: string) {
// false: 国内电话号码不正确
if (!/\d{3}-\d{8}|\d{4}-\d{7}/.test(val)) return false;
// true: 国内电话号码正确
else return true;
}
// 登录账号 (字母开头允许5-16字节允许字母数字下划线)
export function verifyAccount(val: string) {
// false: 登录账号不正确
if (!/^[a-zA-Z][a-zA-Z0-9_]{4,15}$/.test(val)) return false;
// true: 登录账号正确
else return true;
}
// 密码 (以字母开头长度在6~16之间只能包含字母、数字和下划线)
export function verifyPassword(val: string) {
// false: 密码不正确
if (!/^[a-zA-Z]\w{5,15}$/.test(val)) return false;
// true: 密码正确
else return true;
}
// 强密码 (字母+数字+特殊字符长度在6-16之间)
export function verifyPasswordPowerful(val: string) {
// false: 强密码不正确
if (!/^(?![a-zA-z]+$)(?!\d+$)(?![!@#$%^&\.*]+$)(?![a-zA-z\d]+$)(?![a-zA-z!@#$%^&\.*]+$)(?![\d!@#$%^&\.*]+$)[a-zA-Z\d!@#$%^&\.*]{6,16}$/.test(val))
return false;
// true: 强密码正确
else return true;
}
// 密码强度
export function verifyPasswordStrength(val: string) {
let v = '';
// 弱:纯数字,纯字母,纯特殊字符
if (/^(?:\d+|[a-zA-Z]+|[!@#$%^&\.*]+){6,16}$/.test(val)) v = '弱';
// 中:字母+数字,字母+特殊字符,数字+特殊字符
if (/^(?![a-zA-z]+$)(?!\d+$)(?![!@#$%^&\.*]+$)[a-zA-Z\d!@#$%^&\.*]{6,16}$/.test(val)) v = '中';
// 强:字母+数字+特殊字符
if (/^(?![a-zA-z]+$)(?!\d+$)(?![!@#$%^&\.*]+$)(?![a-zA-z\d]+$)(?![a-zA-z!@#$%^&\.*]+$)(?![\d!@#$%^&\.*]+$)[a-zA-Z\d!@#$%^&\.*]{6,16}$/.test(val)) v = '强';
// 返回结果
return v;
}
// IP地址
export function verifyIPAddress(val: string) {
// false: IP地址不正确
if (!/^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/.test(val))
return false;
// true: IP地址正确
else return true;
}
// 邮箱
export function verifyEmail(val: string) {
// false: 邮箱不正确
if (
!/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
val
)
)
return false;
// true: 邮箱正确
else return true;
}
// 身份证
export function verifyIdCard(val: string) {
// false: 身份证不正确
if (!/^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/.test(val)) return false;
// true: 身份证正确
else return true;
}
// 姓名
export function verifyFullName(val: string) {
// false: 姓名不正确
if (!/^[\u4e00-\u9fa5]{1,6}(·[\u4e00-\u9fa5]{1,6}){0,2}$/.test(val)) return false;
// true: 姓名正确
else return true;
}
// 邮政编码
export function verifyPostalCode(val: string) {
// false: 邮政编码不正确
if (!/^[1-9][0-9]{5}$/.test(val)) return false;
// true: 邮政编码正确
else return true;
}
// url
export function verifyUrl(val: string) {
// false: url不正确
if (
!/^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})).?)(?::\d{2,5})?(?:[/?#]\S*)?$/i.test(
val
)
)
return false;
// true: url正确
else return true;
}
// 车牌号
export function verifyCarNum(val: string) {
// false: 车牌号不正确
if (
!/^(([京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z](([0-9]{5}[DF])|([DF]([A-HJ-NP-Z0-9])[0-9]{4})))|([京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z][A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳使领]))$/.test(
val
)
)
return false;
// true车牌号正确
else return true;
}

View File

@@ -1,13 +0,0 @@
const mode = import.meta.env.VITE_ROUTER_MODE;
/**
* @description 获取不同路由模式所对应的 url
* @returns {String}
*/
export function getNowUrl() {
const url = {
hash: location.hash.substring(1),
history: location.pathname + location.search,
};
return url[mode];
}

View File

@@ -1,27 +0,0 @@
// vite 打包相关
import dotenv from 'dotenv';
export interface ViteEnv {
VITE_PORT: number;
VITE_OPEN: boolean;
VITE_PUBLIC_PATH: string;
VITE_EDITOR: string;
}
export function loadEnv(): ViteEnv {
const env = process.env.NODE_ENV;
const ret: any = {};
const envList = [`.env.${env}.local`, `.env.${env}`, '.env.local', '.env', ,];
envList.forEach((e) => {
dotenv.config({ path: e });
});
for (const envName of Object.keys(process.env)) {
console.log(envName);
let realName = (process.env as any)[envName].replace(/\\n/g, '\n');
realName = realName === 'true' ? true : realName === 'false' ? false : realName;
if (envName === 'VITE_PORT') realName = Number(realName);
if (envName === 'VITE_OPEN') realName = Boolean(realName);
ret[envName] = realName;
process.env[envName] = realName;
}
return ret;
}

View File

@@ -5,7 +5,7 @@ import { useUserInfo } from '@/store/userInfo';
* @param code 权限code
* @returns
*/
export function hasPerm(code: string) {
export function hasPerm(code: string): boolean {
if (!code) {
return true;
}
@@ -17,7 +17,7 @@ export function hasPerm(code: string) {
* @returns {"xxx:save": true} key->permission code
* @param permCodes
*/
export function hasPerms(permCodes: any[]) {
export function hasPerms(permCodes: any[]): Record<string, boolean> {
const res = {} as { [key: string]: boolean };
for (let permCode of permCodes) {
if (hasPerm(permCode)) {

View File

@@ -1,7 +1,7 @@
<template>
<transition @enter="onEnter" name="el-zoom-in-center">
<div
aria-hidden="true"
:aria-hidden="state.isShow ? 'false' : 'true'"
class="el-dropdown__popper el-popper is-light is-pure custom-contextmenu"
role="tooltip"
data-popper-placement="bottom"
@@ -126,7 +126,7 @@ const onCurrentContextmenuClick = (ci: ContextmenuItem) => {
emit('currentContextmenuClick', { id: ci.clickId, item: state.item });
};
const headerContextmenuClick = (event: any, data: any) => {
const headerContextmenuClick = (event: any) => {
event.preventDefault(); // 阻止默认的右击菜单行为
};

View File

@@ -12,7 +12,7 @@ const props = defineProps({
required: true,
},
value: {
type: [Object, String, Number, null],
type: [Object, String, Number, null, Boolean],
required: true,
default: () => null,
},

View File

@@ -1,14 +1,16 @@
<template>
<el-form-item v-bind="$attrs">
<template #label>
{{ props.label }}
<div class="flex items-center">
{{ props.label }}
<el-tooltip :placement="props.placement">
<template #content>
<span v-html="props.tooltip"></span>
</template>
<SvgIcon name="QuestionFilled" />
</el-tooltip>
<el-tooltip :placement="props.placement">
<template #content>
<span v-html="props.tooltip"></span>
</template>
<SvgIcon name="QuestionFilled" class="ml-1" />
</el-tooltip>
</div>
</template>
<!-- 遍历父组件传入的 solts 透传给子组件 -->
@@ -24,11 +26,11 @@ import { useSlots } from 'vue';
const props = defineProps({
label: {
type: String,
require: true,
required: true,
},
tooltip: {
type: String,
require: true,
required: true,
},
placement: {
type: String,

View File

@@ -0,0 +1,129 @@
import { ElLink, ElText } from 'element-plus';
import { defineAsyncComponent, defineComponent, h } from 'vue';
type Size = 'large' | 'default' | 'small';
interface ComponentConfig {
component: any;
getDefaultProps?: (size: Size) => Record<string, any>;
}
const linkConf = {
component: ElLink,
getDefaultProps: (size: Size) => {
return {
type: 'primary',
verticalAlign: 'baseline',
style: {
fontSize: size === 'small' ? '12px' : size === 'large' ? '16px' : '14px',
verticalAlign: 'baseline',
},
};
},
};
const components = {
'el-link': linkConf,
a: linkConf,
'error-text': {
component: ElText,
getDefaultProps: (size: Size) => {
return {
type: 'danger',
size,
};
},
},
'machine-info': {
component: defineAsyncComponent(() => import('@/views/ops/machine/component/MachineDetail.vue')),
getDefaultProps: (size: Size) => {
return {
size,
};
},
},
'db-info': {
component: defineAsyncComponent(() => import('@/views/ops/db/component/DbDetail.vue')),
getDefaultProps: (size: Size) => {
return {
size,
};
},
},
} as Record<string, ComponentConfig>;
export const MessageRenderer = defineComponent({
props: {
content: String,
size: {
type: String as () => Size,
default: 'default',
},
},
setup(props) {
const parseContent = (content: string) => {
if (!content) {
return [h('span', '')];
}
// 创建一个包装容器来处理HTML内容
const container = document.createElement('div');
container.innerHTML = content;
const parseNode = (node: Node): any => {
if (node.nodeType === Node.TEXT_NODE) {
return node.textContent;
}
if (node.nodeType === Node.ELEMENT_NODE) {
const element = node as HTMLElement;
const tagName = element.tagName.toLowerCase();
let attrs: Record<string, any> = {};
// 提取属性
for (let i = 0; i < element.attributes.length; i++) {
const attr = element.attributes[i];
attrs[attr.name] = attr.value;
}
const componentConf = components[tagName];
if (!componentConf) {
return h(tagName, attrs, Array.from(element.childNodes).map(parseNode));
}
// 存在默认组件配置,则合并
if (componentConf.getDefaultProps) {
const defaultProps = componentConf.getDefaultProps(props.size);
attrs = {
...defaultProps,
...attrs,
};
}
return h(componentConf.component, attrs, {
default: () => Array.from(element.childNodes).map(parseNode),
});
}
return '';
};
return Array.from(container.childNodes).map(parseNode);
};
return () => {
// 根据 size 属性确定根元素的 class
const rootClass = props.size === 'small' ? 'text-sm' : props.size === 'large' ? 'text-lg' : 'text-base';
try {
const elements = parseContent(props.content || '');
return h('div', { class: rootClass }, elements);
} catch (e) {
console.error('消息渲染失败:', e);
return h('div', { class: rootClass }, props.content || '');
}
};
},
});

View File

@@ -155,6 +155,7 @@ const defaultOptions = {
scrollBeyondLastLine: false,
lineNumbers: 'on',
lineNumbersMinChars: 3,
fixedOverflowWidgets: true, // 使弹出层不被容器限制
} as editor.IStandaloneEditorConstructionOptions;
const monacoTextareaRef: Ref<any> = useTemplateRef('monacoTextareaRef');

View File

@@ -0,0 +1,73 @@
<template>
<div class="h-full">
<monaco-editor
ref="editorRef"
:height="props.height"
class="editor"
language="text"
v-model="modelValue"
:options="{
readOnly: true,
}"
:can-change-mode="false"
/>
</div>
</template>
<script lang="ts" setup>
import { ref, useTemplateRef, watch } from 'vue';
import { useWebSocket } from '@vueuse/core';
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
const props = defineProps({
height: {
type: String,
default: '100%',
},
wsUrl: {
type: String,
default: '',
},
});
const websocketUrl = ref(props.wsUrl);
const { data } = useWebSocket(websocketUrl);
const editorRef: any = useTemplateRef('editorRef');
const modelValue = defineModel<string>('modelValue', {
type: String,
default: '',
});
watch(data, (value) => {
// eslint-disable-next-line no-control-regex
modelValue.value = modelValue.value + value.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '');
setTimeout(() => {
revealLastLine();
}, 200);
});
const reload = (wsUrl: string) => {
modelValue.value = '';
websocketUrl.value = wsUrl;
revealLastLine();
};
const revealLastLine = () => {
const editor = editorRef.value.getEditor();
const lineCount = editor?.getModel().getLineCount();
editor.revealLine(lineCount);
};
defineExpose({
reload,
});
</script>
<style lang="scss" scoped>
.editor {
font-size: 9pt;
font-weight: 600;
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<div class="h-full flex flex-col flex-1 overflow-hidden">
<transition name="el-zoom-in-top">
<transition name="page-table-search-form">
<!-- 查询表单 -->
<SearchForm v-if="isShowSearch" :items="tableSearchItems" v-model="queryForm" :search="search" :reset="reset" :search-col="searchCol">
<!-- 遍历父组件传入的 solts 透传给子组件 -->
@@ -21,7 +21,7 @@
<div class="flex">
<!-- 简易单个搜索项 -->
<div v-if="nowSearchItem" class="flex">
<el-dropdown v-if="searchItems?.length > 1">
<el-dropdown v-if="props.searchItems?.length > 1">
<SvgIcon :size="16" name="CaretBottom" class="!mr-1 !mt-1.5 simple-search-form-btn" />
<template #dropdown>
<el-dropdown-menu>
@@ -54,7 +54,7 @@
<!-- <el-button v-if="showToolButton('refresh')" icon="Refresh" circle @click="execQuery()" /> -->
<el-button
v-if="showToolButton('search') && searchItems?.length > 1"
v-if="showToolButton('search') && props.searchItems?.length > 1"
:icon="isShowSearch ? 'ArrowDown' : 'ArrowUp'"
circle
@click="isShowSearch = !isShowSearch"
@@ -171,9 +171,9 @@ import EnumTag from '@/components/enumtag/EnumTag.vue';
import { useThemeConfig } from '@/store/themeConfig';
import { storeToRefs } from 'pinia';
import Api from '@/common/Api';
import SearchForm from '@/components/SearchForm/index.vue';
import { SearchItem } from '../SearchForm/index';
import SearchFormItem from '../SearchForm/components/SearchFormItem.vue';
import SearchForm from '@/components/pagetable/SearchForm/index.vue';
import { SearchItem } from './SearchForm/index';
import SearchFormItem from './SearchForm/components/SearchFormItem.vue';
import SvgIcon from '@/components/svgIcon/index.vue';
import { usePageTable } from '@/hooks/usePageTable';
import { ElInput, ElTable } from 'element-plus';
@@ -365,4 +365,22 @@ defineExpose({
total,
});
</script>
<style scoped lang="scss"></style>
<style scoped lang="scss">
.page-table-search-form-enter-active {
transition: all 0.3s ease-out;
}
.page-table-search-form-leave-active {
transition: all 0.3s ease-in;
}
.page-table-search-form-enter-from {
opacity: 0;
transform: translateY(-30px) scale(0.95);
}
.page-table-search-form-leave-to {
opacity: 0;
transform: translateY(-30px) scale(0.95);
}
</style>

View File

@@ -37,11 +37,11 @@
</template>
<script setup lang="ts" name="SearchForm">
import { computed, ref } from 'vue';
import { BreakPoint } from '@/components/Grid/interface/index';
import { BreakPoint } from '@/components/pagetable/Grid/interface/index';
import { Delete, Search, ArrowDown, ArrowUp } from '@element-plus/icons-vue';
import SearchFormItem from './components/SearchFormItem.vue';
import Grid from '@/components/Grid/index.vue';
import GridItem from '@/components/Grid/components/GridItem.vue';
import Grid from '@/components/pagetable/Grid/index.vue';
import GridItem from '@/components/pagetable/Grid/components/GridItem.vue';
import SvgIcon from '@/components/svgIcon/index.vue';
import { SearchItem } from './index';

View File

@@ -22,6 +22,7 @@ import { useDebounceFn, useEventListener, useIntervalFn } from '@vueuse/core';
import themes from './themes';
import { TrzszFilter } from 'trzsz';
import { useI18n } from 'vue-i18n';
import { createWebSocket } from '@/common/request';
const { t } = useI18n();
@@ -124,7 +125,7 @@ const initTerm = async () => {
// 注册窗口大小监听器
useEventListener('resize', useDebounceFn(fitTerminal, 400));
initSocket();
await initSocket();
// 注册其他插件
loadAddon();
@@ -140,33 +141,31 @@ const initTerm = async () => {
});
};
const initSocket = () => {
const initSocket = async () => {
if (!props.socketUrl) {
return;
}
socket = new WebSocket(`${props.socketUrl}&rows=${term?.rows}&cols=${term?.cols}`);
// 监听socket连接
socket.onopen = () => {
// 注册心跳
useIntervalFn(sendPing, 15000);
state.status = TerminalStatus.Connected;
focus();
fitTerminal();
// 如果有初始要执行的命令,则发送执行命令
if (props.cmd) {
sendData(props.cmd + ' \r');
}
};
// 监听socket错误信息
socket.onerror = (e: Event) => {
try {
socket = await createWebSocket(`${props.socketUrl}${props.socketUrl.includes('?') ? '&' : '?'}rows=${term?.rows}&cols=${term?.cols}`);
} catch (e) {
term.writeln(`\r\n\x1b[31m${t('components.terminal.connErrMsg')}`);
state.status = TerminalStatus.Error;
console.log('连接错误', e);
};
return;
}
// 注册心跳
useIntervalFn(sendPing, 15000);
state.status = TerminalStatus.Connected;
focus();
fitTerminal();
// 如果有初始要执行的命令,则发送执行命令
if (props.cmd) {
sendData(props.cmd + ' \r');
}
socket.onclose = (e: CloseEvent) => {
console.log('terminal socket close...', e.reason);

View File

@@ -0,0 +1,20 @@
import { ref } from 'vue';
export function useDataState<KeyType, ValueType extends number | boolean | string>() {
const dataState = ref(new Map<KeyType, ValueType>());
const setState = (key: KeyType, value: ValueType) => {
dataState.value.set(key, value as any);
};
const getState = (key: KeyType): ValueType => {
const result = dataState.value.get(key);
return result as ValueType;
};
return {
dataState,
setState,
getState,
};
}

View File

@@ -69,6 +69,7 @@ export default {
fieldNotEmpty: '{field} cannot be empty',
selectAll: 'Select all',
MultiPlaceholder: 'Multiple are separated by commas',
appSlogan: 'Simple, efficient and secure',
},
layout: {
user: {
@@ -150,8 +151,6 @@ export default {
isUniqueOpened: 'Menu accordion',
isFixedHeader: 'Fixed header',
isClassicSplitMenu: 'Classic layout split menu',
isLockScreen: 'Open the lock screen',
lockScreenTime: 'screen locking(s/s)',
interfaceDisplay: 'Interface display',
isShowLogo: 'Sidebar logo',
isBreadcrumb: 'Open breadcrumb',
@@ -192,145 +191,6 @@ export default {
btnTwo: 'Update now',
btnTwoLoading: 'Updating',
},
menu: {
index: 'Home Page',
personalCenter: 'Personal Center',
tag: 'Tag',
tagTree: 'Tag Tree',
tagSave: 'Save Tag',
tagDelete: 'Delete Tag',
authorization: 'Authorization',
authorizationBase: 'Base Permission',
authorizationSave: 'Save Authorization',
authorizationDelete: 'Delete Authorization',
team: 'Team',
teamSave: 'Save Team',
teamDelete: 'Delete Team',
teamMemberAdd: 'Add Member',
teamMemberDelete: 'Delete Member',
teamTagSave: 'Save Team Tag',
machine: 'Machine',
machineOp: 'Machine Operation',
machineOpBase: 'Base Permission',
machineList: 'Machine List',
machineBase: 'Base Permission',
machineCreate: 'Create Machine',
machineEdit: 'Edit Machine',
machineDelete: 'Delete Machine',
machineTerminal: 'Machine Terminal',
machineFileConf: 'File',
machineFileConfCreate: 'File-Add Config',
machineFileConfDelete: 'File-Delete Config',
machineFileCreate: 'File-Create',
machineFileDelete: 'File-Delete',
machineFileWrite: 'File-Write',
machineFileUpload: 'File-Upload',
machineScript: 'Script',
machineScriptSave: 'Script-Save',
machineScriptDelete: 'Script-Delete',
machineScriptRun: 'Script-Run',
machineKillprocess: 'Kill Process',
machineCronJob: 'Cron Job',
machineCronJobSvae: 'Cron Job-Save',
machineCronJobDelete: 'Cron Job-Delete',
machineSecurityConfig: 'Security Config',
machineSecurityCmdSvae: 'Cmd Config-Save',
machineSecurityCmdDelete: 'Cmd Config-Delete',
dbms: 'DBMS',
dbDataOp: 'Data Operation',
dbDataOpBase: 'Base Permission',
dbDataOpSqlScriptRun: 'SQL Script Run',
dbInstance: 'DB Instance',
dbInstanceBase: 'Base Permission',
dbInstanceSave: 'Save Instance',
dbInstanceDelete: 'Delete Instance',
dbBase: 'Db Base Permission',
dbSave: 'Save Db',
dbDelete: 'Delete Db',
dbDataSync: 'Data Sync',
dbDataSyncBase: 'Base Permission',
dbDataSyncSave: 'Save Sync Task',
dbDataSyncDelete: 'Delete Sync Task',
dbDataSyncChangeStatus: 'Enable/Disable Sync Task',
dbDataSyncLog: 'Sync Log',
dbTransfer: 'DB Transfer',
dbTransferBase: 'Base Permission',
dbTransferSave: 'Save Transfer Task',
dbTransferDelete: 'Delete Transfer Task',
dbTransferChangeStatus: 'Enable/Disable Transfer Task',
dbTransferRun: 'Run Transfer Task',
dbTransferRunLog: 'Transfer Log',
dbTransferFileShow: 'ransfer File-Show',
dbTransferFileDelete: 'Transfer File-Delete',
dbTransferFileDownload: 'Transfer File-Download',
dbTransferFileRun: 'Transfer File-Run',
redis: 'Redis',
redisDataOp: 'Data Operation',
redisDataOpBase: 'Base Permission',
redisDataOpSave: 'Save Data',
redisDataOpDelete: 'Delete Data',
redisManage: 'Redis Manage',
redisManageBase: 'Base Permission',
mongo: 'Mongo',
mongoDataOp: 'Data Operation',
mongoDataOpBase: 'Base Permission',
mongoDataOpSave: 'Save Data',
mongoDataOpDelete: 'Delete Data',
mongoManage: 'Mongo Manage',
mongoManageBase: 'Base Permission',
flow: 'Flow',
myTask: 'My Task',
myFlow: 'My Flow',
flowProcDef: 'Process Define',
flowProcDefSave: 'Save Process Define',
flowProcDefDelete: 'Delete Process Define',
msgManage: 'Message',
channel: 'Message Channel',
msgChannelBase: 'Base Permission',
saveMsgChannel: 'Save Message Channel',
delMsgChannel: 'Delete Message Channel',
msgTmpl: 'Message Template',
msgTmplBase: 'Base Permission',
saveMsgTmpl: 'Save Message Template',
delMsgTmpl: 'Delete Message Template',
sendMsg: 'Send Message',
system: 'System',
menuPermission: 'Menu & Permission',
menuPermissionBase: 'Base Permission',
menuPermissionAdd: 'Add Menu Permission',
menuPermissionEdit: 'Edit Menu Permission',
menuPermissionDelete: 'Delete Menu Permission',
menuPermissionEnableDisable: 'Enable/Disable Menu Permission',
account: 'Account',
accountBase: 'Base Permission',
accountAdd: 'Add Account',
accountEdit: 'Edit Account',
accountDelete: 'Delete Account',
accountEnableDisable: 'Enable/Disable Account',
accountRoleAllocation: 'Role Allocation',
role: 'Role',
roleBase: 'Base Permission',
roleAdd: 'Add Role',
roleEdit: 'Edit Role',
roleDelete: 'Delete Role',
roleMenuPermissionAllocation: 'Menu & Permission Allocation',
sysConf: 'System Config',
sysConfBase: 'Base Permission',
sysConfSave: 'Save System Config',
opLog: 'Operation Log',
opLogBase: 'Base Permission',
noPagePermission: 'No Page Permission',
authcertShowciphertext: 'Show Ciphertext',
},
home: {
personalInfo: 'Personal Information',
welcomeMsg: `Hello, {name}, no matter how bad life gets, it doesn't prevent me from getting better!`,

View File

@@ -16,6 +16,7 @@ export default {
dbFilterPlaceholder: 'DB name: Input filterable',
sqlRecord: 'SQL records',
dump: 'Export',
dbDumpFail: 'DB export failed',
dumpContent: 'Export Content',
structure: 'Structure',
data: 'Data',
@@ -55,6 +56,8 @@ export default {
execSuccess: 'Successful execution',
execFail: 'Execution failure',
sqlScriptRun: 'Run SQL Script',
sqlScriptRunSuccess: 'SQL script executed successfully',
sqlScriptRunFail: 'SQL script execution failed',
saveSql: 'Save SQL',
execInfo: 'Execution info',
result: 'Result',
@@ -62,7 +65,7 @@ export default {
resultSet: 'Result Set',
tableDataEmptyTextTips:
'tips: Single table query at the beginning of select * or click the default query data of the table name, double-click the data online modification',
noSelctRunSqlMsg: 'Select the sql you want to execute',
noSelectRunSqlMsg: 'Select the sql you want to execute or move the cursor near the sql you want to execute',
enterExecRemarkTips: 'Please enter remark',
execRemarkPlaceholder: 'Enter the remark to execute the sql',
currentSqlTabIsRunning: 'The current result set tab is being executed, please use the new TAB to execute',

View File

@@ -0,0 +1,83 @@
export default {
docker: {
containerConf: 'Container Config',
addr: 'Address',
addrTips: 'eg: unix:///var/run/docker.sock 、tcp://192.168.1.1',
container: 'Container',
containerName: 'Container Name',
running: 'Running',
stopped: 'Stopped',
name: 'Container Name',
ip: 'IP Address',
status: 'Status',
stats: 'Stats',
memory: 'Memory',
stop: 'Stop',
stopContainerConfirm: 'Are you sure to stop container [{name}] ?',
removeContainerConfirm: 'Are you sure to remove container [{name}] ?',
restart: 'Restart',
createContainer: 'Create Container',
mount: 'Mount',
hostDir: 'Host Directory',
containerDir: 'Container Directory',
permission: 'Permission',
rw: 'RW',
ro: 'RO',
port: 'Port',
image: 'Image',
tag: 'Tag',
size: 'Size',
used: 'Used',
unUsed: 'UnUsed',
imageName: 'Image Name',
log: 'Log',
lines: 'Lines',
follow: 'Follow',
stopImageConfirm: 'Are you sure to stop image [{name}] ?',
export: 'Export',
imageUploading: 'Image uploading, please wait...',
imageTips: 'Support manual input and select',
forcePull: 'Force Pull Image',
hostPortPlaceholder: '80',
forcePullTips: 'Ignore the server existing image, pull again',
server: 'Server',
protocol: 'Protocol',
networkMode: 'Network Mode',
consoleTerminal: 'Console Terminal',
otherOption: 'Other Option',
tty: 'tty',
openStdin: 'stdin (-i)',
privileged: 'Privileged',
restartPolicy: 'Restart Policy',
noRestart: 'No Restart',
alwaysRestart: 'Always Restart',
onFailure: 'On Failure',
unlessStopped: 'Unless Stopped',
cpuShare: 'CPU Share',
cpuShareTips: 'The default container share is 1024 cpus, and increasing it will give the current container more CPU time',
cpuQuota: 'CPU Quota',
cpuLimitTips: 'A CPU limit of 0 turns off the limit',
cpuCanUseTips: 'The maximum available is {cpuTotal} cores',
core: 'Core',
memoryLimit: 'Memory Limit',
memoryLimitTips: 'A memory limit of 0 turns off the limit',
shmSize: 'Shm Size',
memoryCanUseTips: 'Maximum available {memTotal}',
tagTips: `One in a row, for example:
tag1=value1
tag2=value2`,
envParam: 'Env Param',
envParamTips: `One in a row, for example:
env1=value1
env2=value2`,
device: 'Device',
driver: 'Driver',
driverTips: 'Device drivers to be used by the container, e.g. : nvidia, etc',
count: 'Count',
capabilitie: 'Capabilitie',
deviceId: 'Device ID',
capabilitiePlaceholder: 'eg: gpu',
},
};

View File

@@ -1,6 +1,7 @@
export default {
es: {
keywordPlaceholder: 'host / name / code',
protocol: 'Protocol',
port: 'Port',
size: 'size',
docs: 'docs',
@@ -16,11 +17,11 @@ export default {
connSuccess: 'be connected successfully',
shouldTestConn: 'please test connection first',
instance: 'ES Instance',
instanceSave: 'Save Instance',
instanceDel: 'Delete Instance',
operation: 'Data Operation',
dataSave: 'Data Save',
dataDel: 'Data Del',
instanceSave: 'ES-Save Instance',
instanceDel: 'Es-Delete Instance',
operation: 'Es-Data Operation',
dataSave: 'Es-Data Save',
dataDel: 'Es-Data Del',
indexName: 'Index Name',
requireIndexName: 'Index Name Is Required',
indexDetail: 'Index Detail',

View File

@@ -65,7 +65,7 @@ export default {
processName: 'Process Name',
selectSortType: 'Please select a sort type',
selectProcessNum: 'Please select the number of processes',
cpuDesc: 'CUP descending',
cpuDesc: 'CPU descending',
memDesc: 'Memory descending',
virtualMemory: 'Virtual Memory',
fixedMemory: 'Fixed Memory',
@@ -138,5 +138,7 @@ export default {
fileTooLargeTips: 'The file is too large, please download and use it',
uploadSuccess: 'Upload successfully',
fileExceedsSysConf: 'The uploaded file exceeds the system configuration [{uploadMaxFileSize}]',
fileUploadSuccess: 'Machine file upload successful',
fileUploadFail: 'Machine file upload failed',
},
};

View File

@@ -0,0 +1,144 @@
export default {
menu: {
index: 'Home',
personalCenter: 'Personal Center',
myResource: 'Resource',
tag: 'Resource',
tagTree: 'Resource Tree',
tagSave: 'Save Tag',
tagDelete: 'Delete Tag',
authorization: 'Authorization',
authorizationBase: 'Base Permission',
authorizationSave: 'Save Authorization',
authorizationDelete: 'Delete Authorization',
team: 'Team',
teamSave: 'Save Team',
teamDelete: 'Delete Team',
teamMemberAdd: 'Add Member',
teamMemberDelete: 'Delete Member',
teamTagSave: 'Save Team Tag',
machine: 'Machine',
machineOp: 'Machine Operation',
machineOpBase: 'Base Permission',
machineList: 'Machine List',
machineBase: 'Base Permission',
machineCreate: 'Create Machine',
machineEdit: 'Edit Machine',
machineDelete: 'Delete Machine',
machineTerminal: 'Machine Terminal',
machineFileConf: 'File',
machineFileConfCreate: 'File-Add Config',
machineFileConfDelete: 'File-Delete Config',
machineFileCreate: 'File-Create',
machineFileDelete: 'File-Delete',
machineFileWrite: 'File-Write',
machineFileUpload: 'File-Upload',
machineScript: 'Script',
machineScriptSave: 'Script-Save',
machineScriptDelete: 'Script-Delete',
machineScriptRun: 'Script-Run',
machineKillprocess: 'Kill Process',
machineCronJob: 'Cron Job',
machineCronJobSvae: 'Cron Job-Save',
machineCronJobDelete: 'Cron Job-Delete',
machineSecurityConfig: 'Security Config',
machineSecurityCmdSvae: 'Cmd Config-Save',
machineSecurityCmdDelete: 'Cmd Config-Delete',
dbms: 'DBMS',
dbDataOp: 'Data Operation',
dbDataOpBase: 'Base Permission',
dbDataOpSqlScriptRun: 'SQL Script Run',
dbInstance: 'DB Instance',
dbInstanceBase: 'Base Permission',
dbInstanceSave: 'Save Instance',
dbInstanceDelete: 'Delete Instance',
dbBase: 'Db Base Permission',
dbSave: 'Save Db',
dbDelete: 'Delete Db',
dbDataSync: 'Data Sync',
dbDataSyncBase: 'Base Permission',
dbDataSyncSave: 'Save Sync Task',
dbDataSyncDelete: 'Delete Sync Task',
dbDataSyncChangeStatus: 'Enable/Disable Sync Task',
dbDataSyncLog: 'Sync Log',
dbTransfer: 'DB Transfer',
dbTransferBase: 'Base Permission',
dbTransferSave: 'Save Transfer Task',
dbTransferDelete: 'Delete Transfer Task',
dbTransferChangeStatus: 'Enable/Disable Transfer Task',
dbTransferRun: 'Run Transfer Task',
dbTransferRunLog: 'Transfer Log',
dbTransferFileShow: 'ransfer File-Show',
dbTransferFileDelete: 'Transfer File-Delete',
dbTransferFileDownload: 'Transfer File-Download',
dbTransferFileRun: 'Transfer File-Run',
redis: 'Redis',
redisDataOp: 'Data Operation',
redisDataOpBase: 'Base Permission',
redisDataOpSave: 'Save Data',
redisDataOpDelete: 'Delete Data',
redisManage: 'Redis Manage',
redisManageBase: 'Base Permission',
mongo: 'Mongo',
mongoDataOp: 'Data Operation',
mongoDataOpBase: 'Base Permission',
mongoDataOpSave: 'Save Data',
mongoDataOpDelete: 'Delete Data',
mongoManage: 'Mongo Manage',
mongoManageBase: 'Base Permission',
containerManageBase: 'Container Manage - Base Permission',
flow: 'Flow',
myTask: 'My Task',
myFlow: 'My Flow',
flowProcDef: 'Process Define',
flowProcDefSave: 'Save Process Define',
flowProcDefDelete: 'Delete Process Define',
msgManage: 'Message',
channel: 'Message Channel',
msgChannelBase: 'Base Permission',
saveMsgChannel: 'Save Message Channel',
delMsgChannel: 'Delete Message Channel',
msgTmpl: 'Message Template',
msgTmplBase: 'Base Permission',
saveMsgTmpl: 'Save Message Template',
delMsgTmpl: 'Delete Message Template',
sendMsg: 'Send Message',
system: 'System',
menuPermission: 'Menu & Permission',
menuPermissionBase: 'Base Permission',
menuPermissionAdd: 'Add Menu Permission',
menuPermissionEdit: 'Edit Menu Permission',
menuPermissionDelete: 'Delete Menu Permission',
menuPermissionEnableDisable: 'Enable/Disable Menu Permission',
account: 'Account',
accountBase: 'Base Permission',
accountAdd: 'Add Account',
accountEdit: 'Edit Account',
accountDelete: 'Delete Account',
accountEnableDisable: 'Enable/Disable Account',
accountRoleAllocation: 'Role Allocation',
role: 'Role',
roleBase: 'Base Permission',
roleAdd: 'Add Role',
roleEdit: 'Edit Role',
roleDelete: 'Delete Role',
roleMenuPermissionAllocation: 'Menu & Permission Allocation',
sysConf: 'System Config',
sysConfBase: 'Base Permission',
sysConfSave: 'Save System Config',
opLog: 'Operation Log',
opLogBase: 'Base Permission',
noPagePermission: 'No Page Permission',
authcertShowciphertext: 'Show Ciphertext',
},
};

View File

@@ -7,6 +7,7 @@ export default {
tagTips1: '1. Used to group assets',
tagTips2: '2. Can be allocated in team management for resource isolation',
tagTips3: '3. Team members who own a parent tag have access to resources that manipulate their own or child tag associations',
tagTips4: '4. Right-click nodes to edit or add child tags',
machine: 'Machine',
db: 'Db',
code: 'Code',
@@ -14,6 +15,12 @@ export default {
createSubTagTitle: 'Creates a child tag for {codePath}',
rootTag: 'Root Tag',
selectTagPlaceholder: 'Select the associated tag',
machineOp: 'Machine Operation',
dbDataOp: 'Db Operation',
redisDataOp: 'Redis Operation',
esDataOp: 'Es Operation',
mongoDataOp: 'Mongo Operation',
allResource: 'All Resource',
},
team: {
team: 'Team',

View File

@@ -69,6 +69,7 @@ export default {
fieldNotEmpty: '{field}不能为空',
selectAll: '全选',
MultiPlaceholder: '多个用逗号隔开',
appSlogan: '简洁 · 高效 · 安全',
},
layout: {
user: {
@@ -152,8 +153,6 @@ export default {
isUniqueOpened: '菜单手风琴',
isFixedHeader: '固定 Header',
isClassicSplitMenu: '经典布局分割菜单',
isLockScreen: '开启锁屏',
lockScreenTime: '自动锁屏(s/秒)',
interfaceDisplay: '界面显示',
isShowLogo: '侧边栏 Logo',
isBreadcrumb: '开启 Breadcrumb',
@@ -202,145 +201,6 @@ export default {
btnTwo: '马上更新',
btnTwoLoading: '更新中',
},
menu: {
index: '首页',
personalCenter: '个人中心',
tag: '标签管理',
tagTree: '标签树',
tagSave: '保存标签',
tagDelete: '删除标签',
authorization: '授权凭证',
authorizationBase: '基础权限',
authorizationSave: '保存权限',
authorizationDelete: '删除权限',
team: '团队管理',
teamSave: '保存团队',
teamDelete: '删除团队',
teamMemberAdd: '添加成员',
teamMemberDelete: '删除成员',
teamTagSave: '保存团队标签',
machine: '机器管理',
machineOp: '机器操作',
machineOpBase: '基本权限',
machineList: '机器列表',
machineBase: '基本权限',
machineCreate: '创建机器',
machineEdit: '编辑机器',
machineDelete: '删除机器',
machineTerminal: '机器终端',
machineFileConf: '文件管理',
machineFileConfCreate: '文件-添加配置',
machineFileConfDelete: '文件-删除配置',
machineFileCreate: '文件-创建',
machineFileDelete: '文件-删除',
machineFileWrite: '文件-写入',
machineFileUpload: '文件-上传',
machineScript: '脚本管理',
machineScriptSave: '脚本-保存',
machineScriptDelete: '脚本-删除',
machineScriptRun: '脚本-执行',
machineKillprocess: '终止进程',
machineCronJob: '计划任务',
machineCronJobSvae: '计划任务-保存',
machineCronJobDelete: '计划任务-删除',
machineSecurityConfig: '安全配置',
machineSecurityCmdSvae: '命令配置-保存',
machineSecurityCmdDelete: '命令配置-删除',
dbms: 'DBMS',
dbDataOp: '数据操作',
dbDataOpBase: '基本权限',
dbDataOpSqlScriptRun: 'SQL脚本执行',
dbInstance: '数据库实例',
dbInstanceBase: '基本权限',
dbInstanceSave: '保存实例',
dbInstanceDelete: '删除实例',
dbBase: '数据库基本权限',
dbSave: '保存数据库',
dbDelete: '删除数据库',
dbDataSync: '数据同步',
dbDataSyncBase: '基本权限',
dbDataSyncSave: '保存同步',
dbDataSyncDelete: '删除同步',
dbDataSyncChangeStatus: '启用停用',
dbDataSyncLog: '同步日志',
dbTransfer: '数据库迁移',
dbTransferBase: '基本权限',
dbTransferSave: '保存迁移任务',
dbTransferDelete: '删除迁移任务',
dbTransferChangeStatus: '启用停用',
dbTransferRun: '执行迁移任务',
dbTransferRunLog: '迁移日志查看',
dbTransferFileShow: '迁移文件-查看',
dbTransferFileDelete: '迁移文件-删除',
dbTransferFileDownload: '迁移文件-下载',
dbTransferFileRun: '迁移文件-执行',
redis: 'Redis',
redisDataOp: '数据操作',
redisDataOpBase: '基本权限',
redisDataOpSave: '数据保存',
redisDataOpDelete: '数据删除',
redisManage: 'Redis管理',
redisManageBase: '基本权限',
mongo: 'Mongo',
mongoDataOp: '数据操作',
mongoDataOpBase: '基本权限',
mongoDataOpSave: '数据保存',
mongoDataOpDelete: '数据删除',
mongoManage: 'Mongo管理',
mongoManageBase: '基本权限',
flow: '工单流程',
myTask: '我的任务',
myFlow: '我的流程',
flowProcDef: '流程定义',
flowProcDefSave: '保存流程定义',
flowProcDefDelete: '删除流程定义',
msgManage: '消息管理',
channel: '消息渠道',
msgChannelBase: '基础权限',
saveMsgChannel: '保存消息渠道',
delMsgChannel: '删除消息渠道',
msgTmpl: '消息模板',
msgTmplBase: '基础权限',
saveMsgTmpl: '保存消息模板',
delMsgTmpl: '删除消息模板',
sendMsg: '发送消息',
system: '系统管理',
menuPermission: '菜单权限',
menuPermissionBase: '基本权限',
menuPermissionAdd: '添加菜单权限',
menuPermissionEdit: '编辑菜单权限',
menuPermissionDelete: '删除菜单权限',
menuPermissionEnableDisable: '启用/禁用菜单权限',
account: '账号管理',
accountBase: '基本权限',
accountAdd: '添加账号',
accountEdit: '编辑账号',
accountDelete: '删除账号',
accountEnableDisable: '启用/禁用账号',
accountRoleAllocation: '角色分配',
role: '角色管理',
roleBase: '基本权限',
roleAdd: '添加角色',
roleEdit: '编辑角色',
roleDelete: '删除角色',
roleMenuPermissionAllocation: '菜单权限分配',
sysConf: '系统配置',
sysConfBase: '基本权限',
sysConfSave: '保存配置',
opLog: '操作日志',
opLogBase: '基本权限',
noPagePermission: '无页面权限',
authcertShowciphertext: '授权凭证密文查看',
},
home: {
personalInfo: '个人信息',
welcomeMsg: '您好, {name},生活变的再糟糕,也不妨碍我变得更好!',

View File

@@ -16,6 +16,7 @@ export default {
dbFilterPlaceholder: '库名: 输入可过滤',
sqlRecord: 'SQL记录',
dump: '导出',
dbDumpFail: '数据库导出失败',
dumpContent: '导出内容',
structure: '结构',
data: '数据',
@@ -55,13 +56,15 @@ export default {
execSuccess: '执行成功',
execFail: '执行失败',
sqlScriptRun: 'SQL脚本执行',
sqlScriptRunSuccess: 'SQL脚本执行成功',
sqlScriptRunFail: 'SQL脚本执行失败',
saveSql: '保存SQL',
execInfo: '执行信息',
result: '结果',
times: '耗时',
resultSet: '结果集',
tableDataEmptyTextTips: 'tips: select *开头的单表查询或点击表名默认查询的数据,可双击数据在线修改',
noSelctRunSqlMsg: '请选中需要执行的sql',
noSelectRunSqlMsg: '请选中需要执行的sql或将光标移动到要执行sql附近',
enterExecRemarkTips: '请输入备注',
execRemarkPlaceholder: '输入执行该sql的备注信息',
currentSqlTabIsRunning: '当前结果集tab正在执行, 请使用新标签执行',

View File

@@ -0,0 +1,83 @@
export default {
docker: {
containerConf: '容器配置',
addr: '地址',
addrTips: '如unix:///var/run/docker.sock 、tcp://192.168.1.1',
container: '容器',
containerName: '容器名',
running: '运行中',
stopped: '已停止',
name: '容器名',
ip: 'IP地址',
status: '状态',
stats: '资源使用率',
memory: '内存',
stop: '停止',
stopContainerConfirm: '确定停止容器 [{name}] ',
removeContainerConfirm: '确定删除容器 [{name}] ',
restart: '重启',
createContainer: '创建容器',
mount: '挂载',
hostDir: '本机目录',
containerDir: '容器目录',
permission: '权限',
rw: '读写',
ro: '只读',
port: '端口',
image: '镜像',
tag: '标签',
size: '大小',
used: '已使用',
unUsed: '未使用',
imageName: '镜像名',
log: '日志',
lines: '行数',
follow: '实时',
stopImageConfirm: '确定删除该镜像?',
export: '导出',
imageUploading: '镜像导入中,请稍后...',
imageTips: '支持手动输入并选择',
forcePull: '强制拉取镜像',
hostPortPlaceholder: '80',
forcePullTips: '忽略服务器已存在的镜像,重新拉取一次',
server: '服务器',
protocol: '协议',
networkMode: '网络模式',
consoleTerminal: '控制台交互',
otherOption: '其他可选项',
tty: '伪终端 (-t)',
openStdin: '标准输入 (-i)',
privileged: '特权模式',
restartPolicy: '重启策略',
noRestart: '不重启',
alwaysRestart: '一直重启',
onFailure: '失败后重启',
unlessStopped: '未手动停止则重启',
cpuShare: 'CPU权重',
cpuShareTips: '容器默认份额为 1024 个 CPU增大可使当前容器获得更多的 CPU 时间',
cpuQuota: 'CPU 限制',
cpuLimitTips: 'CPU限制为 0 则关闭限制',
cpuCanUseTips: '最大可用为{cpuTotal}核',
core: '核',
memoryLimit: '内存限制',
memoryLimitTips: '内存限制为 0 则关闭限制',
shmSize: '共享内存',
memoryCanUseTips: '最大可用为{memTotal}',
tagTips: `一行一个,例如:
tag1=value1
tag2=value2`,
envParam: '环境变量',
envParamTips: `一行一个,例如:
env1=value1
env2=value2`,
device: '设备',
driver: '驱动',
driverTips: '容器需要使用的设备驱动程序,如: nvidia 等',
count: '数量',
capabilitie: '能力',
deviceId: '设备ID',
capabilitiePlaceholder: '如: gpu',
},
};

View File

@@ -1,6 +1,7 @@
export default {
es: {
keywordPlaceholder: 'host / 名称 / 编号',
protocol: '协议',
port: '端口',
size: '存储大小',
docs: '文档数',
@@ -16,11 +17,11 @@ export default {
connSuccess: '连接成功',
shouldTestConn: '请先测试连接可用性',
instance: 'ES实例',
instanceSave: '实例保存',
instanceDel: '实例删除',
operation: '数据操作',
dataSave: '数据保存',
dataDel: '数据删除',
instanceSave: 'Es-实例保存',
instanceDel: 'Es-实例删除',
operation: 'Es-数据操作',
dataSave: 'Es-数据保存',
dataDel: 'Es-数据删除',
indexName: '索引名',
requireIndexName: '请填写索引名',
indexDetail: '索引详情',

View File

@@ -66,7 +66,7 @@ export default {
processName: '进程名',
selectSortType: '请选择排序类型',
selectProcessNum: '请选择进程个数',
cpuDesc: 'CUP降序',
cpuDesc: 'CPU降序',
memDesc: '内存降序',
virtualMemory: '虚拟内存',
fixedMemory: '固定内存',
@@ -139,5 +139,7 @@ export default {
fileTooLargeTips: '文件太大, 请下载使用',
uploadSuccess: '上传成功',
fileExceedsSysConf: '上传的文件超过系统配置的【{uploadMaxFileSize}】',
fileUploadSuccess: '机器文件上传成功',
fileUploadFail: '机器文件上传失败',
},
};

View File

@@ -0,0 +1,144 @@
export default {
menu: {
index: '首页',
personalCenter: '个人中心',
myResource: '我的资源',
tag: '资源',
tagTree: '资源树',
tagSave: '保存标签',
tagDelete: '删除标签',
authorization: '授权凭证',
authorizationBase: '基础权限',
authorizationSave: '保存权限',
authorizationDelete: '删除权限',
team: '团队',
teamSave: '保存团队',
teamDelete: '删除团队',
teamMemberAdd: '添加成员',
teamMemberDelete: '删除成员',
teamTagSave: '保存团队标签',
machine: '机器',
machineOp: '机器操作',
machineOpBase: '机器操作-基本权限',
machineList: '机器列表',
machineBase: '机器-基本权限',
machineCreate: '机器-创建机器',
machineEdit: '机器-编辑机器',
machineDelete: '机器-删除机器',
machineTerminal: '机器-机器终端',
machineFileConf: '机器-文件管理',
machineFileConfCreate: '机器-文件-添加配置',
machineFileConfDelete: '机器-文件-删除配置',
machineFileCreate: '机器-文件-创建',
machineFileDelete: '机器-文件-删除',
machineFileWrite: '机器-文件-写入',
machineFileUpload: '机器-文件-上传',
machineScript: '机器-脚本管理',
machineScriptSave: '机器-脚本-保存',
machineScriptDelete: '机器-脚本-删除',
machineScriptRun: '机器-脚本-执行',
machineKillprocess: '机器-终止进程',
machineCronJob: '计划任务',
machineCronJobSvae: '机器-计划任务-保存',
machineCronJobDelete: '机器-计划任务-删除',
machineSecurityConfig: '安全配置',
machineSecurityCmdSvae: '机器-命令配置-保存',
machineSecurityCmdDelete: '机器-命令配置-删除',
dbms: 'DBMS',
dbDataOp: '数据操作',
dbDataOpBase: 'Db-数据操作-基本权限',
dbDataOpSqlScriptRun: 'Db-SQL脚本执行',
dbInstance: '数据库实例',
dbInstanceBase: 'Db-基本权限',
dbInstanceSave: 'Db-保存实例',
dbInstanceDelete: 'Db-删除实例',
dbBase: '数据库基本权限',
dbSave: 'Db-保存数据库',
dbDelete: 'Db-删除数据库',
dbDataSync: '数据同步',
dbDataSyncBase: '基本权限',
dbDataSyncSave: '保存同步',
dbDataSyncDelete: '删除同步',
dbDataSyncChangeStatus: '启用停用',
dbDataSyncLog: '同步日志',
dbTransfer: '数据库迁移',
dbTransferBase: '基本权限',
dbTransferSave: '保存迁移任务',
dbTransferDelete: '删除迁移任务',
dbTransferChangeStatus: '启用停用',
dbTransferRun: '执行迁移任务',
dbTransferRunLog: '迁移日志查看',
dbTransferFileShow: '迁移文件-查看',
dbTransferFileDelete: '迁移文件-删除',
dbTransferFileDownload: '迁移文件-下载',
dbTransferFileRun: '迁移文件-执行',
redis: 'Redis',
redisDataOp: 'Redis-数据操作',
redisDataOpBase: 'Redis-数据操作-基本权限',
redisDataOpSave: 'Redis-数据操作-数据保存',
redisDataOpDelete: 'Redis-数据操作-数据删除',
redisManage: 'Redis管理',
redisManageBase: 'Redis-管理-基本权限',
mongo: 'Mongo',
mongoDataOp: '数据操作',
mongoDataOpBase: 'Mongo-数据操作-基本权限',
mongoDataOpSave: 'Mongo-数据操作-数据保存',
mongoDataOpDelete: 'Mongo-数据操作-数据删除',
mongoManage: 'Mongo管理',
mongoManageBase: 'Mongo-管理-基本权限',
containerManageBase: '容器-管理-基本权限',
flow: '工单流程',
myTask: '我的任务',
myFlow: '我的流程',
flowProcDef: '流程定义',
flowProcDefSave: '保存流程定义',
flowProcDefDelete: '删除流程定义',
msgManage: '消息',
channel: '消息渠道',
msgChannelBase: '基础权限',
saveMsgChannel: '保存消息渠道',
delMsgChannel: '删除消息渠道',
msgTmpl: '消息模板',
msgTmplBase: '基础权限',
saveMsgTmpl: '保存消息模板',
delMsgTmpl: '删除消息模板',
sendMsg: '发送消息',
system: '系统管理',
menuPermission: '菜单权限',
menuPermissionBase: '基本权限',
menuPermissionAdd: '添加菜单权限',
menuPermissionEdit: '编辑菜单权限',
menuPermissionDelete: '删除菜单权限',
menuPermissionEnableDisable: '启用/禁用菜单权限',
account: '账号管理',
accountBase: '基本权限',
accountAdd: '添加账号',
accountEdit: '编辑账号',
accountDelete: '删除账号',
accountEnableDisable: '启用/禁用账号',
accountRoleAllocation: '角色分配',
role: '角色管理',
roleBase: '基本权限',
roleAdd: '添加角色',
roleEdit: '编辑角色',
roleDelete: '删除角色',
roleMenuPermissionAllocation: '菜单权限分配',
sysConf: '系统配置',
sysConfBase: '基本权限',
sysConfSave: '保存配置',
opLog: '操作日志',
opLogBase: '基本权限',
noPagePermission: '无页面权限',
authcertShowciphertext: '授权凭证密文查看',
},
};

View File

@@ -7,14 +7,23 @@ export default {
tagTips1: '1. 用于将资产进行归类',
tagTips2: '2. 可在团队管理中进行分配,用于资源隔离',
tagTips3: '3. 拥有父标签的团队成员可访问操作其自身或子标签关联的资源',
tagTips4: '4. 右击节点可进行编辑或添加子标签操作',
machine: '机器',
db: '数据库',
es: 'ES',
container: '容器',
code: '编号',
createSubTag: '创建子标签',
createSubTagTitle: '创建【{codePath}】的子标签',
rootTag: '根标签',
selectTagPlaceholder: '请选择关联标签',
machineOp: '机器操作',
dbDataOp: '数据库操作',
redisDataOp: 'Redis操作',
esDataOp: 'ES操作',
mongoDataOp: 'Mongo操作',
containerOp: '容器操作',
allResource: '所有资源',
},
team: {
team: '团队',

View File

@@ -1,5 +1,5 @@
<template>
<el-aside class="layout-aside" :class="setCollapseWidth" v-if="state.clientWidth > 1000">
<el-aside class="layout-aside" :class="setCollapseWidth" v-if="clientWidth > 1000">
<Logo v-if="setShowLogo" />
<el-scrollbar class="flex-auto" ref="layoutAsideScrollbarRef">
<Vertical :menuList="state.menuList" :class="setCollapseWidth" />
@@ -16,25 +16,31 @@
</template>
<script lang="ts" setup name="layoutAside">
import { reactive, computed, watch, getCurrentInstance, onBeforeMount, onUnmounted } from 'vue';
import { reactive, computed, watch, getCurrentInstance, onBeforeMount, inject, defineAsyncComponent } from 'vue';
import pinia from '@/store/index';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
import { useRoutesList } from '@/store/routesList';
import Logo from '@/layout/logo/index.vue';
import Vertical from '@/layout/navMenu/vertical.vue';
import mittBus from '@/common/utils/mitt';
import { useWindowSize } from '@vueuse/core';
const Logo = defineAsyncComponent(() => import('@/layout/logo/index.vue'));
const Vertical = defineAsyncComponent(() => import('@/layout/navMenu/vertical.vue'));
const { proxy } = getCurrentInstance() as any;
const { themeConfig } = storeToRefs(useThemeConfig());
const { routesList } = storeToRefs(useRoutesList());
const state: any = reactive({
menuList: [],
clientWidth: '',
const { width: clientWidth } = useWindowSize();
const state = reactive({
menuList: [] as any[],
});
// 注入 菜单数据
const columnsMenuData: any = inject('columnsMenuData', null);
const classicMenuData: any = inject('classicMenuData', null);
// 设置菜单展开/收起时的宽度
const setCollapseWidth = computed(() => {
let { layout, isCollapse, menuBar } = themeConfig.value;
@@ -64,7 +70,9 @@ const setShowLogo = computed(() => {
// 设置/过滤路由(非静态路由/是否显示在菜单中)
const setFilterRoutes = () => {
if (themeConfig.value.layout === 'columns') return false;
if (themeConfig.value.layout === 'columns') {
return false;
}
state.menuList = filterRoutesFun(routesList.value);
};
@@ -78,53 +86,58 @@ const filterRoutesFun = (arr: Array<object>) => {
return item;
});
};
// 设置菜单导航是否固定(移动端)
const initMenuFixed = (clientWidth: number) => {
state.clientWidth = clientWidth;
};
// 监听 themeConfig 配置文件的变化,更新菜单 el-scrollbar 的高度
watch(themeConfig.value, (val) => {
if (val.isShowLogoChange !== val.isShowLogo) {
if (!proxy.$refs.layoutAsideScrollbarRef) return false;
if (!proxy.$refs.layoutAsideScrollbarRef) {
return false;
}
proxy.$refs.layoutAsideScrollbarRef.update();
}
});
// 监听路由的变化,动态赋值给菜单中
watch(pinia.state, (val) => {
if (val.routesList.routesList.length === state.menuList.length) return false;
if (val.routesList.routesList.length === state.menuList.length) {
return false;
}
let { layout, isClassicSplitMenu } = val.themeConfig.themeConfig;
if (layout === 'classic' && isClassicSplitMenu) return false;
if (layout === 'classic' && isClassicSplitMenu) {
return;
}
setFilterRoutes();
});
// 监听经典布局分割菜单的变化
watch(
() => themeConfig.value.isClassicSplitMenu,
() => {
// 当经典布局分割菜单选项变化时,重新设置过滤路由
setFilterRoutes();
}
);
// 页面加载前
onBeforeMount(() => {
initMenuFixed(document.body.clientWidth);
setFilterRoutes();
mittBus.on('setSendColumnsChildren', (res: any) => {
state.menuList = res.children;
});
mittBus.on('setSendClassicChildren', (res: any) => {
let { layout, isClassicSplitMenu } = themeConfig.value;
if (layout === 'classic' && isClassicSplitMenu) {
state.menuList = [];
state.menuList = res.children;
}
});
mittBus.on('getBreadcrumbIndexSetFilterRoutes', () => {
setFilterRoutes();
});
mittBus.on('layoutMobileResize', (res: any) => {
initMenuFixed(res.clientWidth);
});
});
// 页面卸载时
onUnmounted(() => {
mittBus.off('setSendColumnsChildren');
mittBus.off('setSendClassicChildren');
mittBus.off('getBreadcrumbIndexSetFilterRoutes');
mittBus.off('layoutMobileResize');
if (columnsMenuData) {
watch(columnsMenuData, (newVal) => {
if (newVal) {
state.menuList = newVal.children;
}
});
}
if (classicMenuData) {
watch(classicMenuData, (newVal) => {
let { layout, isClassicSplitMenu } = themeConfig.value;
if (newVal && layout === 'classic' && isClassicSplitMenu) {
state.menuList = [];
state.menuList = newVal.children;
}
});
}
});
</script>

View File

@@ -1,7 +1,7 @@
<template>
<div class="layout-columns-aside">
<div class="w-[64px] h-full bg-[var(--bg-columnsMenuBar)]">
<el-scrollbar>
<ul>
<ul class="relative">
<li
v-for="(v, k) in state.columnsAsideList"
:key="k"
@@ -11,63 +11,86 @@
if (el) columnsAsideOffsetTopRefs[k] = el;
}
"
:class="{ 'layout-columns-active': state.liIndex === k }"
:class="[
{ 'text-white': state.liIndex === k },
'color-[var(--bg-columnsMenuBarColor)] w-full h-[50px] text-center flex cursor-pointer relative z-[1] transition-[color] duration-300 ease-in-out',
]"
:title="$t(v.meta.title)"
>
<div class="layout-columns-aside-li-box" v-if="!v.meta.link || (v.meta.link && v.meta.linkType == 1)">
<div class="mx-auto my-auto" v-if="!v.meta.link || (v.meta.link && v.meta.linkType == 1)">
<i :class="v.meta.icon"></i>
<div class="layout-columns-aside-li-box-title !text-[12px]">
{{ $t(v.meta.title) && $t(v.meta.title).length >= 4 ? $t(v.meta.title).substr(0, 4) : $t(v.meta.title) }}
<div class="pt-[1px] !text-[12px]">
{{ $t(v.meta.title) && $t(v.meta.title).length >= 4 ? $t(v.meta.title).substring(0, 4) : $t(v.meta.title) }}
</div>
</div>
<div class="layout-columns-aside-li-box" v-else>
<a :href="v.meta.link" target="_blank">
<div class="mx-auto my-auto" v-else>
<a :href="v.meta.link" target="_blank" class="no-underline color-[var(--bg-columnsMenuBarColor)]">
<i :class="v.meta.icon"></i>
<div class="layout-columns-aside-li-box-title !text-[12px]">
{{ $t(v.meta.title) && $t(v.meta.title).length >= 4 ? $t(v.meta.title).substr(0, 4) : $t(v.meta.title) }}
<div class="pt-[1px] !text-[12px]">
{{ $t(v.meta.title) && $t(v.meta.title).length >= 4 ? $t(v.meta.title).substring(0, 4) : $t(v.meta.title) }}
</div>
</a>
</div>
</li>
<div ref="columnsAsideActiveRef" :class="setColumnsAsideStyle"></div>
<div
ref="columnsAsideActiveRef"
:class="[
'absolute z-[0] bg-[var(--el-color-primary)] text-white transition-all duration-300 ease-in-out',
setColumnsAsideStyle === 'columnsRound'
? 'left-1/2 top-[2px] h-[44px] w-[58px] -translate-x-1/2 rounded-[5px]'
: 'left-0 top-0 h-[50px] w-full rounded-[0]',
]"
></div>
</ul>
</el-scrollbar>
</div>
</template>
<script lang="ts" setup name="layoutColumnsAside">
import { reactive, ref, computed, onMounted, nextTick, getCurrentInstance, watch } from 'vue';
import { reactive, ref, computed, onMounted, nextTick, watch, inject } from 'vue';
import { useRoute, useRouter, onBeforeRouteUpdate } from 'vue-router';
import pinia from '@/store/index';
import { useThemeConfig } from '@/store/themeConfig';
import { useRoutesList } from '@/store/routesList';
import mittBus from '@/common/utils/mitt';
const columnsAsideOffsetTopRefs: any = ref([]);
const columnsAsideActiveRef = ref();
const route = useRoute();
const router = useRouter();
const state: any = reactive({
columnsAsideList: [],
const state = reactive({
columnsAsideList: [] as any[],
liIndex: 0,
difference: 0,
routeSplit: [],
routeSplit: [] as any[],
});
// 注入 columnsMenuData
const columnsMenuData: any = inject('columnsMenuData');
// 设置高亮样式
const setColumnsAsideStyle = computed(() => {
return useThemeConfig().themeConfig.columnsAsideStyle;
});
// 设置菜单高亮位置移动
const setColumnsAsideMove = (k: number) => {
state.liIndex = k;
columnsAsideActiveRef.value.style.top = `${columnsAsideOffsetTopRefs.value[k].offsetTop + state.difference}px`;
};
// 菜单高亮点击事件
const onColumnsAsideMenuClick = (v: Object, k: number) => {
const onColumnsAsideMenuClick = (v: any, k: number) => {
setColumnsAsideMove(k);
let { path, redirect } = v as any;
if (redirect) router.push(redirect);
else router.push(path);
if (v.children && v.children.length > 0) {
router.push(v.children[0].path);
} else {
router.push(v.path);
}
// if (redirect) {
// router.push(redirect);
// } else {
// router.push(path);
// }
};
// 设置高亮动态位置
const onColumnsAsideDown = (k: number) => {
@@ -80,119 +103,97 @@ const setFilterRoutes = () => {
state.columnsAsideList = filterRoutesFun(useRoutesList().routesList);
const resData: any = setSendChildren(route.path);
onColumnsAsideDown(resData.item[0].k);
mittBus.emit('setSendColumnsChildren', resData);
nextTick(() => {
setTimeout(() => {
if (columnsMenuData) {
columnsMenuData.value = resData;
}
}, 300);
});
};
// 传送当前子级数据到菜单中
const setSendChildren = (path: string) => {
const currentPathSplit = path.split('/');
let currentData: any = {};
state.columnsAsideList.map((v: any, k: number) => {
if (v.path === `/${currentPathSplit[1]}`) {
v['k'] = k;
currentData['item'] = [{ ...v }];
currentData['children'] = [{ ...v }];
if (v.children) currentData['children'] = v.children;
const result = findRootRoute(state.columnsAsideList, path);
if (result) {
const k = state.columnsAsideList.findIndex((v: any) => v === result);
if (k !== -1) {
result['k'] = k;
currentData['item'] = [{ ...result }];
currentData['children'] = [{ ...result }];
if (result.children) currentData['children'] = result.children;
}
});
}
return currentData;
};
// 路由过滤递归函数
const filterRoutesFun = (arr: Array<object>) => {
return arr
.filter((item: any) => !item.meta.isHide)
.map((item: any) => {
item = Object.assign({}, item);
if (item.children) item.children = filterRoutesFun(item.children);
if (item.children) {
item.children = filterRoutesFun(item.children);
}
return item;
});
};
// tagsView 点击时,根据路由查找下标 columnsAsideList实现左侧菜单高亮
const setColumnsMenuHighlight = (path: string) => {
state.routeSplit = path.split('/');
state.routeSplit.shift();
const routeFirst = `/${state.routeSplit[0]}`;
const currentSplitRoute = state.columnsAsideList.find((v: any) => v.path === routeFirst);
// 延迟拿值,防止取不到
setTimeout(() => {
onColumnsAsideDown(currentSplitRoute.k);
}, 0);
const rootRoute = findRootRoute(state.columnsAsideList, path);
if (rootRoute) {
// 延迟拿值,防止取不到
setTimeout(() => {
onColumnsAsideDown(rootRoute.k);
}, 0);
}
};
// 递归查找路由并返回根节点
const findRootRoute = (routes: any[], currentPath: string): any => {
for (const route of routes) {
// 直接匹配
if (route.path === currentPath) {
return route;
}
// 在子路由中查找
if (route.children && route.children.length > 0) {
const found = findRootRoute(route.children, currentPath);
if (found) {
// 如果在子路由中找到了,返回根节点
return route;
}
}
}
return null;
};
// 监听路由的变化,动态赋值给菜单中
watch(pinia.state, (val) => {
val.themeConfig.themeConfig.columnsAsideStyle === 'columnsRound' ? (state.difference = 3) : (state.difference = 0);
if (val.routesList.routesList.length === state.columnsAsideList.length) return false;
if (val.routesList.routesList.length === state.columnsAsideList.length) {
return;
}
setFilterRoutes();
});
// 页面加载时
onMounted(() => {
setFilterRoutes();
});
// 路由更新时
onBeforeRouteUpdate((to) => {
setColumnsMenuHighlight(to.path);
mittBus.emit('setSendColumnsChildren', setSendChildren(to.path));
if (columnsMenuData) {
columnsMenuData.value = setSendChildren(to.path);
}
});
</script>
<style scoped lang="scss">
.layout-columns-aside {
width: 64px;
height: 100%;
background: var(--bg-columnsMenuBar);
ul {
position: relative;
li {
color: var(--bg-columnsMenuBarColor);
width: 100%;
height: 50px;
text-align: center;
display: flex;
cursor: pointer;
position: relative;
z-index: 1;
.layout-columns-aside-li-box {
margin: auto;
.layout-columns-aside-li-box-title {
padding-top: 1px;
}
}
a {
text-decoration: none;
color: var(--bg-columnsMenuBarColor);
}
}
.layout-columns-active {
color: #ffffff;
transition: 0.3s ease-in-out;
}
.columns-round {
background: var(--el-color-primary);
color: #ffffff;
position: absolute;
left: 50%;
top: 2px;
height: 44px;
width: 58px;
transform: translateX(-50%);
z-index: 0;
transition: 0.3s ease-in-out;
border-radius: 5px;
}
.columns-card {
@extend .columns-round;
top: 0;
height: 50px;
width: 100%;
border-radius: 0;
}
}
}
</style>

View File

@@ -1,26 +1,11 @@
<template>
<el-header class="layout-header" :height="setHeaderHeight">
<el-header class="layout-header">
<NavBarsIndex />
</el-header>
</template>
<script lang="ts">
import { computed } from 'vue';
import NavBarsIndex from '@/layout/navBars/index.vue';
import { useThemeConfig } from '@/store/themeConfig';
export default {
name: 'layoutHeader',
components: { NavBarsIndex },
setup() {
// 设置 header 的高度
const setHeaderHeight = computed(() => {
let { isTagsview, layout } = useThemeConfig().themeConfig;
if (isTagsview && layout !== 'classic') return '84px';
else return '50px';
});
return {
setHeaderHeight,
};
},
};
<script setup lang="ts" name="layoutHeader">
import { defineAsyncComponent } from 'vue';
const NavBarsIndex = defineAsyncComponent(() => import('@/layout/navBars/index.vue'));
</script>

View File

@@ -1,17 +1,10 @@
<template>
<el-main class="layout-main !h-full">
<el-scrollbar ref="layoutScrollbarRef" view-class="!h-full" v-show="!state.currentRouteMeta.link && state.currentRouteMeta.linkType != 1">
<el-main class="layout-main h-full">
<el-scrollbar ref="layoutScrollbarRef" view-class="h-full">
<LayoutParentView />
</el-scrollbar>
<Link class="!h-full" :meta="state.currentRouteMeta" v-if="state.currentRouteMeta.link && state.currentRouteMeta.linkType == 2" />
<Iframes
class="!h-full"
:meta="state.currentRouteMeta"
v-if="state.currentRouteMeta.link && state.currentRouteMeta.linkType == 1 && state.isShowLink"
@getCurrentRouteMeta="onGetCurrentRouteMeta"
/>
<el-backtop target=".layout-backtop .el-main .el-scrollbar__wrap"></el-backtop>
</el-main>
<el-footer v-if="themeConfig.isFooter">
@@ -20,52 +13,41 @@
</template>
<script setup lang="ts" name="layoutMain">
import { reactive, getCurrentInstance, watch, onBeforeMount } from 'vue';
import { watch, defineAsyncComponent, useTemplateRef, nextTick, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
import LayoutParentView from '@/layout/routerView/parent.vue';
import Footer from '@/layout/footer/index.vue';
import Link from '@/layout/routerView/link.vue';
import Iframes from '@/layout/routerView/iframes.vue';
const { proxy } = getCurrentInstance() as any;
const LayoutParentView = defineAsyncComponent(() => import('@/layout/routerView/parent.vue'));
const Footer = defineAsyncComponent(() => import('@/layout/footer/index.vue'));
const layoutScrollbarRef = useTemplateRef('layoutScrollbarRef');
const { themeConfig } = storeToRefs(useThemeConfig());
const route = useRoute();
const state = reactive({
currentRouteMeta: {} as any,
isShowLink: false,
});
// 子组件触发更新
const onGetCurrentRouteMeta = () => {
initCurrentRouteMeta(route.meta);
};
// 初始化当前路由 meta 信息
const initCurrentRouteMeta = (meta: object) => {
state.isShowLink = false;
state.currentRouteMeta = meta;
setTimeout(() => {
state.isShowLink = true;
}, 100);
};
// 页面加载前
onBeforeMount(() => {
initCurrentRouteMeta(route.meta);
});
// 监听 themeConfig 配置文件的变化,更新菜单 el-scrollbar 的高度
watch(themeConfig.value, (val) => {
if (val.isFixedHeaderChange !== val.isFixedHeader) {
if (!proxy.$refs.layoutScrollbarRef) return false;
proxy.$refs.layoutScrollbarRef.update();
if (!layoutScrollbarRef.value) {
return;
}
layoutScrollbarRef.value.update();
}
});
// 监听路由的变化
watch(
() => route.path,
() => {
initCurrentRouteMeta(route.meta);
proxy.$refs.layoutScrollbarRef.wrapRef.scrollTop = 0;
nextTick(() => {
if (!layoutScrollbarRef.value) {
return;
}
setTimeout(() => {
layoutScrollbarRef.value.update();
}, 500);
layoutScrollbarRef.value.setScrollTop();
});
}
);
</script>

View File

@@ -1,47 +1,18 @@
<template>
<Defaults v-if="themeConfig.layout === 'defaults'" />
<Classic v-else-if="themeConfig.layout === 'classic'" />
<Transverse v-else-if="themeConfig.layout === 'transverse'" />
<Columns v-else-if="themeConfig.layout === 'columns'" />
<component :is="layouts[themeConfig.layout]" />
</template>
<script setup lang="ts" name="layout">
import { onBeforeMount, onUnmounted } from 'vue';
import { getLocal, setLocal } from '@/common/utils/storage';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
import Defaults from '@/layout/main/defaults.vue';
import Classic from '@/layout/main/classic.vue';
import Transverse from '@/layout/main/transverse.vue';
import Columns from '@/layout/main/columns.vue';
import mittBus from '@/common/utils/mitt';
import { defineAsyncComponent } from 'vue';
const layouts: any = {
defaults: defineAsyncComponent(() => import('@/layout/main/defaults.vue')),
classic: defineAsyncComponent(() => import('@/layout/main/classic.vue')),
transverse: defineAsyncComponent(() => import('@/layout/main/transverse.vue')),
columns: defineAsyncComponent(() => import('@/layout/main/columns.vue')),
};
const { themeConfig } = storeToRefs(useThemeConfig());
// 窗口大小改变时(适配移动端)
const onLayoutResize = () => {
if (!getLocal('oldLayout')) setLocal('oldLayout', themeConfig.value.layout);
const clientWidth = document.body.clientWidth;
if (clientWidth < 1000) {
themeConfig.value.isCollapse = false;
mittBus.emit('layoutMobileResize', {
layout: 'defaults',
clientWidth,
});
} else {
mittBus.emit('layoutMobileResize', {
layout: getLocal('oldLayout') ? getLocal('oldLayout') : 'defaults',
clientWidth,
});
}
};
// 页面加载前
onBeforeMount(() => {
onLayoutResize();
window.addEventListener('resize', onLayoutResize);
});
// 页面卸载时
onUnmounted(() => {
window.removeEventListener('resize', onLayoutResize);
});
</script>

View File

@@ -1,352 +0,0 @@
<template>
<div v-show="state.isShowLockScreen">
<div class="layout-lock-screen-mask"></div>
<div class="layout-lock-screen-img" :class="{ 'layout-lock-screen-filter': state.isShowLoockLogin }"></div>
<div class="layout-lock-screen">
<div
class="layout-lock-screen-date"
ref="layoutLockScreenDateRef"
@mousedown="onDownPc"
@mousemove="onMovePc"
@mouseup="onEnd"
@touchstart.stop="onDownApp"
@touchmove.stop="onMoveApp"
@touchend.stop="onEnd"
>
<div class="layout-lock-screen-date-box">
<div class="layout-lock-screen-date-box-time">
{{ state.time.hm }}<span class="layout-lock-screen-date-box-minutes">{{ state.time.s }}</span>
</div>
<div class="layout-lock-screen-date-box-info">{{ state.time.mdq }}</div>
</div>
<div class="layout-lock-screen-date-top">
<SvgIcon name="ele-Top" />
<div class="layout-lock-screen-date-top-text">上滑解锁</div>
</div>
</div>
<transition name="el-zoom-in-center">
<div v-show="state.isShowLoockLogin" class="layout-lock-screen-login">
<div class="layout-lock-screen-login-box">
<div class="layout-lock-screen-login-box-img">
<img src="https://img2.baidu.com/it/u=1978192862,2048448374&fm=253&fmt=auto&app=138&f=JPEG?w=504&h=500" />
</div>
<div class="layout-lock-screen-login-box-name">Administrator</div>
<div class="layout-lock-screen-login-box-value">
<el-input
placeholder="请输入密码"
ref="layoutLockScreenInputRef"
v-model="state.lockScreenPassword"
@keyup.enter.native.stop="onLockScreenSubmit()"
>
<template #append>
<el-button @click="onLockScreenSubmit">
<el-icon class="el-input__icon">
<ele-Right />
</el-icon>
</el-button>
</template>
</el-input>
</div>
</div>
<div class="layout-lock-screen-login-icon">
<SvgIcon name="ele-Microphone" :size="20" />
<SvgIcon name="ele-AlarmClock" :size="20" />
<SvgIcon name="ele-SwitchButton" :size="20" />
</div>
</div>
</transition>
</div>
</div>
</template>
<script setup lang="ts" name="layoutLockScreen">
import { nextTick, onMounted, reactive, ref, onUnmounted } from 'vue';
import { formatDate } from '@/common/utils/format';
import { setLocal } from '@/common/utils/storage';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
// 定义变量内容
const layoutLockScreenDateRef = ref<any>();
const layoutLockScreenInputRef = ref();
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
const state = reactive({
transparency: 1,
downClientY: 0,
moveDifference: 0,
isShowLoockLogin: false,
isFlags: false,
querySelectorEl: '' as any,
time: {
hm: '',
s: '',
mdq: '',
},
setIntervalTime: 0,
isShowLockScreen: false,
isShowLockScreenIntervalTime: 0,
lockScreenPassword: '',
});
// 鼠标按下 pc
const onDownPc = (down: MouseEvent) => {
state.isFlags = true;
state.downClientY = down.clientY;
};
// 鼠标按下 app
const onDownApp = (down: TouchEvent) => {
state.isFlags = true;
state.downClientY = down.touches[0].clientY;
};
// 鼠标移动 pc
const onMovePc = (move: MouseEvent) => {
state.moveDifference = move.clientY - state.downClientY;
onMove();
};
// 鼠标移动 app
const onMoveApp = (move: TouchEvent) => {
state.moveDifference = move.touches[0].clientY - state.downClientY;
onMove();
};
// 鼠标移动事件
const onMove = () => {
if (state.isFlags) {
const el = <HTMLElement>state.querySelectorEl;
const opacitys = (state.transparency -= 1 / 200);
if (state.moveDifference >= 0) return false;
el.setAttribute('style', `top:${state.moveDifference}px;cursor:pointer;opacity:${opacitys};`);
if (state.moveDifference < -400) {
el.setAttribute('style', `top:${-el.clientHeight}px;cursor:pointer;transition:all 0.3s ease;`);
state.moveDifference = -el.clientHeight;
setTimeout(() => {
el && el.parentNode?.removeChild(el);
}, 300);
}
if (state.moveDifference === -el.clientHeight) {
state.isShowLoockLogin = true;
layoutLockScreenInputRef.value.focus();
}
}
};
// 鼠标松开
const onEnd = () => {
state.isFlags = false;
state.transparency = 1;
if (state.moveDifference >= -400) {
(<HTMLElement>state.querySelectorEl).setAttribute('style', `top:0px;opacity:1;transition:all 0.3s ease;`);
}
};
// 获取要拖拽的初始元素
const initGetElement = () => {
nextTick(() => {
state.querySelectorEl = layoutLockScreenDateRef.value;
});
};
// 时间初始化
const initTime = () => {
state.time.hm = formatDate(new Date(), 'HH:MM');
state.time.s = formatDate(new Date(), 'SS');
state.time.mdq = formatDate(new Date(), 'mm月dd日WWW');
};
// 时间初始化定时器
const initSetTime = () => {
initTime();
state.setIntervalTime = window.setInterval(() => {
initTime();
}, 1000);
};
// 锁屏时间定时器
const initLockScreen = () => {
if (themeConfig.value.isLockScreen) {
state.isShowLockScreenIntervalTime = window.setInterval(() => {
if (themeConfig.value.lockScreenTime <= 1) {
state.isShowLockScreen = true;
setLocalThemeConfig();
return false;
}
themeConfig.value.lockScreenTime--;
}, 1000);
} else {
clearInterval(state.isShowLockScreenIntervalTime);
}
};
// 存储布局配置
const setLocalThemeConfig = () => {
themeConfig.value.isDrawer = false;
setLocal('themeConfig', themeConfig.value);
};
// 密码输入点击事件
const onLockScreenSubmit = () => {
themeConfig.value.isLockScreen = false;
themeConfig.value.lockScreenTime = 30;
setLocalThemeConfig();
};
// 页面加载时
onMounted(() => {
initGetElement();
initSetTime();
initLockScreen();
});
// 页面卸载时
onUnmounted(() => {
window.clearInterval(state.setIntervalTime);
window.clearInterval(state.isShowLockScreenIntervalTime);
});
</script>
<style scoped lang="scss">
.layout-lock-screen-fixed {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.layout-lock-screen-filter {
filter: blur(1px);
}
.layout-lock-screen-mask {
background: var(--el-color-white);
@extend .layout-lock-screen-fixed;
z-index: 9999990;
}
.layout-lock-screen-img {
@extend .layout-lock-screen-fixed;
background: url('@/assets/image/login-bg-main.svg') no-repeat;
background-size: 100% 100%;
z-index: 9999991;
}
.layout-lock-screen {
@extend .layout-lock-screen-fixed;
z-index: 9999992;
&-date {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
color: var(--el-color-white);
z-index: 9999993;
user-select: none;
&-box {
position: absolute;
left: 30px;
bottom: 50px;
&-time {
font-size: 100px;
color: var(--el-color-white);
}
&-info {
font-size: 40px;
color: var(--el-color-white);
}
&-minutes {
font-size: 16px;
}
}
&-top {
width: 40px;
height: 40px;
line-height: 40px;
border-radius: 100%;
border: 1px solid var(--el-border-color-light, #ebeef5);
background: rgba(255, 255, 255, 0.1);
color: var(--el-color-white);
opacity: 0.8;
position: absolute;
right: 30px;
bottom: 50px;
text-align: center;
overflow: hidden;
transition: all 0.3s ease;
i {
transition: all 0.3s ease;
}
&-text {
opacity: 0;
position: absolute;
top: 150%;
font-size: 12px;
color: var(--el-color-white);
left: 50%;
line-height: 1.2;
transform: translate(-50%, -50%);
transition: all 0.3s ease;
width: 35px;
}
&:hover {
border: 1px solid rgba(255, 255, 255, 0.5);
background: rgba(255, 255, 255, 0.2);
box-shadow: 0 0 12px 0 rgba(255, 255, 255, 0.5);
color: var(--el-color-white);
opacity: 1;
transition: all 0.3s ease;
i {
transform: translateY(-40px);
transition: all 0.3s ease;
}
.layout-lock-screen-date-top-text {
opacity: 1;
top: 50%;
transition: all 0.3s ease;
}
}
}
}
&-login {
position: relative;
z-index: 9999994;
width: 100%;
height: 100%;
left: 0;
top: 0;
display: flex;
flex-direction: column;
justify-content: center;
color: var(--el-color-white);
&-box {
text-align: center;
margin: auto;
&-img {
width: 180px;
height: 180px;
margin: auto;
img {
width: 100%;
height: 100%;
border-radius: 100%;
}
}
&-name {
font-size: 26px;
margin: 15px 0 30px;
}
}
&-icon {
position: absolute;
right: 30px;
bottom: 30px;
i {
font-size: 20px;
margin-left: 15px;
cursor: pointer;
opacity: 0.8;
&:hover {
opacity: 1;
}
}
}
}
}
:deep(.el-input-group__append) {
background: var(--el-color-white);
padding: 0px 15px;
}
:deep(.el-input__inner) {
border-right-color: var(--el-border-color-extra-light);
&:hover {
border-color: var(--el-border-color-extra-light);
}
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<div class="layout-logo" v-if="setShowLogo" @click="onThemeConfigChange">
<img :src="themeConfig.logoIcon" class="layout-logo-medium-img" />
<span>
<span class="logo-title">
{{ `${themeConfig.globalTitle}` }}
<sub
><span style="font-size: 10px; color: goldenrod">{{ ` ${config.version}` }}</span></sub
@@ -18,7 +18,6 @@ import { computed } from 'vue';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
import config from '@/common/config';
import mittBus from '@/common/utils/mitt';
const { themeConfig } = storeToRefs(useThemeConfig());
@@ -30,7 +29,6 @@ const setShowLogo = computed(() => {
// logo 点击实现菜单展开/收起
const onThemeConfigChange = () => {
if (themeConfig.value.layout === 'transverse') return false;
mittBus.emit('onMenuClick');
themeConfig.value.isCollapse = !themeConfig.value.isCollapse;
};
</script>
@@ -55,8 +53,17 @@ const onThemeConfigChange = () => {
}
&-medium-img {
width: 20px;
margin-right: 5px;
width: 24px;
height: 24px;
margin-right: 8px;
}
.logo-title {
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: calc(100% - 32px);
}
}
@@ -66,10 +73,12 @@ const onThemeConfigChange = () => {
display: flex;
cursor: pointer;
animation: logoAnimation 0.3s ease-in-out;
justify-content: center;
align-items: center;
&-img {
width: 20px;
margin: auto;
width: 24px;
height: 24px;
}
&:hover {

View File

@@ -8,17 +8,22 @@
<Main />
</div>
</el-container>
<el-backtop target=".layout-backtop .el-main .el-scrollbar__wrap"></el-backtop>
</el-container>
</template>
<script lang="ts" setup name="layoutClassic">
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
import Aside from '@/layout/component/aside.vue';
import Header from '@/layout/component/header.vue';
import Main from '@/layout/component/main.vue';
import TagsView from '@/layout/navBars/tagsView/tagsView.vue';
import { defineAsyncComponent, provide, ref } from 'vue';
const Aside = defineAsyncComponent(() => import('@/layout/component/aside.vue'));
const Header = defineAsyncComponent(() => import('@/layout/component/header.vue'));
const Main = defineAsyncComponent(() => import('@/layout/component/main.vue'));
const TagsView = defineAsyncComponent(() => import('@/layout/navBars/tagsView/tagsView.vue'));
const { themeConfig } = storeToRefs(useThemeConfig());
// 提供 classic 布局的菜单数据
const classicMenuData = ref<any>(null);
provide('classicMenuData', classicMenuData);
</script>

View File

@@ -9,18 +9,22 @@
<Main />
</el-container>
</div>
<el-backtop target=".layout-backtop .el-scrollbar__wrap"></el-backtop>
</el-container>
</template>
<script lang="ts" setup name="layoutColumns">
import { computed } from 'vue';
import Aside from '@/layout/component/aside.vue';
import Header from '@/layout/component/header.vue';
import Main from '@/layout/component/main.vue';
import ColumnsAside from '@/layout/component/columnsAside.vue';
import { computed, defineAsyncComponent, provide, ref } from 'vue';
import { useThemeConfig } from '@/store/themeConfig';
const Aside = defineAsyncComponent(() => import('@/layout/component/aside.vue'));
const Header = defineAsyncComponent(() => import('@/layout/component/header.vue'));
const Main = defineAsyncComponent(() => import('@/layout/component/main.vue'));
const ColumnsAside = defineAsyncComponent(() => import('@/layout/component/columnsAside.vue'));
// 提供响应式数据给子组件
const columnsMenuData = ref<any>(null);
provide('columnsMenuData', columnsMenuData);
const isFixedHeader = computed(() => {
return useThemeConfig().themeConfig.isFixedHeader;
});

View File

@@ -6,18 +6,18 @@
<Header v-if="!isFixedHeader" />
<Main />
</el-container>
<el-backtop target=".layout-backtop .el-scrollbar__wrap"></el-backtop>
</el-container>
</template>
<script lang="ts" setup name="layoutDefaults">
import { computed, getCurrentInstance, watch } from 'vue';
import { computed, defineAsyncComponent, getCurrentInstance, watch } from 'vue';
import { useRoute } from 'vue-router';
import Aside from '@/layout/component/aside.vue';
import Header from '@/layout/component/header.vue';
import Main from '@/layout/component/main.vue';
import { useThemeConfig } from '@/store/themeConfig';
const Aside = defineAsyncComponent(() => import('@/layout/component/aside.vue'));
const Header = defineAsyncComponent(() => import('@/layout/component/header.vue'));
const Main = defineAsyncComponent(() => import('@/layout/component/main.vue'));
const { proxy } = getCurrentInstance() as any;
const route = useRoute();
const isFixedHeader = computed(() => {

View File

@@ -1,12 +1,13 @@
<template>
<el-container class="layout-container flex-center layout-backtop">
<el-container class="layout-container layout-backtop !flex-col">
<Header />
<Main />
<el-backtop target=".layout-backtop .el-main .el-scrollbar__wrap"></el-backtop>
</el-container>
</template>
<script lang="ts" setup name="layoutTransverse">
import Header from '@/layout/component/header.vue';
import Main from '@/layout/component/main.vue';
import { defineAsyncComponent } from 'vue';
const Main = defineAsyncComponent(() => import('@/layout/component/main.vue'));
const Header = defineAsyncComponent(() => import('@/layout/component/header.vue'));
</script>

View File

@@ -1,15 +1,19 @@
<template>
<div class="layout-navbars-breadcrumb" v-show="themeConfig.isBreadcrumb">
<SvgIcon class="layout-navbars-breadcrumb-icon" :name="themeConfig.isCollapse ? 'expand' : 'fold'" @click="onThemeConfigChange" />
<div class="flex flex-1 h-inherit items-center pl-4" v-show="themeConfig.isBreadcrumb">
<SvgIcon
class="cursor-pointer text-18px mr-4 text-[var(--bg-topBarColor)]"
:name="themeConfig.isCollapse ? 'expand' : 'fold'"
@click="onThemeConfigChange"
/>
<el-breadcrumb class="layout-navbars-breadcrumb-hide">
<transition-group name="breadcrumb">
<el-breadcrumb-item v-for="(v, k) in state.breadcrumbList" :key="v.meta.title">
<span v-if="k === state.breadcrumbList.length - 1" class="layout-navbars-breadcrumb-span">
<SvgIcon :name="v.meta.icon" class="layout-navbars-breadcrumb-iconfont" v-if="themeConfig.isBreadcrumbIcon" />
<span v-if="k === state.breadcrumbList.length - 1 || (!v.redirect && !v.component)" class="opacity-70 text-[var(--bg-topBarColor)]">
<SvgIcon :name="v.meta.icon" class="text-14px mr-1.25" v-if="themeConfig.isBreadcrumbIcon" />
{{ $t(v.meta.title) }}
</span>
<a v-else @click.prevent="onBreadcrumbClick(v)">
<SvgIcon :name="v.meta.icon" class="layout-navbars-breadcrumb-iconfont" v-if="themeConfig.isBreadcrumbIcon" />
<a v-else @click.prevent="onBreadcrumbClick(v)" class="opacity-100 text-[var(--bg-topBarColor)] hover:opacity-100">
<SvgIcon :name="v.meta.icon" class="text-14px mr-1.25" v-if="themeConfig.isBreadcrumbIcon" />
{{ $t(v.meta.title) }}
</a>
</el-breadcrumb-item>
@@ -24,91 +28,104 @@ import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
import { useRoutesList } from '@/store/routesList';
import mittBus from '@/common/utils/mitt';
const { themeConfig } = storeToRefs(useThemeConfig());
const { routesList } = storeToRefs(useRoutesList());
const route = useRoute();
const router = useRouter();
const state: any = reactive({
breadcrumbList: [],
routeSplit: [],
routeSplitFirst: '',
routeSplitIndex: 1,
const state = reactive({
breadcrumbList: [] as any[],
});
// 面包屑点击时
const onBreadcrumbClick = (v: any) => {
const { redirect, path } = v;
if (redirect) router.push(redirect);
else router.push(path);
if (redirect) {
router.push(redirect);
return;
}
if (v.component) {
router.push(path);
}
};
// 展开/收起左侧菜单点击
const onThemeConfigChange = () => {
mittBus.emit('onMenuClick');
themeConfig.value.isCollapse = !themeConfig.value.isCollapse;
};
// 处理面包屑数据
const getBreadcrumbList = (arr: Array<object>) => {
arr.map((item: any) => {
state.routeSplit.map((v: any, k: number, arrs: any) => {
if (state.routeSplitFirst === item.path) {
state.routeSplitFirst += `/${arrs[state.routeSplitIndex]}`;
state.breadcrumbList.push(item);
state.routeSplitIndex++;
if (item.children) getBreadcrumbList(item.children);
// 根据当前路径生成面包屑列表
const generateBreadcrumbList = (currentPath: string) => {
if (!themeConfig.value.isBreadcrumb) {
return;
}
// 初始化面包屑列表,包含首页
const homeRoute = routesList.value.length > 0 ? routesList.value[0] : null;
const breadcrumbList = homeRoute ? [homeRoute] : [];
// 查找匹配的路由及其所有父级路由(除了首页)
if (homeRoute && currentPath !== homeRoute.path) {
const matchedRoutes = findMatchedRoutes(routesList.value, currentPath);
// 如果找到匹配的路由,添加到面包屑列表中(排除首页,避免重复)
if (matchedRoutes.length > 0) {
// 过滤掉首页路由,避免重复添加
const filteredRoutes = matchedRoutes.filter((r) => r !== homeRoute);
breadcrumbList.push(...filteredRoutes);
}
}
state.breadcrumbList = breadcrumbList;
};
// 在路由树中查找匹配当前路径的路由,并返回该路由及其所有父级路由
const findMatchedRoutes = (routes: any[], currentPath: string): any[] => {
for (const route of routes) {
// 精确匹配
if (route.path === currentPath) {
return [route];
}
// 前缀匹配且有子路由
if (currentPath.startsWith(route.path + '/') && route.children) {
const matchedChildren = findMatchedRoutes(route.children, currentPath);
if (matchedChildren.length > 0) {
return [route, ...matchedChildren];
}
});
});
};
// 当前路由字符串切割成数组,并删除第一项空内容
const initRouteSplit = (path: string) => {
if (!themeConfig.value.isBreadcrumb) return false;
state.breadcrumbList = [routesList.value[0]];
state.routeSplit = path.split('/');
state.routeSplit.shift();
state.routeSplitFirst = `/${state.routeSplit[0]}`;
state.routeSplitIndex = 1;
getBreadcrumbList(routesList.value);
}
// 处理子路由匹配但当前路由是根路径的情况
if (route.path === '/' && route.children) {
const matchedChildren = findMatchedRoutes(route.children, currentPath);
if (matchedChildren.length > 0) {
return [route, ...matchedChildren];
}
}
// 递归查找子路由
if (route.children) {
const matchedChildren = findMatchedRoutes(route.children, currentPath);
if (matchedChildren.length > 0) {
return [route, ...matchedChildren];
}
}
}
return [];
};
// 页面加载时
onMounted(() => {
initRouteSplit(route.path);
generateBreadcrumbList(route.path);
});
// 路由更新时
onBeforeRouteUpdate((to) => {
initRouteSplit(to.path);
generateBreadcrumbList(to.path);
});
</script>
<style scoped lang="scss">
.layout-navbars-breadcrumb {
flex: 1;
height: inherit;
display: flex;
align-items: center;
padding-left: 15px;
.layout-navbars-breadcrumb-icon {
cursor: pointer;
font-size: 18px;
margin-right: 15px;
color: var(--bg-topBarColor);
}
.layout-navbars-breadcrumb-span {
opacity: 0.7;
color: var(--bg-topBarColor);
}
.layout-navbars-breadcrumb-iconfont {
font-size: 14px;
margin-right: 5px;
}
::v-deep(.el-breadcrumb__separator) {
opacity: 0.7;
color: var(--bg-topBarColor);
}
<style scoped>
::v-deep(.el-breadcrumb__separator) {
opacity: 0.7;
color: var(--bg-topBarColor);
}
</style>

View File

@@ -8,17 +8,17 @@
</template>
<script lang="ts" setup name="layoutBreadcrumbIndex">
import { computed, reactive, onMounted, onUnmounted, watch } from 'vue';
import { computed, reactive, onMounted, watch, defineAsyncComponent } from 'vue';
import { useRoute } from 'vue-router';
import pinia from '@/store/index';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
import { useRoutesList } from '@/store/routesList';
import Breadcrumb from '@/layout/navBars/breadcrumb/breadcrumb.vue';
import User from '@/layout/navBars/breadcrumb/user.vue';
import Logo from '@/layout/logo/index.vue';
import Horizontal from '@/layout/navMenu/horizontal.vue';
import mittBus from '@/common/utils/mitt';
const Breadcrumb = defineAsyncComponent(() => import('@/layout/navBars/breadcrumb/breadcrumb.vue'));
const User = defineAsyncComponent(() => import('@/layout/navBars/breadcrumb/user.vue'));
const Logo = defineAsyncComponent(() => import('@/layout/logo/index.vue'));
const Horizontal = defineAsyncComponent(() => import('@/layout/navMenu/horizontal.vue'));
const { themeConfig } = storeToRefs(useThemeConfig());
const { routesList } = storeToRefs(useRoutesList());
@@ -42,8 +42,6 @@ const setFilterRoutes = () => {
let { layout, isClassicSplitMenu } = themeConfig.value;
if (layout === 'classic' && isClassicSplitMenu) {
state.menuList = delClassicChildren(filterRoutesFun(routesList.value));
const resData = setSendClassicChildren(route.path);
mittBus.emit('setSendClassicChildren', resData);
} else {
state.menuList = filterRoutesFun(routesList.value);
}
@@ -87,13 +85,6 @@ watch(pinia.state, (val) => {
// 页面加载时
onMounted(() => {
setFilterRoutes();
mittBus.on('getBreadcrumbIndexSetFilterRoutes', () => {
setFilterRoutes();
});
});
// 页面卸载时
onUnmounted(() => {
mittBus.off('getBreadcrumbIndexSetFilterRoutes');
});
</script>

View File

@@ -134,15 +134,6 @@
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex mt14">
<div class="layout-breadcrumb-seting-bar-flex-label">
{{ $t('layout.config.menuBarActiveColor') }}
</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-switch v-model="themeConfig.isMenuBarColorHighlight" @change="onMenuBarHighlightChange"></el-switch>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex mt14">
<div class="layout-breadcrumb-seting-bar-flex-label">
{{ $t('layout.config.isMenuBarColorGradual') }}
@@ -236,23 +227,6 @@
</el-switch>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex !mt-3.5">
<div class="layout-breadcrumb-seting-bar-flex-label">
{{ $t('layout.config.isLockScreen') }}
</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-switch v-model="themeConfig.isLockScreen"></el-switch>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex mt11">
<div class="layout-breadcrumb-seting-bar-flex-label">
{{ $t('layout.config.lockScreenTime') }}
</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-input-number v-model="themeConfig.lockScreenTime" controls-position="right" :min="0" :max="9999" size="small" style="width: 90px">
</el-input-number>
</div>
</div>
<!-- 界面显示 -->
<el-divider content-position="left">{{ $t('layout.config.interfaceDisplay') }}</el-divider>
@@ -309,7 +283,7 @@
{{ $t('layout.config.isSortableTagsView') }}
</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-switch v-model="themeConfig.isSortableTagsView" @change="onSortableTagsViewChange"></el-switch>
<el-switch v-model="themeConfig.isSortableTagsView"></el-switch>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex !mt-3.5">
@@ -451,19 +425,60 @@
</template>
<script lang="ts" setup name="layoutBreadcrumbSeting">
import { nextTick, onUnmounted, onMounted, ref } from 'vue';
import { nextTick, onMounted, ref, watch } from 'vue';
import { ElMessage } from 'element-plus';
import ClipboardJS from 'clipboard';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
import { getLightColor } from '@/common/utils/theme';
import { setLocal, getLocal } from '@/common/utils/storage';
import mittBus from '@/common/utils/mitt';
import themes from '@/components/terminal/themes';
import { useWindowSize } from '@vueuse/core';
const copyConfigBtnRef = ref();
const { themeConfig } = storeToRefs(useThemeConfig()) as any;
// 获取窗口大小
const { width } = useWindowSize();
watch(width, () => {
checkClientWidth();
});
onMounted(() => {
nextTick(() => {
checkClientWidth();
window.addEventListener('load', () => {
// 刷新页面时,设置了值,直接取缓存中的值进行初始化
setTimeout(() => {
// 顶栏背景渐变
if (getLocal('navbarsBgStyle') && themeConfig.value.isTopBarColorGradual) {
const breadcrumbIndexEl: any = document.querySelector('.layout-navbars-breadcrumb-index');
breadcrumbIndexEl.style.cssText = getLocal('navbarsBgStyle');
}
// 菜单背景渐变
if (getLocal('asideBgStyle') && themeConfig.value.isMenuBarColorGradual) {
const asideEl: any = document.querySelector('.layout-container .el-aside');
asideEl.style.cssText = getLocal('asideBgStyle');
}
// 分栏菜单背景渐变
if (getLocal('columnsBgStyle') && themeConfig.value.isColumnsMenuBarColorGradual) {
const asideEl: any = document.querySelector('.layout-container .layout-columns-aside');
asideEl.style.cssText = getLocal('columnsBgStyle');
}
// 灰色模式/色弱模式
if (getLocal('appFilterStyle')) {
const appEl: any = document.querySelector('#app');
appEl.style.cssText = getLocal('appFilterStyle');
}
// // 语言国际化
// if (getLocal('themeConfig')) proxy.$i18n.locale = getLocal('themeConfig').globalI18n;
}, 100);
});
});
});
// 1、全局主题
const onColorPickerChange = (color: string) => {
setPropertyFun(`--color-${color}`, themeConfig.value[color]);
@@ -512,26 +527,9 @@ const setGraduaFun = (el: string, bool: boolean, color: string) => {
if (elColumns) setLocal('columnsBgStyle', elColumns.style.cssText);
});
};
// 2、菜单 / 顶栏 --> 菜单字体背景高亮
const onMenuBarHighlightChange = () => {
nextTick(() => {
setTimeout(() => {
let elsItems = document.querySelectorAll('.el-menu-item');
let elActive = document.querySelector('.el-menu-item.is-active');
if (!elActive) return false;
if (themeConfig.value.isMenuBarColorHighlight) {
elsItems.forEach((el: any) => el.setAttribute('id', ``));
elActive.setAttribute('id', `add-is-active`);
setLocal('menuBarHighlightId', elActive.getAttribute('id'));
} else {
elActive.setAttribute('id', ``);
}
}, 0);
});
};
// 3、界面设置 --> 菜单水平折叠
const onThemeConfigChange = () => {
onMenuBarHighlightChange();
setDispatchThemeConfig();
};
// 3、界面设置 --> 固定 Header
@@ -541,8 +539,6 @@ const onIsFixedHeaderChange = () => {
// 3、界面设置 --> 经典布局分割菜单
const onClassicSplitMenuChange = () => {
themeConfig.value.isBreadcrumb = false;
mittBus.emit('getBreadcrumbIndexSetFilterRoutes');
};
// 4、界面显示 --> 侧边栏 Logo
const onIsShowLogoChange = () => {
@@ -554,10 +550,7 @@ const onIsBreadcrumbChange = () => {
themeConfig.value.isClassicSplitMenu = false;
}
};
// 4、界面显示 --> 开启 TagsView 拖拽
const onSortableTagsViewChange = () => {
mittBus.emit('openOrCloseSortable');
};
// 4、界面显示 --> 暗模式/灰色模式/色弱模式
const onAddFilterChange = (attr: string) => {
if (attr === 'grayscale') {
@@ -571,14 +564,16 @@ const onAddFilterChange = (attr: string) => {
setLocal('appFilterStyle', appEle.style.cssText);
};
// 5、布局切换
const onSetLayout = (layout: string) => {
setLocal('oldLayout', layout);
if (themeConfig.value.layout === layout) return false;
if (themeConfig.value.layout === layout) {
return;
}
themeConfig.value.layout = layout;
themeConfig.value.isDrawer = false;
initSetLayoutChange();
onMenuBarHighlightChange();
};
// 设置布局切换,重置主题样式
const initSetLayoutChange = () => {
@@ -627,14 +622,6 @@ const onDrawerClose = () => {
themeConfig.value.isShowLogoChange = false;
themeConfig.value.isDrawer = false;
};
// 布局配置弹窗打开
const openDrawer = () => {
themeConfig.value.isDrawer = true;
nextTick(() => {
// 初始化复制功能,防止点击两次才可以复制
onCopyConfigClick(copyConfigBtnRef.value?.$el);
});
};
// 触发 store 布局配置更新
const setDispatchThemeConfig = () => {
@@ -665,63 +652,24 @@ const onCopyConfigClick = (target: any) => {
clipboard.destroy();
});
};
onMounted(() => {
nextTick(() => {
// 监听菜单点击,菜单字体背景高亮
mittBus.on('onMenuClick', () => {
onMenuBarHighlightChange();
});
// 监听窗口大小改变,非默认布局,设置成默认布局(适配移动端)
mittBus.on('layoutMobileResize', (res: any) => {
themeConfig.value.layout = res.layout;
themeConfig.value.isDrawer = false;
initSetLayoutChange();
onMenuBarHighlightChange();
themeConfig.value.isCollapse = false;
});
window.addEventListener('load', () => {
// 刷新页面时,设置了值,直接取缓存中的值进行初始化
setTimeout(() => {
// 顶栏背景渐变
if (getLocal('navbarsBgStyle') && themeConfig.value.isTopBarColorGradual) {
const breadcrumbIndexEl: any = document.querySelector('.layout-navbars-breadcrumb-index');
breadcrumbIndexEl.style.cssText = getLocal('navbarsBgStyle');
}
// 菜单背景渐变
if (getLocal('asideBgStyle') && themeConfig.value.isMenuBarColorGradual) {
const asideEl: any = document.querySelector('.layout-container .el-aside');
asideEl.style.cssText = getLocal('asideBgStyle');
}
// 分栏菜单背景渐变
if (getLocal('columnsBgStyle') && themeConfig.value.isColumnsMenuBarColorGradual) {
const asideEl: any = document.querySelector('.layout-container .layout-columns-aside');
asideEl.style.cssText = getLocal('columnsBgStyle');
}
// 菜单字体背景高亮
if (getLocal('menuBarHighlightId') && themeConfig.value.isMenuBarColorHighlight) {
let els = document.querySelector('.el-menu-item.is-active');
if (!els) return false;
els.setAttribute('id', getLocal('menuBarHighlightId'));
}
// 灰色模式/色弱模式
if (getLocal('appFilterStyle')) {
const appEl: any = document.querySelector('#app');
appEl.style.cssText = getLocal('appFilterStyle');
}
// // 语言国际化
// if (getLocal('themeConfig')) proxy.$i18n.locale = getLocal('themeConfig').globalI18n;
}, 100);
});
});
});
onUnmounted(() => {
// 取消监听菜单点击,菜单字体背景高亮
mittBus.off('onMenuClick');
mittBus.off('layoutMobileResize');
});
const checkClientWidth = () => {
let oldLayout = getLocal('oldLayout');
if (!oldLayout) {
oldLayout = themeConfig.value.layout;
setLocal('oldLayout', themeConfig.value.layout);
}
if (width.value < 1000) {
themeConfig.value.isCollapse = false;
themeConfig.value.layout = 'defaults';
} else {
themeConfig.value.layout = oldLayout ? oldLayout : 'defaults';
}
defineExpose({ openDrawer });
themeConfig.value.isDrawer = false;
initSetLayoutChange();
themeConfig.value.isCollapse = false;
};
</script>
<style scoped lang="scss">

View File

@@ -41,25 +41,27 @@
<div class="layout-navbars-breadcrumb-user-icon" @click="onSearchClick">
<SvgIcon name="search" :title="$t('layout.user.menuSearch')" />
</div>
<div class="layout-navbars-breadcrumb-user-icon" @click="onLayoutSetingClick">
<SvgIcon name="setting" :title="$t('layout.user.layoutConf')" />
</div>
<div class="layout-navbars-breadcrumb-user-icon">
<el-popover placement="bottom" trigger="click" :visible="state.isShowUserNewsPopover" :width="300" popper-class="el-popover-pupop-user-news">
<template #reference>
<el-badge :is-dot="false" @click="state.isShowUserNewsPopover = !state.isShowUserNewsPopover">
<el-popover @show="onShowMsgs" @hide="userNewsRef?.clearMsg()" placement="bottom" trigger="click" :width="500">
<template #reference>
<div class="layout-navbars-breadcrumb-user-icon">
<el-badge :show-zero="false" :value="state.unreadMsgCount">
<SvgIcon name="bell" :title="$t('layout.user.news')" />
</el-badge>
</template>
<transition name="el-zoom-in-top">
<UserNews v-show="state.isShowUserNewsPopover" />
</transition>
</el-popover>
</div>
</div>
</template>
<UserNews ref="userNewsRef" @update:count="state.unreadMsgCount = $event" />
</el-popover>
<div class="layout-navbars-breadcrumb-user-icon mr-2" @click="onScreenfullClick">
<SvgIcon v-if="!state.isScreenfull" name="full-screen" :title="$t('layout.user.fullScreenOff')" />
<SvgIcon v-else name="crop" />
</div>
<el-dropdown trigger="click" :show-timeout="70" :hide-timeout="50" @command="onHandleCommandClick">
<span class="layout-navbars-breadcrumb-user-link cursor-pointer">
<img :src="userInfo.photo" class="layout-navbars-breadcrumb-user-link-photo mr-1" />
@@ -79,7 +81,7 @@
</template>
<script setup lang="ts" name="layoutBreadcrumbUser">
import { ref, computed, reactive, onMounted, watch } from 'vue';
import { ref, computed, reactive, onMounted, watch, useTemplateRef } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessageBox, ElMessage } from 'element-plus';
import screenfull from 'screenfull';
@@ -90,7 +92,6 @@ import { useThemeConfig } from '@/store/themeConfig';
import { clearSession } from '@/common/utils/storage';
import UserNews from '@/layout/navBars/breadcrumb/userNews.vue';
import SearchMenu from '@/layout/navBars/breadcrumb/search.vue';
import mittBus from '@/common/utils/mitt';
import openApi from '@/common/openApi';
import { getThemeConfig } from '@/common/utils/storage';
import { useDark, usePreferredDark } from '@vueuse/core';
@@ -100,10 +101,12 @@ import EnumValue from '@/common/Enum';
const router = useRouter();
const searchRef = ref();
const userNewsRef = useTemplateRef('userNewsRef');
const state = reactive({
isScreenfull: false,
isShowUserNewsPopover: false,
disabledSize: '',
unreadMsgCount: 0,
});
const { userInfo } = storeToRefs(useUserInfo());
const themeConfigStore = useThemeConfig();
@@ -126,8 +129,15 @@ onMounted(() => {
initComponentSize();
isDark.value = themeConfig.isDark;
}
// 获取未读消息数量
state.unreadMsgCount = 0;
});
const onShowMsgs = () => {
userNewsRef.value?.loadMsgs(true);
};
// 全屏点击时
const onScreenfullClick = () => {
if (!screenfull.isEnabled) {
@@ -139,7 +149,7 @@ const onScreenfullClick = () => {
};
// 布局配置 icon 点击时
const onLayoutSetingClick = () => {
mittBus.emit('openSetingsDrawer');
themeConfig.value.isDrawer = true;
};
// 下拉菜单点击时
const onHandleCommandClick = (path: string) => {

View File

@@ -1,117 +1,164 @@
<template>
<div class="layout-navbars-breadcrumb-user-news">
<div class="head-box">
<div class="head-box-title">{{ $t('layout.user.newTitle') }}</div>
<div class="head-box-btn" v-if="newsList.length > 0" @click="onAllReadClick">{{ $t('layout.user.newBtn') }}</div>
<div class="rounded-xl shadow-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 overflow-hidden w-full">
<!-- Header -->
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-100 dark:border-gray-700">
<h3 class="font-semibold text-lg text-gray-800 dark:text-gray-100 flex items-center">
<SvgIcon class="mr-2" name="Bell" :size="16" />
{{ $t('layout.user.newTitle') }}
</h3>
<el-badge :value="unreadCount" :max="99" :hidden="unreadCount === 0" type="primary">
<el-button v-if="unreadCount > 0" size="small" type="primary" link @click="onRead()" class="text-sm">
{{ $t('layout.user.newBtn') }}
</el-button>
</el-badge>
</div>
<div class="content-box">
<template v-if="newsList.length > 0">
<div class="content-box-item" v-for="(v, k) in newsList" :key="k">
<div>{{ v.label }}</div>
<div class="content-box-msg">
{{ v.value }}
<!-- Content -->
<el-scrollbar height="360px" v-loading="loadingMsgs" class="px-3 py-2" :class="{ 'py-8': msgs.length === 0 }">
<template v-if="msgs.length > 0">
<div
v-for="(v, k) in msgs"
:key="k"
class="px-3 py-3 my-1 rounded-lg transition-all duration-200 cursor-pointer hover:shadow-sm hover:bg-gray-100 dark:hover:bg-gray-700"
:class="{
' hover:bg-gray-100 dark:hover:bg-gray-200 border border-blue-100 dark:border-blue-800/50': v.status == -1,
'bg-gray-50 hover:bg-gray-100 dark:bg-gray-600/20 dark:hover:bg-gray-200 border border-transparent': v.status == 1,
}"
@click="onRead(v)"
>
<div class="flex justify-between items-start">
<el-tag
size="small"
:type="EnumValue.getEnumByValue(MsgSubtypeEnum, v.subtype)?.extra?.notifyType || 'info'"
effect="light"
class="rounded-full"
>
{{ $t(EnumValue.getEnumByValue(MsgSubtypeEnum, v.subtype)?.label || '') }}
</el-tag>
<el-text size="small" type="info" class="text-xs whitespace-nowrap ml-2">
{{ formatDate(v.createTime) }}
</el-text>
</div>
<div class="content-box-time">{{ v.time }}</div>
<div class="mt-2 text-gray-700 dark:text-gray-300 text-sm leading-relaxed">
<MessageRenderer :content="v.msg" size="small" />
</div>
</div>
<div class="text-center py-3" v-if="!loadMoreDisable">
<el-button link type="primary" size="small" @click="loadMsgs()">
{{ $t('redis.loadMore') }}
<SvgIcon name="ArrowDown" />
</el-button>
</div>
</template>
<el-empty :description="$t('layout.user.newDesc')" v-else></el-empty>
</div>
<div class="foot-box" @click="toMsgCenter" v-if="newsList.length > 0">{{ $t('layout.user.newGo') }}</div>
<div v-else-if="!loadingMsgs" class="text-center py-6">
<SvgIcon name="ChatLineRound" :size="36" class="mb-3 text-gray-300 dark:text-gray-600" />
<p class="text-gray-500 dark:text-gray-400 text-2xl">{{ $t('layout.user.newDesc') }}</p>
</div>
</el-scrollbar>
</div>
</template>
<script lang="ts">
import { reactive, toRefs } from 'vue';
export default {
name: 'layoutBreadcrumbUserNews',
setup() {
const state = reactive({
newsList: [
{
label: '关于学习交流的通知',
value: 'QQ群号码 119699946',
time: '2021-09-08',
},
],
});
// 全部已读点击
const onAllReadClick = () => {
state.newsList = [];
};
// 前往通知中心点击
const toMsgCenter = () => {};
return {
onAllReadClick,
toMsgCenter,
...toRefs(state),
};
},
<script lang="ts" setup>
import { MsgSubtypeEnum } from '@/common/commonEnum';
import EnumValue from '@/common/Enum';
import { formatDate } from '@/common/utils/format';
import { MessageRenderer } from '@/components/message/message';
import { personApi } from '@/views/personal/api';
import { useIntervalFn } from '@vueuse/core';
import { onMounted, ref, watchEffect } from 'vue';
const emit = defineEmits(['update:count']);
const msgQuery = ref({
pageNum: 1,
pageSize: 10,
});
const loadMoreDisable = ref(true);
const loadingMsgs = ref(true);
const msgs = ref<Array<any>>([]);
const unreadCount = ref(0);
onMounted(() => {
useIntervalFn(
() => {
// 定时更新未读消息数
personApi.getUnreadMsgCount.request().then((res) => {
unreadCount.value = res;
});
},
10 * 1000,
{ immediate: true, immediateCallback: true }
);
});
watchEffect(() => {
emit('update:count', unreadCount.value);
});
const loadMsgs = async (research: boolean = false) => {
if (research) {
msgQuery.value.pageNum = 1;
msgs.value = [];
}
const msgList = await getMsgs();
msgs.value.push(...msgList.list);
msgQuery.value.pageNum += 1;
loadMoreDisable.value = msgList.total <= msgs.value.length;
};
const getMsgs = async () => {
try {
loadingMsgs.value = true;
return await personApi.getMsgs.request(msgQuery.value);
} catch (e) {
//
} finally {
loadingMsgs.value = false;
}
};
const onRead = async (msg: any = null) => {
if (msg && (msg.status == 1 || !msg.status)) {
return;
}
await personApi.readMsg.request({ id: msg?.id || 0 });
if (!msg) {
loadMsgs(true);
// 如果是全部已读,重置未读消息数
unreadCount.value = 0;
} else {
msg.status = 1;
// 如果是单条已读,减少未读消息数
unreadCount.value = Math.max(unreadCount.value - 1, 0);
}
};
defineExpose({
loadMsgs,
clearMsg: function () {
msgQuery.value.pageNum = 1;
msgs.value = [];
loadingMsgs.value = true;
},
});
const toMsgCenter = () => {};
</script>
<style scoped lang="scss">
.layout-navbars-breadcrumb-user-news {
.head-box {
display: flex;
border-bottom: 1px solid #ebeef5;
box-sizing: border-box;
color: #333333;
justify-content: space-between;
height: 35px;
align-items: center;
:deep(.el-scrollbar__view) {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.head-box-btn {
color: var(--el-color-primary);
font-size: 13px;
cursor: pointer;
opacity: 0.8;
&:hover {
opacity: 1;
}
}
}
.content-box {
font-size: 13px;
.content-box-item {
padding-top: 12px;
&:last-of-type {
padding-bottom: 12px;
}
.content-box-msg {
color: #999999;
margin-top: 5px;
margin-bottom: 5px;
}
.content-box-time {
color: #999999;
}
}
}
.foot-box {
height: 35px;
color: var(--el-color-primary);
font-size: 13px;
cursor: pointer;
opacity: 0.8;
display: flex;
align-items: center;
justify-content: center;
border-top: 1px solid #ebeef5;
&:hover {
opacity: 1;
}
}
::v-deep(.el-empty__description p) {
font-size: 13px;
}
:deep(.el-tag) {
border: none;
}
</style>

View File

@@ -5,25 +5,17 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts" name="layoutNavBars">
import { computed } from 'vue';
import { useThemeConfig } from '@/store/themeConfig';
import BreadcrumbIndex from '@/layout/navBars/breadcrumb/index.vue';
import TagsView from '@/layout/navBars/tagsView/tagsView.vue';
export default {
name: 'layoutNavBars',
components: { BreadcrumbIndex, TagsView },
setup() {
// 是否显示 tagsView
const setShowTagsView = computed(() => {
let { layout, isTagsview } = useThemeConfig().themeConfig;
return layout !== 'classic' && isTagsview;
});
return {
setShowTagsView,
};
},
};
// 是否显示 tagsView
const setShowTagsView = computed(() => {
let { layout, isTagsview } = useThemeConfig().themeConfig;
return layout !== 'classic' && isTagsview;
});
</script>
<style scoped lang="scss">

View File

@@ -46,12 +46,11 @@
</template>
<script lang="ts" setup name="layoutTagsView">
import { reactive, onMounted, computed, ref, nextTick, onBeforeUpdate, onBeforeMount, onUnmounted, getCurrentInstance } from 'vue';
import { reactive, onMounted, computed, ref, nextTick, onBeforeUpdate, getCurrentInstance, watch } from 'vue';
import { useRoute, useRouter, onBeforeRouteUpdate } from 'vue-router';
import screenfull from 'screenfull';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
import mittBus from '@/common/utils/mitt';
import Sortable from 'sortablejs';
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
import { getTagViews, setTagViews, removeTagViews } from '@/common/utils/storage';
@@ -185,7 +184,7 @@ const refreshCurrentTagsView = async (path: string) => {
const item = getTagsView(path);
await keepAliveNamesStores.delCachedView(item);
keepAliveNamesStores.addCachedView(item);
mittBus.emit('onTagsViewRefreshRouterView', path);
useTagsViews().setCurrentRefreshPath(path);
};
const getTagsView = (path: string) => {
@@ -375,18 +374,15 @@ const initSortable = () => {
}
};
// 页面加载前
onBeforeMount(() => {
// 监听布局配置界面开启/关闭拖拽
mittBus.on('openOrCloseSortable', () => {
initSortable();
});
});
// 页面卸载时
onUnmounted(() => {
// 取消监听布局配置界面开启/关闭拖拽
mittBus.off('openOrCloseSortable');
});
watch(
() => themeConfig.value.isSortableTagsView,
(isSortableTagsView: boolean) => {
if (isSortableTagsView) {
initSortable();
}
}
);
// 页面更新时
onBeforeUpdate(() => {
tagsRefs.value = [];

View File

@@ -1,40 +1,44 @@
<template>
<div class="el-menu-horizontal-warp">
<el-scrollbar @wheel.prevent="onElMenuHorizontalScroll" ref="elMenuHorizontalScrollRef">
<el-menu router :default-active="state.defaultActive" background-color="transparent" mode="horizontal" @select="onHorizontalSelect">
<template v-for="val in menuLists">
<el-sub-menu :index="val.path" v-if="val.children && val.children.length > 0" :key="val.path">
<template #title>
<SvgIcon :name="val.meta.icon" />
<span>{{ $t(val.meta.title) }}</span>
</template>
<SubItem :chil="val.children" />
</el-sub-menu>
<el-menu-item :index="val.path" :key="val?.path" v-else>
<template #title v-if="!val.meta.link || (val.meta.link && val.meta.linkType == 1)">
<el-menu
router
:default-active="state.defaultActive"
background-color="transparent"
mode="horizontal"
@select="onHorizontalSelect"
class="horizontal-menu"
>
<template v-for="val in menuLists">
<el-sub-menu :index="val.path" v-if="val.children && val.children.length > 0" :key="val.path">
<template #title>
<SvgIcon :name="val.meta.icon" />
<span>{{ $t(val.meta.title) }}</span>
</template>
<SubItem :chil="val.children" />
</el-sub-menu>
<el-menu-item :index="val.path" :key="val?.path" v-else>
<template #title v-if="!val.meta.link || (val.meta.link && val.meta.linkType == 1)">
<SvgIcon :name="val.meta.icon" />
{{ $t(val.meta.title) }}
</template>
<template #title v-else>
<a class="w-full" :href="val.meta.link" target="_blank">
<SvgIcon :name="val.meta.icon" />
{{ $t(val.meta.title) }}
</template>
<template #title v-else>
<a :href="val.meta.link" target="_blank">
<SvgIcon :name="val.meta.icon" />
{{ $t(val.meta.title) }}
</a>
</template>
</el-menu-item>
</template>
</el-menu>
</el-scrollbar>
</a>
</template>
</el-menu-item>
</template>
</el-menu>
</div>
</template>
<script lang="ts" setup name="navMenuHorizontal">
import { reactive, computed, getCurrentInstance, onMounted, nextTick } from 'vue';
import { reactive, computed, onMounted, inject } from 'vue';
import { useRoute, onBeforeRouteUpdate } from 'vue-router';
import SubItem from '@/layout/navMenu/subItem.vue';
import { useRoutesList } from '@/store/routesList';
import { useThemeConfig } from '@/store/themeConfig';
import mittBus from '@/common/utils/mitt';
// 定义父组件传过来的值
const props = defineProps({
@@ -45,28 +49,18 @@ const props = defineProps({
},
});
const { proxy } = getCurrentInstance() as any;
const route = useRoute();
const state: any = reactive({
defaultActive: null,
});
// 注入 classicMenuData
const classicMenuData: any = inject('classicMenuData', null);
// 获取父级菜单数据
const menuLists = computed(() => {
return props.menuList;
});
// 设置横向滚动条可以鼠标滚轮滚动
const onElMenuHorizontalScroll = (e: any) => {
const eventDelta = e.wheelDelta || -e.deltaY * 40;
proxy.$refs.elMenuHorizontalScrollRef.$refs.wrapRef.scrollLeft = proxy.$refs.elMenuHorizontalScrollRef.$refs.wrapRef.scrollLeft + eventDelta / 4;
};
// 初始化数据,页面刷新时,滚动条滚动到对应位置
const initElMenuOffsetLeft = () => {
nextTick(() => {
let els: any = document.querySelector('.el-menu.el-menu--horizontal li.is-active');
if (!els) return false;
proxy.$refs.elMenuHorizontalScrollRef.$refs.wrapRef.scrollLeft = els.offsetLeft;
});
};
// 设置页面当前路由高亮
const setCurrentRouterHighlight = (path: string) => {
const currentPathSplit = path.split('/');
@@ -102,17 +96,17 @@ const setSendClassicChildren = (path: string) => {
};
// 菜单激活回调
const onHorizontalSelect = (path: string) => {
mittBus.emit('setSendClassicChildren', setSendClassicChildren(path));
if (classicMenuData) {
classicMenuData.value = setSendClassicChildren(path);
}
};
// 页面加载时
onMounted(() => {
initElMenuOffsetLeft();
setCurrentRouterHighlight(route.path);
});
// 路由更新时
onBeforeRouteUpdate((to) => {
setCurrentRouterHighlight(to.path);
mittBus.emit('onMenuClick');
});
</script>
@@ -135,6 +129,17 @@ onBeforeRouteUpdate((to) => {
height: 100%;
width: 100%;
box-sizing: border-box;
border-bottom: none !important;
}
}
// 菜单项基础样式 - 统一一级菜单和子菜单目录的宽度
.horizontal-menu :deep(.el-menu-item),
.horizontal-menu :deep(.el-sub-menu__title) {
margin: 0 5px !important;
justify-content: center;
width: fit-content;
text-align: center; // 使文字居中对齐
padding: 0 16px !important; // 统一内边距
}
</style>

View File

@@ -13,7 +13,7 @@
<span>{{ $t(val.meta.title) }}</span>
</template>
<template v-else>
<a :href="val.meta.link" target="_blank">
<a class="w-full" :href="val.meta.link" target="_blank">
<SvgIcon :name="val.meta.icon" />
{{ $t(val.meta.title) }}
</a>
@@ -22,24 +22,20 @@
</template>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue';
export default defineComponent({
name: 'navMenuSubItem',
props: {
chil: {
type: Array,
default: () => [],
},
},
setup(props) {
// 获取父级菜单数据
const chils = computed(() => {
return props.chil as any;
});
return {
chils,
};
},
<script setup lang="ts" name="navMenuSubItem">
import { computed } from 'vue';
// 定义 props
interface Props {
chil?: any[];
}
const props = withDefaults(defineProps<Props>(), {
chil: () => [],
});
// 获取父级菜单数据
const chils = computed(() => {
return props.chil as any;
});
</script>

View File

@@ -21,7 +21,7 @@
<span>{{ $t(val.meta.title) }}</span>
</template>
<template #title v-else>
<a :href="val.meta.link" target="_blank">{{ $t(val.meta.title) }}</a></template
<a class="w-full" :href="val.meta.link" target="_blank">{{ $t(val.meta.title) }}</a></template
>
</el-menu-item>
</template>
@@ -29,12 +29,12 @@
</template>
<script lang="ts" setup name="navMenuVertical">
import { reactive, computed } from 'vue';
import { reactive, computed, defineAsyncComponent } from 'vue';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
import { useRoute, onBeforeRouteUpdate } from 'vue-router';
import SubItem from '@/layout/navMenu/subItem.vue';
import mittBus from '@/common/utils/mitt';
const SubItem = defineAsyncComponent(() => import('@/layout/navMenu/subItem.vue'));
// 定义父组件传过来的值
const props = defineProps({
@@ -46,23 +46,29 @@ const props = defineProps({
});
const { themeConfig } = storeToRefs(useThemeConfig());
const route = useRoute();
const state = reactive({
defaultActive: route.path,
});
// 获取父级菜单数据
const menuLists = computed(() => {
return props.menuList;
});
// 设置菜单的收起/展开
const setIsCollapse = computed(() => {
return document.body.clientWidth < 1000 ? false : themeConfig.value.isCollapse;
});
// 路由更新时
onBeforeRouteUpdate((to) => {
state.defaultActive = to.path;
mittBus.emit('onMenuClick');
const clientWidth = document.body.clientWidth;
if (clientWidth < 1000) themeConfig.value.isCollapse = false;
if (clientWidth < 1000) {
themeConfig.value.isCollapse = false;
}
});
</script>

View File

@@ -1,59 +1,114 @@
<template>
<div>
<div class="layout-view-bg-white flex !h-full" v-loading="iframeLoading">
<iframe :src="iframeUrl" frameborder="0" height="100%" width="100%" id="iframe" v-show="!iframeLoading"></iframe>
<div class="h-full">
<div class="w-full h-full relative" v-for="v in setIframeList" :key="v.path">
<transition-group :name="name">
<div
class="absolute top-0 left-0 w-full h-full flex justify-center items-center bg-white z-[100]"
v-if="v.meta.loading"
:key="`${v.path}-loading`"
>
<div class="flex flex-col items-center text-gray-500">
<i class="el-icon-loading"></i>
<div class="mt-2.5 text-sm">loading...</div>
</div>
</div>
<iframe
:src="v.meta.link"
:key="v.path"
frameborder="0"
height="100%"
width="100%"
style="position: absolute"
:data-url="v.path"
v-show="getRoutePath === v.path"
ref="iframeRef"
/>
</transition-group>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs, onMounted, onBeforeMount, onUnmounted, nextTick } from 'vue';
<script setup lang="ts" name="layoutIframeView">
import { computed, watch, ref, nextTick } from 'vue';
import { useRoute } from 'vue-router';
import mittBus from '@/common/utils/mitt';
export default defineComponent({
name: 'layoutIfameView',
props: {
meta: {
type: Object,
default: () => {},
},
// 定义父组件传过来的值
const props = defineProps({
// 刷新 iframe
refreshKey: {
type: String,
default: () => '',
},
setup(props, { emit }) {
const route = useRoute();
const state = reactive({
iframeLoading: true,
iframeUrl: '',
});
// 初始化页面加载 loading
const initIframeLoad = () => {
nextTick(() => {
state.iframeLoading = true;
const iframe = document.getElementById('iframe');
if (!iframe) return false;
iframe.onload = () => {
state.iframeLoading = false;
};
});
};
// 页面加载前
onBeforeMount(() => {
state.iframeUrl = props.meta.link;
mittBus.on('onTagsViewRefreshRouterView', (path: string) => {
if (route.path !== path) return false;
emit('getCurrentRouteMeta');
});
});
// 页面加载时
onMounted(() => {
initIframeLoad();
});
// 页面卸载时
onUnmounted(() => {
mittBus.off('onTagsViewRefreshRouterView', () => {});
});
return {
...toRefs(state),
};
// 过渡动画 name
name: {
type: String,
default: () => 'slide-right',
},
// iframe 列表
list: {
type: Array,
default: () => [],
},
});
const iframeRef = ref();
const route = useRoute();
// 处理 list 列表,当打开时,才进行加载
const setIframeList = computed(() => {
return props.list.filter((v: any) => v.meta?.isIframeOpen) as any[];
});
// 获取 iframe 当前路由 path
const getRoutePath = computed(() => {
return route.path;
});
// 关闭 iframe loading
const closeIframeLoading = (val: string, item: any) => {
nextTick(() => {
if (!iframeRef.value) return false;
iframeRef.value.forEach((v: HTMLElement) => {
if (v.dataset.url === val) {
v.onload = () => {
if (item.meta?.isIframeOpen && item.meta.loading) item.meta.loading = false;
};
}
});
});
};
// 监听路由变化,初始化 iframe 数据,防止多个 iframe 时,切换不生效
watch(
() => route.fullPath,
(val) => {
const item: any = props.list.find((v: any) => v.path === val);
if (!item) return false;
if (!item.meta.isIframeOpen) item.meta.isIframeOpen = true;
closeIframeLoading(val, item);
},
{
immediate: true,
}
);
// 监听 iframe refreshKey 变化,用于 tagsview 右键菜单刷新
watch(
() => props.refreshKey,
() => {
const item: any = props.list.find((v: any) => v.path === route.path);
if (!item) return false;
if (item.meta.isIframeOpen) item.meta.isIframeOpen = false;
setTimeout(() => {
item.meta.isIframeOpen = true;
item.meta.loading = true;
closeIframeLoading(route.fullPath, item);
});
},
{
deep: true,
}
);
</script>
<style scoped></style>

View File

@@ -1,29 +1,61 @@
<template>
<div>
<div class="layout-view-bg-white flex layout-view-link">
<a :href="currentRouteMeta.link" target="_blank" class="flex-margin"> {{ $t(currentRouteMeta.title) }}{{ currentRouteMeta.link }} </a>
<div class="card flex flex-col h-full p-4 layout-link-container">
<div class="flex-1 overflow-auto layout-padding-view">
<div class="flex flex-col items-center justify-center h-full layout-link-warp">
<i class="relative text-8xl text-primary layout-link-icon iconfont icon-xingqiu">
<span
class="absolute top-0 left-[50px] w-4 h-24 bg-gradient-to-b from-white/5 via-white/20 to-white/5 transform -rotate-12 animate-pulse"
></span>
</i>
<div class="mt-4 text-sm text-gray-500 opacity-70 layout-link-msg">页面 "{{ $t(state.title) }}" 已在新窗口中打开</div>
<el-button class="mt-8 rounded-full" round size="default" @click="onGotoFullPage">
<i class="iconfont icon-lianjie"></i>
<span>立即前往体验</span>
</el-button>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, computed } from 'vue';
export default defineComponent({
name: 'layoutLinkView',
props: {
meta: {
type: Object,
default: () => {},
},
},
setup(props) {
// 获取父级菜单数据
const currentRouteMeta = computed(() => {
return props.meta;
});
return {
currentRouteMeta,
};
},
<script setup lang="ts" name="layoutLinkView">
import { reactive, watch } from 'vue';
import { useRoute } from 'vue-router';
// 定义变量内容
const route = useRoute();
const state = reactive({
title: '',
link: '',
});
// 立即前往
const onGotoFullPage = () => {
window.open(state.link);
};
// 监听路由的变化,设置内容
watch(
() => route.path,
() => {
state.title = <string>route.meta.title;
state.link = <string>route.meta.link;
},
{
immediate: true,
}
);
</script>
<style scoped lang="scss">
.layout-link-container {
.layout-link-warp {
margin: auto;
.layout-link-msg {
font-size: 12px;
color: var(--next-bg-topBarColor);
opacity: 0.7;
margin-top: 15px;
}
}
}
</style>

View File

@@ -1,51 +1,76 @@
<template>
<router-view v-slot="{ Component }">
<transition appear :name="setTransitionName" mode="out-in">
<transition appear :name="themeConfig.animation" mode="out-in">
<keep-alive :include="getKeepAliveNames">
<component :is="Component" :key="state.refreshRouterViewKey" />
<component :is="Component" :key="state.refreshRouterViewKey" v-show="!isIframePage" />
</keep-alive>
</transition>
</router-view>
<transition :name="themeConfig.animation" mode="out-in">
<Iframes class="w-full" v-show="isIframePage" :refreshKey="state.iframeRefreshKey" :name="themeConfig.animation" :list="state.iframes" />
</transition>
</template>
<script lang="ts" setup name="layoutParentView">
import { computed, watch, reactive, onBeforeMount, onMounted, onUnmounted, nextTick } from 'vue';
import { useRoute } from 'vue-router';
import { computed, watch, reactive, onBeforeMount, onMounted, nextTick, defineAsyncComponent } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
import { useKeepALiveNames } from '@/store/keepAliveNames';
import mittBus from '@/common/utils/mitt';
import { getTagViews } from '@/common/utils/storage';
import { useTagsViews } from '@/store/tagsViews';
import { LinkTypeEnum } from '@/common/commonEnum';
const Iframes = defineAsyncComponent(() => import('@/layout/routerView/iframes.vue'));
const route = useRoute();
const router = useRouter();
const { themeConfig } = storeToRefs(useThemeConfig());
const { keepAliveNames, cachedViews } = storeToRefs(useKeepALiveNames());
const state: any = reactive({
refreshRouterViewKey: null,
keepAliveNameList: [],
const state = reactive({
refreshRouterViewKey: '',
keepAliveNameList: [] as any[],
iframeRefreshKey: '', // iframe tagsview 右键菜单刷新时
iframes: [] as any[],
});
const { currentRefreshPath } = storeToRefs(useTagsViews());
// 获取组件缓存列表(name值)
const getKeepAliveNames = computed(() => {
return themeConfig.value.isTagsview ? cachedViews.value : state.keepAliveNameList;
});
// 设置 iframe 显示/隐藏
const isIframePage = computed(() => {
return route.meta.linkType == LinkTypeEnum.Iframes.value;
});
watch(currentRefreshPath, (path) => {
if (decodeURI(route.fullPath) !== path) {
return;
}
state.keepAliveNameList = keepAliveNames.value.filter((name: string) => route.name !== name);
state.refreshRouterViewKey = '';
state.iframeRefreshKey = '';
nextTick(() => {
state.refreshRouterViewKey = path;
state.iframeRefreshKey = path;
state.keepAliveNameList = keepAliveNames.value;
});
useTagsViews().setCurrentRefreshPath('');
});
// 页面加载前,处理缓存,页面刷新时路由缓存处理
onBeforeMount(() => {
state.keepAliveNameList = keepAliveNames.value;
mittBus.on('onTagsViewRefreshRouterView', (path: string) => {
if (decodeURI(route.fullPath) !== path) return false;
state.keepAliveNameList = keepAliveNames.value.filter((name: string) => route.name !== name);
state.refreshRouterViewKey = '';
nextTick(() => {
state.refreshRouterViewKey = path;
state.keepAliveNameList = keepAliveNames.value;
});
});
});
// 页面加载时
onMounted(() => {
getIframesRoutes();
nextTick(() => {
setTimeout(() => {
if (themeConfig.value.isCacheTagsView) {
@@ -55,6 +80,7 @@ onMounted(() => {
}, 0);
});
});
// 监听路由变化,防止 tagsView 多标签时,切换动画消失
watch(
() => route.fullPath,
@@ -65,12 +91,15 @@ watch(
immediate: true,
}
);
// 设置主界面切换动画
const setTransitionName = computed(() => {
return themeConfig.value.animation;
});
// 页面卸载时
onUnmounted(() => {
mittBus.off('onTagsViewRefreshRouterView');
});
// 获取 iframe 组件列表(未进行渲染)
const getIframesRoutes = async () => {
router.getRoutes().forEach((v) => {
if (v.meta.linkType === LinkTypeEnum.Iframes.value) {
v.meta.isIframeOpen = false;
v.meta.loading = true;
state.iframes.push({ ...v });
}
});
};
</script>

View File

@@ -7,6 +7,10 @@ import { useKeepALiveNames } from '@/store/keepAliveNames';
import router from '.';
import { RouteRecordRaw } from 'vue-router';
import { LAYOUT_ROUTE_NAME } from './staticRouter';
import { LinkTypeEnum } from '@/common/commonEnum';
const Link = () => import('@/layout/routerView/link.vue');
const Iframe = () => import('@/layout/routerView/iframes.vue');
/**
* 获取目录下的 route.ts 全部文件
@@ -17,55 +21,46 @@ const routeModules: Record<string, any> = import.meta.glob(['../views/**/route.{
// 后端控制路由:执行路由数据初始化
export async function initBackendRoutes() {
let allModuleRoutes = {};
for (const path in routeModules) {
// 获取默认导出的路由
const routes = routeModules[path]?.default;
allModuleRoutes = { ...allModuleRoutes, ...routes };
}
// 合并所有模块路由
const allModuleRoutes = Object.values(routeModules).reduce((acc: any, module: any) => {
return { ...acc, ...module.default };
}, {});
const token = getToken(); // 获取浏览器缓存 token 值
const token = getToken();
if (!token) {
// 无 token 停止执行下一步
return false;
}
useUserInfo().setUserInfo({});
// 获取路由
let menuRoute = await getBackEndControlRoutes();
const cacheList: Array<string> = [];
// 处理路由component
const routes = backEndRouterConverter(allModuleRoutes, menuRoute, (router: any) => {
// 可能为false时不存在isKeepAlive属性
if (!router.meta.isKeepAlive) {
router.meta.isKeepAlive = false;
}
if (router.meta.isKeepAlive) {
cacheList.push(router.name);
}
});
routes.forEach((item: any) => {
if (item.meta.isFull) {
// 菜单为全屏展示 (示例:数据大屏页面等)
router.addRoute(item as RouteRecordRaw);
} else {
// 要将嵌套路由添加到现有的路由中,可以将路由的 name 作为第一个参数传递给 router.addRoute(),这将有效地添加路由,就像通过 children 添加的一样
router.addRoute(LAYOUT_ROUTE_NAME, item as RouteRecordRaw);
}
});
useKeepALiveNames().setCacheKeepAlive(cacheList);
useRoutesList().setRoutesList(routes);
}
// 后端控制路由isRequestRoutes 为 true则开启后端控制路由
export async function getBackEndControlRoutes() {
try {
// 获取路由和权限
const menuAndPermission = await openApi.getPermissions();
// 赋值权限码,用于控制按钮等
useUserInfo().userInfo.permissions = menuAndPermission.permissions;
return menuAndPermission.menus;
const menuRoute = menuAndPermission.menus;
const cacheList: string[] = [];
// 处理路由component
const routes = backEndRouterConverter(allModuleRoutes, menuRoute, (router: any) => {
// 确保 isKeepAlive 属性存在
router.meta.isKeepAlive = router.meta.isKeepAlive ?? false;
if (router.meta.isKeepAlive) {
cacheList.push(router.name as string);
}
});
// 添加路由
routes.forEach((item: any) => {
if (item.meta.isFull) {
router.addRoute(item as RouteRecordRaw);
} else {
router.addRoute(LAYOUT_ROUTE_NAME, item as RouteRecordRaw);
}
});
useKeepALiveNames().setCacheKeepAlive(cacheList);
useRoutesList().setRoutesList(routes);
} catch (e: any) {
console.error('获取菜单权限信息失败', e);
clearSession();
@@ -93,47 +88,52 @@ type RouterConvCallbackFunc = (router: any) => void;
* @param meta.linkType ==> 外链类型, 内嵌: 以iframe展示、外链: 新标签打开
* @param meta.link ==> 外链地址
* */
export function backEndRouterConverter(allModuleRoutes: any, routes: any, callbackFunc: RouterConvCallbackFunc = null as any, parentPath: string = '/') {
if (!routes) {
return [];
}
export function backEndRouterConverter(allModuleRoutes: any, routes: any, callbackFunc?: RouterConvCallbackFunc, parentPath = '/'): any[] {
if (!routes) return [];
return routes.map((item: any) => {
if (!item.meta) return item;
const routeItems = [];
for (let item of routes) {
if (!item.meta) {
return item;
}
// 将json字符串的meta转为对象
item.meta = JSON.parse(item.meta);
const meta = typeof item.meta === 'string' ? JSON.parse(item.meta) : item.meta;
// 处理路径
let path = item.code;
// 如果不是以 / 开头,则路径需要拼接父路径
if (!path.startsWith('/')) {
path = parentPath + '/' + path;
path = `${parentPath}/${path}`.replace(/\/+/g, '/');
}
item.path = path;
delete item['code'];
// route.meta.title == resource.name
item.meta.title = item.name;
delete item['name'];
// 构建路由对象
const routeItem: any = {
path,
name: meta.routeName,
meta: {
...meta,
title: item.name,
},
};
// route.name == resource.meta.routeName
item.name = item.meta.routeName;
// routerName == 模块下route.ts 字段key == 组件名
item.component = allModuleRoutes[item.meta.routeName];
delete item.meta['routeName'];
// route.redirect == resource.meta.redirect
if (item.meta.redirect) {
item.redirect = item.meta.redirect;
delete item.meta['redirect'];
// 处理外链
if (meta.link) {
routeItem.component = meta.linkType == LinkTypeEnum.Link.value ? Link : Iframe;
} else {
// 使用模块路由组件
routeItem.component = allModuleRoutes[meta.routeName];
}
// 存在回调,则执行回调
callbackFunc && callbackFunc(item);
item.children && backEndRouterConverter(allModuleRoutes, item.children, callbackFunc, item.path);
routeItems.push(item);
}
return routeItems;
// 处理重定向
if (meta.redirect) {
routeItem.redirect = meta.redirect;
}
// 处理子路由
if (item.children) {
routeItem.children = backEndRouterConverter(allModuleRoutes, item.children, callbackFunc, path);
}
// 执行回调
callbackFunc?.(routeItem);
return routeItem;
});
}

View File

@@ -6,24 +6,12 @@ import { defineStore } from 'pinia';
export const useAutoOpenResource = defineStore('autoOpenResource', {
state: () => ({
autoOpenResource: {
machineCodePath: '',
dbCodePath: '',
redisCodePath: '',
mongoCodePath: '',
codePath: '',
},
}),
actions: {
setMachineCodePath(codePath: string) {
this.autoOpenResource.machineCodePath = codePath;
},
setDbCodePath(codePath: string) {
this.autoOpenResource.dbCodePath = codePath;
},
setRedisCodePath(codePath: string) {
this.autoOpenResource.redisCodePath = codePath;
},
setMongoCodePath(codePath: string) {
this.autoOpenResource.mongoCodePath = codePath;
setCodePath(codePath: string) {
this.autoOpenResource.codePath = codePath;
},
},
});

View File

@@ -1,4 +1,3 @@
import { getNowUrl } from '@/common/utils/url';
import { defineStore } from 'pinia';
/**
@@ -7,19 +6,14 @@ import { defineStore } from 'pinia';
export const useTagsViews = defineStore('tagsViews', {
state: (): TagsViewsState => ({
tagsViews: [],
currentRefreshPath: '',
}),
actions: {
setTagsViews(data: Array<TagsView>) {
this.tagsViews = data;
},
// 设置当前页面的tags view title
setNowTitle(title: string) {
this.tagsViews.forEach((item) => {
// console.log(getNowUrl(), item.path);
if (item.path == getNowUrl()) {
item.title = title;
}
});
setCurrentRefreshPath(path: string) {
this.currentRefreshPath = path;
},
},
});

View File

@@ -6,7 +6,7 @@ import { getLocal, getThemeConfig } from '@/common/utils/storage';
// 系统默认logo图标对应于@/assets/image/logo.svg
const logoIcon =
'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/PjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+PHN2ZyB0PSIxNjIxODU5MDA5NjA1IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9Ijk3MDkiIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCI+PGRlZnM+PHN0eWxlIHR5cGU9InRleHQvY3NzIj48L3N0eWxlPjwvZGVmcz48cGF0aCBkPSJNODIwLjIwMzkyMiA4MTIuMTcyNTQ5SDY4NC42NzQ1MXYtNDUuMTc2NDcxaDExMi40MzkyMTVWMjc5LjA5MDE5Nkg2MzMuNDc0NTFsLTg1LjMzMzMzNCAyNzcuMDgyMzUzYy0zLjAxMTc2NSAxMC4wMzkyMTYtMTIuMDQ3MDU5IDE2LjA2Mjc0NS0yMi4wODYyNzQgMTYuMDYyNzQ1LTEwLjAzOTIxNiAwLTE5LjA3NDUxLTcuMDI3NDUxLTIxLjA4MjM1My0xNy4wNjY2NjdsLTcxLjI3ODQzMS0yODAuMDk0MTE3aC0xODAuNzA1ODgzVjc2Mi45ODAzOTJoMTIwLjQ3MDU4OXY0NS4xNzY0NzFIMjI5Ljg5ODAzOWMtMTIuMDQ3MDU5IDAtMjIuMDg2Mjc1LTEwLjAzOTIxNi0yMi4wODYyNzQtMjIuMDg2Mjc1VjI1Mi45ODgyMzVjMC0xMi4wNDcwNTkgMTAuMDM5MjE2LTIyLjA4NjI3NSAyMi4wODYyNzQtMjIuMDg2Mjc0SDQ1MS43NjQ3MDZjMTAuMDM5MjE2IDAgMTkuMDc0NTEgNy4wMjc0NTEgMjIuMDg2Mjc0IDE3LjA2NjY2Nmw1NS4yMTU2ODcgMjE4Ljg1NDkwMkw1OTUuMzI1NDkgMjUwLjk4MDM5MmMzLjAxMTc2NS05LjAzNTI5NCAxMi4wNDcwNTktMTYuMDYyNzQ1IDIxLjA4MjM1My0xNi4wNjI3NDVoMjAyLjc5MjE1N2MxMi4wNDcwNTkgMCAyMi4wODYyNzUgMTAuMDM5MjE2IDIyLjA4NjI3NSAyMi4wODYyNzV2NTMzLjA4MjM1M2MxLjAwMzkyMiAxMi4wNDcwNTktOS4wMzUyOTQgMjIuMDg2Mjc1LTIxLjA4MjM1MyAyMi4wODYyNzR6IG0wIDAiIGZpbGw9IiNlMjU4MTMiIHAtaWQ9Ijk3MTAiPjwvcGF0aD48cGF0aCBkPSJNNzMxLjg1ODgyNCA0MjUuNjYyNzQ1YzQuMDE1Njg2LTEyLjA0NzA1OS0yLjAwNzg0My0yNS4wOTgwMzktMTQuMDU0OTAyLTI5LjExMzcyNS0xMi4wNDcwNTktNC4wMTU2ODYtMjUuMDk4MDM5IDIuMDA3ODQzLTI5LjExMzcyNiAxNC4wNTQ5MDJMNTYzLjIgNzY2Ljk5NjA3OGgtNzMuMjg2Mjc1TDM3MS40NTA5OCA0MTAuNjAzOTIyYy00LjAxNTY4Ni0xMi4wNDcwNTktMTcuMDY2NjY3LTE4LjA3MDU4OC0yOC4xMDk4MDQtMTQuMDU0OTAyLTEyLjA0NzA1OSA0LjAxNTY4Ni0xOC4wNzA1ODggMTcuMDY2NjY3LTE0LjA1NDkwMSAyOC4xMDk4MDRsMTIzLjQ4MjM1MiAzNzEuNDUwOThjMy4wMTE3NjUgOS4wMzUyOTQgMTIuMDQ3MDU5IDE1LjA1ODgyNCAyMS4wODIzNTMgMTUuMDU4ODIzaDcyLjI4MjM1M2wtNTMuMjA3ODQzIDE2MC42Mjc0NTEgNDYuMTgwMzkyIDIuMDA3ODQ0IDE5Mi43NTI5NDItNTQ4LjE0MTE3N3oiIGZpbGw9IiMyYzJjMmMiIHAtaWQ9Ijk3MTEiPjwvcGF0aD48L3N2Zz4=';
'data:image/svg+xml;charset=utf-8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/Pgo8IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPgo8c3ZnIHQ9IjE2MjE4NTkwMDk2MDUiIGNsYXNzPSJpY29uIiB2aWV3Qm94PSIwIDAgMTAyNCAxMDI0IiB2ZXJzaW9uPSIxLjEiIAogICAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgcC1pZD0iOTcwOSIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIAogICAgIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj4KICAgICA8ZGVmcz48c3R5bGUgdHlwZT0idGV4dC9jc3MiPjwvc3R5bGU+PC9kZWZzPgogICAgIDxwYXRoIGQ9Ik04MjAuMjAzOTIyIDgxMi4xNzI1NDlINjg0LjY3NDUxdi00NS4xNzY0NzFoMTEyLjQzOTIxNVYyNzkuMDkwMTk2SDYzMy40NzQ1MWwtODUuMzMzMzM0IDI3Ny4wODIzNTNjLTMuMDExNzY1IDEwLjAzOTIxNi0xMi4wNDcwNTkgMTYuMDYyNzQ1LTIyLjA4NjI3NCAxNi4wNjI3NDUtMTAuMDM5MjE2IDAtMTkuMDc0NTEtNy4wMjc0NTEtMjEuMDgyMzUzLTE3LjA2NjY2N2wtNzEuMjc4NDMxLTI4MC4wOTQxMTdoLTE4MC43MDU4ODNWNzYyLjk4MDM5MmgxMjAuNDcwNTg5djQ1LjE3NjQ3MUgyMjkuODk4MDM5Yy0xMi4wNDcwNTkgMC0yMi4wODYyNzUtMTAuMDM5MjE2LTIyLjA4NjI3NC0yMi4wODYyNzVWMjUyLjk4ODIzNWMwLTEyLjA0NzA1OSAxMC4wMzkyMTYtMjIuMDg2Mjc1IDIyLjA4NjI3NC0yMi4wODYyNzRINDUxLjc2NDcwNmMxMC4wMzkyMTYgMCAxOS4wNzQ1MSA3LjAyNzQ1MSAyMi4wODYyNzQgMTcuMDY2NjY2bDU1LjIxNTY4NyAyMTguODU0OTAyTDU5NS4zMjU0OSAyNTAuOTgwMzkyYzMuMDExNzY1LTkuMDM1Mjk0IDEyLjA0NzA1OS0xNi4wNjI3NDUgMjEuMDgyMzUzLTE2LjA2Mjc0NWgyMDIuNzkyMTU3YzEyLjA0NzA1OSAwIDIyLjA4NjI3NSAxMC4wMzkyMTYgMjIuMDg2Mjc1IDIyLjA4NjI3NXY1MzMuMDgyMzUzYzEuMDAzOTIyIDEyLjA0NzA1OS05LjAzNTI5NCAyMi4wODYyNzUtMjEuMDgyMzUzIDIyLjA4NjI3NHogbTAgMCIgZmlsbD0iI2UyNTgxMyIgcC1pZD0iOTcxMCIgc3Ryb2tlLXdpZHRoPSIzMCIgc3Ryb2tlPSIjZTI1ODEzIj48L3BhdGg+CiAgICAgPHBhdGggZD0iTTczMS44NTg4MjQgNDI1LjY2Mjc0NWM0LjAxNTY4Ni0xMi4wNDcwNTktMi4wMDc4NDMtMjUuMDk4MDM5LTE0LjA1NDkwMi0yOS4xMTM3MjUtMTIuMDQ3MDU5LTQuMDE1Njg2LTI1LjA5ODAzOSAyLjAwNzg0My0yOS4xMTM3MjYgMTQuMDU0OTAyTDU2My4yIDc2Ni45OTYwNzhoLTczLjI4NjI3NUwzNzEuNDUwOTggNDEwLjYwMzkyMmMtNC4wMTU2ODYtMTIuMDQ3MDU5LTE3LjA2NjY2Ny0xOC4wNzA1ODgtMjguMTA5ODA0LTE0LjA1NDkwMi0xMi4wNDcwNTkgNC4wMTU2ODYtMTguMDcwNTg4IDE3LjA2NjY2Ny0xNC4wNTQ5MDEgMjguMTA5ODA0bDEyMy40ODIzNTIgMzcxLjQ1MDk4YzMuMDExNzY1IDkuMDM1Mjk0IDEyLjA0NzA1OSAxNS4wNTg4MjQgMjEuMDgyMzUzIDE1LjA1ODgyM2g3Mi4yODIzNTNsLTUzLjIwNzg0MyAxNjAuNjI3NDUxIDQ2LjE4MDM5MiAyLjAwNzg0NCAxOTIuNzUyOTQyLTU0OC4xNDExNzd6IiBmaWxsPSIjMmMyYzJjIiBwLWlkPSI5NzExIiBzdHJva2Utd2lkdGg9IjMwIiBzdHJva2U9IiMyYzJjMmMiPjwvcGF0aD4KPC9zdmc+';
export const useThemeConfig = defineStore('themeConfig', {
state: (): ThemeConfigState => ({
@@ -63,10 +63,6 @@ export const useThemeConfig = defineStore('themeConfig', {
isFixedHeaderChange: false,
// 是否开启经典布局分割菜单(仅经典布局生效)
isClassicSplitMenu: false,
// 是否开启自动锁屏
isLockScreen: false,
// 开启自动锁屏倒计时(s/秒)
lockScreenTime: 30,
/* 界面显示
------------------------------- */
@@ -138,6 +134,7 @@ export const useThemeConfig = defineStore('themeConfig', {
globalTitle: 'mayfly',
// 网站副标题(登录页顶部文字)
globalViceTitle: 'mayfly-go',
appSlogan: 'common.appSlogan',
// 网站logo icon, base64编码内容
logoIcon: logoIcon,
// 默认初始语言,可选值"<zh-cn|en|zh-tw>",默认 zh-cn

View File

@@ -60,6 +60,7 @@ body,
.layout-header {
padding: 0 !important;
height: auto !important;
}
.layout-main {

View File

@@ -40,6 +40,41 @@
opacity: 0;
}
// 淡入淡出滑动效果
.slide-fade-enter-active {
transition: all 0.3s ease;
}
.slide-fade-leave-active {
transition: all 0.3s cubic-bezier(1, 0.5, 0.8, 1);
}
.slide-fade-enter-from {
transform: translateX(20px);
opacity: 0;
}
.slide-fade-leave-to {
transform: translateX(-20px);
opacity: 0;
}
// 水平滑动效果
.slide-x-enter-active,
.slide-x-leave-active {
transition: all 0.25s ease-out;
}
.slide-x-enter-from {
opacity: 0;
transform: translateX(30px);
}
.slide-x-leave-to {
opacity: 0;
transform: translateX(-30px);
}
/* Breadcrumb 面包屑过渡动画
------------------------------- */
.breadcrumb-enter-active,

View File

@@ -1,42 +1,12 @@
@use 'mixins/index' as mixins;
/* Button 按钮
------------------------------- */
/* Input 输入框、InputNumber 计数器
------------------------------- */
// 菜单搜索
.el-autocomplete-suggestion__wrap {
max-height: 280px !important;
}
/* Alert 警告
------------------------------- */
.el-alert {
border: 1px solid;
}
.el-alert__title {
word-break: break-all;
}
/* Message 消息提示
------------------------------- */
.el-message {
min-width: unset !important;
padding: 15px !important;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.02);
}
/* NavMenu 导航菜单
------------------------------- */
$radius: 6px;
$menuHeight: 46px !important;
$menuHeight: 42px !important;
$spacing: 8px;
// 鼠标 hover 时颜色
.el-menu-hover-bg-color {
background-color: var(--bg-menuBarActiveColor) !important;
background-color: #f0f8ff !important; // 更舒适的悬停背景色
}
// 默认样式修改
@@ -45,21 +15,39 @@ $menuHeight: 46px !important;
width: 220px;
}
.el-menu-item {
.el-menu-item,
.el-sub-menu__title {
height: $menuHeight;
line-height: $menuHeight;
border-radius: $radius;
color: #5a5a5a; // 统一调整菜单字体颜色为更舒适的深灰色
transition: all 0.2s ease;
}
.el-menu-item,
.el-sub-menu__title {
color: var(--bg-menuBarColor);
height: $menuHeight;
.el-menu-item {
margin: 2px $spacing;
width: calc(100% - #{2 * $spacing});
}
// 修复点击左侧菜单折叠再展开时,宽度不跟随问题
.el-menu--collapse {
width: 64px !important;
// 菜单收起时,图标不居中问题
.el-menu-item,
.el-sub-menu__title {
margin: 4px 0;
width: auto;
.iconfont,
.fa {
margin-right: 0 !important;
}
}
.el-sub-menu__title {
padding-right: 0 !important;
}
}
// 外部链接时
@@ -71,29 +59,48 @@ $menuHeight: 46px !important;
text-decoration: none;
}
// 第三方图标字体间距/大小设置
.el-menu-item .icon,
.el-sub-menu .icon,
.el-menu-item .fa,
.el-sub-menu .fa {
@include mixins.generalIcon;
}
// 水平菜单、横向菜单高亮 背景色,鼠标 hover 时,有子级菜单的背景色
.el-menu-item.is-active,
.el-sub-menu.is-active .el-sub-menu__title,
.el-sub-menu.is-active>.el-sub-menu__title,
.el-sub-menu:not(.is-opened):hover .el-sub-menu__title {
@extend .el-menu-hover-bg-color;
border-radius: $radius;
color: #409eff;
}
.el-menu-item:hover {
@extend .el-menu-hover-bg-color;
border-radius: $radius;
transform: translateX(2px);
}
.el-sub-menu.is-active.is-opened .el-sub-menu__title {
// 确保展开的子菜单项在hover时也使用统一的样式
.el-sub-menu.is-opened .el-sub-menu__title:hover {
@extend .el-menu-hover-bg-color;
}
// 只有直接激活的菜单项才应用高亮样式
.el-menu-item.is-active {
color: #409eff !important;
}
// 只有当前路由匹配的菜单项才应用高亮样式
.el-menu-item.is-active:not(.is-disabled) {
color: #409eff !important;
}
// 重置所有子菜单标题的默认样式,确保与普通菜单项一致
.el-sub-menu .el-sub-menu__title {
color: #5a5a5a !important;
}
// 只有真正激活且未展开的子菜单才应用高亮样式
.el-sub-menu.is-active:not(.is-opened)>.el-sub-menu__title {
color: #409eff !important;
}
// 展开的子菜单保持默认样式
.el-sub-menu.is-active.is-opened>.el-sub-menu__title {
background-color: unset !important;
color: #5a5a5a !important;
}
// 水平菜单、横向菜单折叠 a 标签
@@ -107,59 +114,19 @@ $menuHeight: 46px !important;
// 水平菜单
.el-menu--vertical {
background: var(--bg-menuBar);
background: #fafafa; // 更舒适的背景色
.el-sub-menu.is-active .el-sub-menu__title {
color: var(--el-menu-active-color);
}
.el-popper.is-pure.is-light {
.el-menu--vertical {
.el-sub-menu .el-sub-menu__title {
background-color: unset !important;
color: var(--bg-menuBarColor);
}
.el-sub-menu.is-active .el-sub-menu__title {
color: var(--el-menu-active-color);
}
}
}
}
// 横向菜单
.el-menu--horizontal {
background: var(--bg-topBar);
.el-menu-item,
.el-sub-menu {
height: 48px !important;
line-height: 48px !important;
color: var(--bg-topBarColor);
.el-sub-menu__title {
height: 48px !important;
line-height: 48px !important;
color: var(--bg-topBarColor);
}
.el-popper.is-pure.is-light {
.el-menu--horizontal {
.el-sub-menu .el-sub-menu__title {
background-color: unset !important;
color: var(--bg-topBarColor);
}
.el-sub-menu.is-active .el-sub-menu__title {
color: var(--el-menu-active-color);
}
}
}
}
.el-menu-item.is-active,
.el-sub-menu.is-active .el-sub-menu__title {
color: var(--el-menu-active-color);
}
}
}
@@ -171,36 +138,94 @@ $menuHeight: 46px !important;
.el-menu-item,
.el-sub-menu__title {
height: 48px !important;
color: var(--bg-topBarColor);
padding: 0 10px !important; // 减小内边距
border-bottom: none !important;
}
.el-menu-item:not(.is-active):hover,
.el-sub-menu:not(.is-active):hover .el-sub-menu__title {
color: var(--bg-topBarColor);
// 为水平菜单的子菜单项正确处理箭头图标位置
.el-sub-menu {
.el-sub-menu__title {
padding-right: 22px !important; // 调整箭头图标空间
border-bottom: none !important;
}
// 确保水平菜单的箭头图标正确显示在右侧
.el-sub-menu__icon-arrow {
right: 8px !important;
margin-top: -6px !important;
}
}
// 移除可能的伪元素下划线
.el-menu-item::after,
.el-sub-menu__title::after {
display: none !important;
}
}
// 菜单收起时,图标不居中问题
.el-menu--collapse {
.el-menu-item .iconfont,
.el-sub-menu .iconfont,
.el-menu-item .fa,
.el-sub-menu .fa {
margin-right: 0 !important;
// 暗黑模式下的菜单样式
html.dark {
.el-menu-hover-bg-color {
background-color: #2c2c2c !important; // 暗黑模式下的悬停背景色
}
.el-menu-item,
.el-sub-menu__title {
padding-right: 0 !important;
color: #b2b2b2; // 暗黑模式下的菜单字体颜色
}
.el-menu-item.is-active,
.el-menu-item.is-active:not(.is-disabled) {
color: #409eff !important;
}
// 重置所有子菜单标题的默认样式,确保与普通菜单项一致 - 暗黑模式
.el-sub-menu .el-sub-menu__title {
color: #b2b2b2 !important;
}
// 只有真正激活且未展开的子菜单才应用高亮样式 - 暗黑模式
.el-sub-menu.is-active:not(.is-opened)>.el-sub-menu__title {
color: #409eff !important;
}
// 展开的子菜单保持默认样式 - 暗黑模式
.el-sub-menu.is-active.is-opened>.el-sub-menu__title {
background-color: unset !important;
color: #b2b2b2 !important;
}
// 水平菜单、横向菜单折叠背景色 - 暗黑模式
.el-popper.is-pure.is-light {
// 水平菜单
.el-menu--vertical {
background: #1f1f1f; // 暗黑模式下的背景色
}
}
// 横向菜单(经典、横向)布局 - 暗黑模式
.el-menu.el-menu--horizontal {
.el-menu-item,
.el-sub-menu__title {
color: #b2b2b2; // 暗黑模式下的字体颜色
}
}
.el-menu {
background-color: #1f1f1f; // 暗黑模式下的菜单背景色
}
// 确保暗黑模式下展开的子菜单项在hover时也使用统一的样式
.el-sub-menu.is-opened .el-sub-menu__title:hover {
@extend .el-menu-hover-bg-color;
}
}
/* Tabs 标签页
------------------------------- */
.el-tabs__nav-wrap::after {
height: 1px !important;
}
/* Dropdown 下拉菜单
------------------------------- */
@@ -218,8 +243,6 @@ $menuHeight: 46px !important;
}
}
/* Dialog 对话框
------------------------------- */
/* Card 卡片
------------------------------- */
@@ -227,62 +250,6 @@ $menuHeight: 46px !important;
padding: 15px 20px;
}
/* Table 表格 element plus 2.2.0 版本
------------------------------- */
.el-table {
.el-button.is-text {
padding: 0;
}
}
/* scrollbar
------------------------------- */
.el-scrollbar__bar {
z-index: 4;
}
/*防止页面切换时,滚动条高度不变的问题(滚动条高度非滚动条滚动高度)*/
.el-scrollbar__wrap {
max-height: 100%;
}
.el-select-dropdown .el-scrollbar__wrap {
overflow-x: scroll !important;
}
/*修复Select 选择器高度问题*/
.el-select-dropdown__wrap {
max-height: 274px !important;
}
/*修复Cascader 级联选择器高度问题*/
.el-cascader-menu__wrap.el-scrollbar__wrap {
height: 204px !important;
}
/*用于界面高度自适应main.vue区分 scrollbar__view防止其它使用 scrollbar 的地方出现滚动条消失*/
.layout-container-view .el-scrollbar__view {
height: 100%;
}
/*防止分栏布局二级菜单很多时,滚动条消失问题*/
.layout-columns-warp .layout-aside .el-scrollbar__view {
height: unset !important;
}
/* Pagination 分页
------------------------------- */
.el-pagination__editor {
margin-right: 8px;
}
/*深色模式时分页高亮问题*/
.el-pagination.is-background .btn-next.is-active,
.el-pagination.is-background .btn-prev.is-active,
.el-pagination.is-background .el-pager li.is-active {
background-color: var(--el-color-primary) !important;
color: var(--el-color-white) !important;
}
/* Breadcrumb 面包屑
------------------------------- */
@@ -310,6 +277,8 @@ $menuHeight: 46px !important;
}
/* Dialog 对话框
------------------------------- */
.el-dialog {
border-radius: 6px;
/* 设置圆角 */

View File

@@ -1,51 +1,129 @@
.loading-next {
width: 100%;
height: 100%;
position: fixed !important;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: linear-gradient(135deg, #f5f7fa 0%, #e4edf5 100%);
z-index: 999999;
overflow: hidden;
margin: 0;
padding: 0;
&.dark {
background: linear-gradient(135deg, #1a2a3a 0%, #111827 100%);
}
}
.loading-next .loading-next-box {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
.loading-next-box {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 40px;
height: 40px;
}
.loading-next .loading-next-box-warp {
width: 80px;
height: 80px;
.loading-next-box-warp {
width: 40px;
height: 40px;
position: relative;
transform: rotate(45deg);
animation: loading-next-box-warp 2s infinite linear;
}
.loading-next .loading-next-box-warp .loading-next-box-item {
width: 33.333333%;
height: 33.333333%;
background: var(--el-color-primary);
float: left;
animation: loading-next-animation 1.2s infinite ease;
border-radius: 1px;
.loading-next-box-item {
width: 10px;
height: 10px;
position: absolute;
background: #0ea5e9; // cyan-500
border-radius: 2px;
animation: loading-next-box-item 2s infinite linear;
&:nth-child(1) {
top: 0;
left: 0;
}
&:nth-child(2) {
top: 0;
right: 0;
animation-delay: 0.2s;
}
&:nth-child(3) {
bottom: 0;
right: 0;
animation-delay: 0.4s;
}
&:nth-child(4) {
bottom: 0;
left: 0;
animation-delay: 0.6s;
}
&:nth-child(5) {
top: 0;
left: 15px;
animation-delay: 0.1s;
}
&:nth-child(6) {
top: 15px;
right: 0;
animation-delay: 0.3s;
}
&:nth-child(7) {
bottom: 15px;
right: 0;
animation-delay: 0.5s;
}
&:nth-child(8) {
bottom: 0;
left: 15px;
animation-delay: 0.7s;
}
&:nth-child(9) {
top: 15px;
left: 0;
animation-delay: 0.8s;
}
}
.loading-next .loading-next-box-warp .loading-next-box-item:nth-child(7) {
animation-delay: 0s;
@keyframes loading-next-box-warp {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.loading-next .loading-next-box-warp .loading-next-box-item:nth-child(4),
.loading-next .loading-next-box-warp .loading-next-box-item:nth-child(8) {
animation-delay: 0.1s;
@keyframes loading-next-box-item {
0% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.5;
transform: scale(0.5);
}
100% {
opacity: 1;
transform: scale(1);
}
}
.loading-next .loading-next-box-warp .loading-next-box-item:nth-child(1),
.loading-next .loading-next-box-warp .loading-next-box-item:nth-child(5),
.loading-next .loading-next-box-warp .loading-next-box-item:nth-child(9) {
animation-delay: 0.2s;
}
.loading-next .loading-next-box-warp .loading-next-box-item:nth-child(2),
.loading-next .loading-next-box-warp .loading-next-box-item:nth-child(6) {
animation-delay: 0.3s;
}
.loading-next .loading-next-box-warp .loading-next-box-item:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes loading-next-animation {
0%,
70%,
100% {
transform: scale3D(1, 1, 1);
}
35% {
transform: scale3D(0, 0, 1);
}
// 暗黑模式样式
.dark {
.loading-next-box-item {
background: #06b6d4; // cyan-400 (暗色模式下更亮一些)
}
}

View File

@@ -1,15 +1,3 @@
/* 第三方图标字体间距/大小设置
------------------------------- */
@mixin generalIcon {
font-size: 14px !important;
display: inline-block;
vertical-align: middle;
margin-right: 5px;
width: 24px;
text-align: center;
justify-content: center;
}
/* 文本不换行
------------------------------- */
@mixin text-no-wrap() {

View File

@@ -1,2 +1,3 @@
@import "tailwindcss";
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));

View File

@@ -25,8 +25,6 @@ declare interface ThemeConfigState {
isFixedHeader: boolean;
isFixedHeaderChange: boolean;
isClassicSplitMenu: boolean;
isLockScreen: boolean;
lockScreenTime: number;
isShowLogo: boolean;
isShowLogoChange: boolean;
isBreadcrumb: boolean;
@@ -49,6 +47,7 @@ declare interface ThemeConfigState {
isRequestRoutes: boolean;
globalTitle: string;
globalViceTitle: string;
appSlogan: string;
logoIcon: string;
globalI18n: string;
globalComponentSize: string;
@@ -98,6 +97,7 @@ declare interface TagsView {
// TagsView 路由列表
declare interface TagsViewsState<> {
tagsViews: TagsView[];
currentRefreshPath: string; // 当前刷新的路由 path
}
// 路由列表

View File

@@ -43,7 +43,7 @@ import { procdefApi, procinstApi } from './api';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { hasPerms } from '@/components/auth/auth';
import { SearchItem } from '@/components/SearchForm';
import { SearchItem } from '@/components/pagetable/SearchForm';
import ProcdefEdit from './ProcdefEdit.vue';
import { ProcdefStatus } from './enums';
import TagCodePath from '../ops/component/TagCodePath.vue';

View File

@@ -46,7 +46,7 @@ import { ref, toRefs, reactive, Ref } from 'vue';
import { procinstApi } from './api';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { SearchItem } from '@/components/SearchForm';
import { SearchItem } from '@/components/pagetable/SearchForm';
import ProcinstDetail from './ProcinstDetail.vue';
import { FlowBizType, ProcinstBizStatus, ProcinstStatus } from './enums';
import { formatTime } from '@/common/utils/format';

View File

@@ -63,7 +63,7 @@ import { ref, toRefs, reactive, Ref, useTemplateRef } from 'vue';
import { procinstTaskApi } from './api';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { SearchItem } from '@/components/SearchForm';
import { SearchItem } from '@/components/pagetable/SearchForm';
import ProcinstDetail from './ProcinstDetail.vue';
import { FlowBizType, ProcinstStatus, ProcinstTaskStatus } from './enums';
import { formatTime } from '@/common/utils/format';

View File

@@ -1,7 +1,7 @@
<template>
<el-form :model="bizForm" ref="formRef" :rules="rules" label-width="auto">
<el-form-item prop="id" label="DB" required>
<TagTreeResourceSelect
<ResourceSelect
v-bind="$attrs"
v-model="selectRedis"
@change="changeRedis"
@@ -9,7 +9,7 @@
:tag-path-node-type="NodeTypeTagPath"
:placeholder="$t('flow.selectRedisPlaceholder')"
>
</TagTreeResourceSelect>
</ResourceSelect>
</el-form-item>
<el-form-item prop="cmd" label="CMD" required>
@@ -21,12 +21,13 @@
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import TagTreeResourceSelect from '@/views/ops/component/TagTreeResourceSelect.vue';
import ResourceSelect from '@/views/ops/resource/ResourceSelect.vue';
import { NodeType, TagTreeNode } from '@/views/ops/component/tag';
import { redisApi } from '@/views/ops/redis/api';
import { sleep } from '@/common/utils/loading';
import { useI18n } from 'vue-i18n';
import { Rules } from '@/common/rule';
import { RedisIcon } from '@/views/ops/redis/resource';
const { t } = useI18n();
@@ -52,7 +53,7 @@ const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(asyn
await sleep(100);
return redisInfos.map((x: any) => {
x.tagPath = parentNode.key;
return new TagTreeNode(`${x.code}`, x.name, NodeTypeRedis).withParams(x);
return new TagTreeNode(`${x.code}`, x.name, NodeTypeRedis).withParams(x).withIcon(RedisIcon);
});
});
@@ -61,15 +62,18 @@ const NodeTypeRedis = new NodeType(1).withLoadNodesFunc(async (parentNode: TagTr
const redisInfo = parentNode.params;
let dbs: TagTreeNode[] = redisInfo.db.split(',').map((x: string) => {
return new TagTreeNode(x, `db${x}`, 2 as any).withIsLeaf(true).withParams({
id: redisInfo.id,
db: x,
name: `db${x}`,
keys: 0,
tagPath: redisInfo.tagPath,
redisName: redisInfo.name,
code: redisInfo.code,
});
return new TagTreeNode(x, `db${x}`, 2 as any)
.withIsLeaf(true)
.withParams({
id: redisInfo.id,
db: x,
name: `db${x}`,
keys: 0,
tagPath: redisInfo.tagPath,
redisName: redisInfo.name,
code: redisInfo.code,
})
.withIcon({ name: 'Coin', color: '#67c23a' });
});
if (redisInfo.mode == 'cluster') {

Some files were not shown because too many files have changed in this diff Show More