Compare commits
25 Commits
v1.9.4
...
c86f2ad412
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c86f2ad412 | ||
|
|
82fd97e06a | ||
|
|
614a144f60 | ||
|
|
7d344c71e1 | ||
|
|
6ad6c69660 | ||
|
|
e96379b6c0 | ||
|
|
f7480f3bac | ||
|
|
54d3a5b368 | ||
|
|
7eb4d064ea | ||
|
|
cc66fcddf5 | ||
|
|
aac4c2b42b | ||
|
|
7a17042276 | ||
|
|
42fbfd3c47 | ||
|
|
e273ade0b0 | ||
|
|
bcaa4563ac | ||
|
|
e0c01d4561 | ||
|
|
d6280ea280 | ||
|
|
666b191b6c | ||
|
|
778cb7f4de | ||
|
|
142bbd265d | ||
|
|
f676ec9e7b | ||
|
|
44d379a016 | ||
|
|
2170509d92 | ||
|
|
798ab7d18b | ||
|
|
abd2b4bac0 |
@@ -15,11 +15,14 @@
|
||||
<img src="https://img.shields.io/github/stars/dromara/mayfly-go.svg?style=social" alt="github star"/>
|
||||
<img src="https://img.shields.io/github/forks/dromara/mayfly-go.svg?style=social" alt="github fork"/>
|
||||
</a>
|
||||
<a href="https://github.com/dromara/mayfly-go" target="_blank">
|
||||
<img src="https://gitcode.com/dromara/mayfly-go/star/badge.svg" alt="github star"/>
|
||||
</a>
|
||||
<a href="https://hub.docker.com/r/mayflygo/mayfly-go/tags" target="_blank">
|
||||
<img src="https://img.shields.io/docker/pulls/mayflygo/mayfly-go.svg?label=docker%20pulls&color=fac858" alt="docker pulls"/>
|
||||
</a>
|
||||
<a href="https://github.com/golang/go" target="_blank">
|
||||
<img src="https://img.shields.io/badge/Golang-1.22%2B-yellow.svg" alt="golang"/>
|
||||
<img src="https://img.shields.io/badge/Golang-1.24%2B-yellow.svg" alt="golang"/>
|
||||
</a>
|
||||
<a href="https://cn.vuejs.org" target="_blank">
|
||||
<img src="https://img.shields.io/badge/Vue-3.x-green.svg" alt="vue">
|
||||
@@ -28,7 +31,7 @@
|
||||
|
||||
## 前言
|
||||
|
||||
Web版 **统一管理操作平台**,集成了对Linux系统的全面操作支持(包括终端管理[终端回放、命令过滤]、文件管理、脚本执行、进程监控及计划任务设置),同时提供了多种数据库(如 MySQL、PostgreSQL、Oracle、SQL Server、达梦、高斯、SQLite 等)的数据操作、数据同步与数据迁移功能。此外,还支持 Redis(单机、哨兵、集群模式)以及 MongoDB 的操作管理,并结合工单流程审批功能,为企业提供一站式的运维与管理解决方案。
|
||||
Web 版 **统一管理操作平台**,集成了对 Linux 系统的全面操作支持(包括终端管理[终端回放、命令过滤]、文件管理、脚本执行、进程监控及计划任务设置),同时提供了多种数据库(如 MySQL、PostgreSQL、Oracle、SQL Server、达梦、高斯、SQLite 等)的数据操作、数据同步与数据迁移功能。此外,还支持 Redis(单机、哨兵、集群模式)、 MongoDB 、Es 的操作管理,并结合工单流程审批功能,为企业提供一站式的运维与管理解决方案。
|
||||
|
||||
## 开发语言与主要框架
|
||||
|
||||
@@ -106,7 +109,7 @@ http://go.mayfly.run
|
||||
|
||||
## 💌 支持作者
|
||||
|
||||
如果觉得项目不错,或者已经在使用了,希望你可以去 <a target="_blank" href="https://github.com/dromara/mayfly-go">Github</a> 或者 <a target="_blank" href="https://gitee.com/dromara/mayfly-go">Gitee</a> 帮我点个 ⭐ Star,这将是对我极大的鼓励与支持。
|
||||
如果觉得项目不错,或者已经在使用了,希望你可以去 <a target="_blank" href="https://github.com/dromara/mayfly-go">Github</a> 或 <a target="_blank" href="https://gitee.com/dromara/mayfly-go">Gitee</a> 或 <a target="_blank" href="https://gitcode.com/dromara/mayfly-go">Gitcode</a> 帮我点个 ⭐ Star,这将是对我极大的鼓励与支持。
|
||||
|
||||
> 喝杯咖啡 ☕️ 或者来杯奶茶 🧋,让作者更有精神,写出更棒的代码!
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
## Preface
|
||||
|
||||
Web-based **Unified Management and Operation Platform**, integrating comprehensive operation support for Linux systems (including terminal management [terminal playback, command filtering], file management, script execution, process monitoring, and cronjob settings). It also provides data operation, data synchronization, and data migration for multiple databases (such as MySQL, PostgreSQL, Oracle, SQL Server, Dameng, Gauss, SQLite, etc.). Additionally, it supports Redis operations (standalone, sentinel, and cluster modes) and MongoDB management, combined with work order process approval functionality to offer enterprises an all-in-one solution for operations and management.
|
||||
Web-based **Unified Management and Operation Platform**, integrating comprehensive operation support for Linux systems (including terminal management [terminal playback, command filtering], file management, script execution, process monitoring, and cronjob settings). It also provides data operation, data synchronization, and data migration for multiple databases (such as MySQL, PostgreSQL, Oracle, SQL Server, Dameng, Gauss, SQLite, etc.). Additionally, it supports Redis operations (standalone, sentinel, and cluster modes) and MongoDB、Es management, combined with work order process approval functionality to offer enterprises an all-in-one solution for operations and management.
|
||||
|
||||
## Development languages and major frameworks
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -4,8 +4,4 @@ ENV = 'development'
|
||||
VITE_OPEN = true
|
||||
|
||||
# 本地环境接口地址
|
||||
VITE_API_URL = '/api'
|
||||
|
||||
# 路由模式
|
||||
# Optional: hash | history
|
||||
VITE_ROUTER_MODE = hash
|
||||
VITE_API_URL = '/api'
|
||||
@@ -3,7 +3,3 @@ ENV = 'production'
|
||||
|
||||
# 线上环境接口地址
|
||||
VITE_API_URL = '/api'
|
||||
|
||||
# 路由模式
|
||||
# Optional: hash | history
|
||||
VITE_ROUTER_MODE = hash
|
||||
@@ -11,7 +11,7 @@ module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
sourceType: 'module',
|
||||
},
|
||||
extends: ['plugin:vue/vue3-essential', 'plugin:vue/essential', 'eslint:recommended'],
|
||||
extends: ['plugin:vue/essential', 'eslint:recommended'],
|
||||
plugins: ['vue', '@typescript-eslint'],
|
||||
overrides: [
|
||||
{
|
||||
@@ -35,9 +35,8 @@ module.exports = {
|
||||
'@typescript-eslint/ban-types': 'off',
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-redeclare': 'error',
|
||||
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [2],
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'vue/custom-event-name-casing': 'off',
|
||||
'vue/attributes-order': 'off',
|
||||
'vue/one-component-per-file': 'off',
|
||||
@@ -53,6 +52,7 @@ module.exports = {
|
||||
'vue/no-arrow-functions-in-watch': 'off',
|
||||
'vue/no-template-key': 'off',
|
||||
'vue/no-v-html': 'off',
|
||||
'vue/no-unused-vars': 'off',
|
||||
'vue/comment-directive': 'off',
|
||||
'vue/no-parsing-error': 'off',
|
||||
'vue/no-deprecated-v-on-native-modifier': 'off',
|
||||
@@ -67,7 +67,7 @@ module.exports = {
|
||||
'generator-star-spacing': 'off',
|
||||
'no-unreachable': 'off',
|
||||
'no-multiple-template-root': 'off',
|
||||
'no-unused-vars': 'error',
|
||||
'no-unused-vars': 'off',
|
||||
'no-v-model-argument': 'off',
|
||||
'no-case-declarations': 'off',
|
||||
// 'no-console': 'error',
|
||||
|
||||
@@ -10,58 +10,61 @@
|
||||
"lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@vueuse/core": "^13.1.0",
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"@logicflow/core": "^2.1.1",
|
||||
"@logicflow/extension": "^2.1.2",
|
||||
"@vueuse/core": "^13.6.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.9.0",
|
||||
"asciinema-player": "^3.10.0",
|
||||
"axios": "^1.6.2",
|
||||
"clipboard": "^2.0.11",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"echarts": "^5.6.0",
|
||||
"element-plus": "^2.9.7",
|
||||
"echarts": "^6.0.0",
|
||||
"element-plus": "^2.10.7",
|
||||
"js-base64": "^3.7.7",
|
||||
"jsencrypt": "^3.3.2",
|
||||
"mitt": "^3.0.1",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"monaco-sql-languages": "^0.12.2",
|
||||
"monaco-themes": "^0.4.4",
|
||||
"monaco-sql-languages": "^0.15.1",
|
||||
"monaco-themes": "^0.4.6",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^3.0.2",
|
||||
"pinia": "^3.0.3",
|
||||
"qrcode.vue": "^3.6.0",
|
||||
"screenfull": "^6.0.2",
|
||||
"sortablejs": "^1.15.6",
|
||||
"splitpanes": "^4.0.3",
|
||||
"sql-formatter": "^15.4.10",
|
||||
"sql-formatter": "^15.6.5",
|
||||
"trzsz": "^1.1.5",
|
||||
"uuid": "^9.0.1",
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^11.1.3",
|
||||
"vue-router": "^4.5.0",
|
||||
"uuid": "^11.1.0",
|
||||
"vue": "^v3.6.0-alpha.2",
|
||||
"vue-i18n": "^11.1.11",
|
||||
"vue-router": "^4.5.1",
|
||||
"vuedraggable": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@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.3",
|
||||
"@vue/compiler-sfc": "^3.5.13",
|
||||
"code-inspector-plugin": "^0.4.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint": "^8.35.0",
|
||||
"eslint-plugin-vue": "^10.0.0",
|
||||
"prettier": "^3.2.5",
|
||||
"sass": "^1.86.3",
|
||||
"typescript": "^5.8.2",
|
||||
"vite": "^6.2.6",
|
||||
"@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": "^1.0.4",
|
||||
"eslint": "^9.29.0",
|
||||
"eslint-plugin-vue": "^10.4.0",
|
||||
"postcss": "^8.5.6",
|
||||
"prettier": "^3.6.1",
|
||||
"sass": "^1.90.0",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"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%",
|
||||
|
||||
6
frontend/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 9.4 KiB |
@@ -1,39 +1,34 @@
|
||||
<template>
|
||||
<el-config-provider :size="getGlobalComponentSize" :locale="getGlobalI18n">
|
||||
<div class="h100">
|
||||
<el-watermark
|
||||
:zIndex="10000000"
|
||||
:width="210"
|
||||
v-if="themeConfig.isWatermark"
|
||||
:font="{ color: 'rgba(180, 180, 180, 0.3)' }"
|
||||
:content="themeConfig.watermarkText"
|
||||
class="h100"
|
||||
>
|
||||
<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,
|
||||
|
||||
1
frontend/src/assets/icon/es/es-color.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M96.426667 649.173333H712.96a137.173333 137.173333 0 0 0 0-274.346666H96.426667c-12.8 43.52-19.626667 89.514667-19.626667 137.173333s6.826667 93.696 19.626667 137.173333z" fill="#07A5DE" p-id="6101"></path><path d="M563.2 25.6A486.4 486.4 0 0 0 125.354667 299.946667H837.546667c52.096 0 97.450667-29.013333 120.661333-71.808A485.76 485.76 0 0 0 563.2 25.6z" fill="#EFBF19" p-id="6102"></path><path d="M942.421333 816.64a137.258667 137.258667 0 0 0-129.749333-92.586667H125.312A486.4 486.4 0 0 0 563.2 998.4c153.344 0 290.090667-70.954667 379.221333-181.76z" fill="#3EBEB1" p-id="6103"></path><path d="M506.197333 649.173333c12.8-43.52 19.626667-89.514667 19.626667-137.173333s-6.826667-93.696-19.626667-137.173333H96.469333c-12.8 43.52-19.626667 89.514667-19.626666 137.173333s6.826667 93.696 19.626666 137.173333h409.728z" fill="#231F20" p-id="6104"></path><path d="M477.269333 724.053333H125.354667a488.533333 488.533333 0 0 0 175.957333 197.888 488.533333 488.533333 0 0 0 175.957333-197.930666z" fill="#019B8F" p-id="6105"></path><path d="M301.312 102.058667a488.533333 488.533333 0 0 1 175.957333 197.930666H125.354667a488.533333 488.533333 0 0 1 175.957333-197.930666z" fill="#D8A22A" p-id="6106"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
frontend/src/assets/icon/es/es.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M465.664 679.168c105.301333 0.597333 172.970667 1.066667 202.922667 1.450667 20.48 0.256 36.181333 0.426667 47.274666 0.469333h3.84c45.824 0 84.096 8.533333 114.901334 25.258667 31.189333 16.938667 54.826667 42.368 70.826666 76.245333l1.152 2.517333a24.106667 24.106667 0 0 1-1.621333 3.413334c-46.336 67.84-101.034667 116.565333-164.096 146.346666-63.146667 29.824-134.613333 40.704-214.485333 32.469334-159.232-16.384-283.477333-106.24-372.352-269.994667a5.973333 5.973333 0 0 1 3.584-8.618667c13.653333-3.968 27.733333-6.528 41.941333-7.594666 91.306667-1.365333 170.538667-1.877333 238.165333-1.962667h27.946667z m44.885333 63.829333l-0.64 1.152c-3.754667 6.485333-9.386667 15.36-16.128 25.6l-2.645333 3.925334-1.578667 2.346666c-21.205333 31.445333-51.072 72.234667-70.784 94.464 64.853333 34.304 133.162667 45.44 227.157334 27.52 95.146667-18.090667 145.450667-52.565333 175.829333-114.090666-5.034667-10.581333-14.592-19.285333-31.488-27.733334-12.8-6.4-32.426667-11.050667-58.752-14.250666l-221.013333 1.066666z m-257.578666-5.546666l1.237333 1.536c21.504 26.112 67.712 72.277333 96.896 95.786666 15.146667-14.08 29.098667-29.397333 41.642667-45.824 13.952-18.261333 24.149333-32.64 35.370666-52.821333l-175.146666 1.322667z m471.296-360.874667c38.229333 5.077333 67.626667 18.944 88.448 41.301333 20.736 22.229333 33.024 52.992 36.565333 92.373334 3.626667 39.722667-5.76 71.808-27.733333 96.426666-20.906667 23.381333-53.461333 40.106667-97.877334 49.706667l-2.645333 0.597333-2.816 0.554667H144.725333a8.021333 8.021333 0 0 1-7.893333-6.485333 1545.173333 1545.173333 0 0 1-0.298667-1.578667c-12.373333-62.378667-18.517333-106.666667-18.517333-132.906667 0-38.570667 5.888-81.962667 17.706667-130.261333l1.066666-4.394667a7.082667 7.082667 0 0 1 6.826667-5.333333h580.650667zM197.546667 442.88l-0.853334 2.688c-7.509333 24.064-12.544 44.330667-12.117333 70.954667 0 30.293333 5.418667 54.272 13.653333 81.664h283.050667l0.341333-2.218667 0.469334-3.2c3.541333-24.448 4.010667-47.701333 4.010666-76.544 0-30.805333-1.066667-51.541333-6.4-75.264l-282.154666 1.92z m493.397333-3.029333l-131.797333 1.024 0.512 2.474666c4.48 22.357333 6.741333 43.861333 6.741333 73.216 0 30.421333-2.432 53.76-7.552 79.189334l134.826667-0.170667 1.962666-0.213333c28.16-2.901333 49.194667-7.210667 62.421334-23.04 11.52-13.866667 17.152-32.469333 17.152-55.765334 0-24.746667-6.272-42.624-19.456-54.826666-13.653333-12.714667-34.474667-19.2-61.994667-21.674667l-2.816-0.213333z m49.877333-342.784c63.104 29.824 117.845333 78.592 164.181334 146.346666l1.536 2.304a23.466667 23.466667 0 0 1-1.066667 3.669334c-16 33.92-39.594667 59.306667-70.784 76.245333-30.805333 16.768-69.12 25.258667-114.986667 25.258667-11.178667 0-28.16 0.213333-50.944 0.469333-45.098667 0.597333-112.597333 1.408-202.965333 1.493333h-14.122667c-70.613333 0-154.453333-0.512-251.733333-1.962666a207.061333 207.061333 0 0 1-42.24-7.594667 5.973333 5.973333 0 0 1-3.626667-8.618667C242.986667 170.922667 367.146667 81.066667 526.378667 64.682667c79.829333-8.277333 151.296 2.56 214.4 32.426666z m-102.101333 28.501333c-85.205333-15.36-143.957333-4.010667-213.717333 27.221333 11.648 13.312 26.410667 33.621333 40.874666 55.04l1.578667 2.389334 2.56 3.754666 3.498667 5.376 2.346666 3.584 1.237334 1.877334c18.688 28.757333 35.157333 56.746667 41.728 70.613333h213.674666l2.474667-0.298667 2.56-0.341333c21.290667-2.986667 38.144-10.794667 55.978667-19.754667 17.408-8.576 30.122667-18.304 40.106666-29.866666-49.493333-63.018667-108.586667-104.106667-194.901333-119.594667zM367.744 186.453333c-12.458667 10.069333-34.304 29.44-56.192 50.048l-1.706667 1.621334-3.328 3.157333-3.498666 3.328-1.877334 1.792-2.048 2.005333c-17.322667 16.64-33.578667 33.109333-44.501333 45.909334l179.797333-1.536-1.109333-1.877334a3067.264 3067.264 0 0 1-12.672-21.418666l-11.776-20.053334-2.474667-4.053333-2.56-4.266667-1.152-2.005333-1.237333-2.005333c-12.458667-20.693333-24.917333-40.405333-33.706667-50.645334z" fill="#2c2c2c" p-id="5739"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 8.4 KiB |
@@ -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 |
@@ -1,5 +1,5 @@
|
||||
import request from './request';
|
||||
import { useApiFetch } from '@/hooks/useRequest';
|
||||
import { RequestOptions, useApiFetch } from '@/hooks/useRequest';
|
||||
|
||||
/**
|
||||
* 可用于各模块定义各自api请求
|
||||
@@ -49,7 +49,7 @@ class Api {
|
||||
* @param reqOptions 其他可选值
|
||||
* @returns
|
||||
*/
|
||||
useApi<T>(params: any = null, reqOptions: RequestInit = {}) {
|
||||
useApi<T>(params: any = null, reqOptions?: RequestOptions) {
|
||||
return useApiFetch<T>(this, params, reqOptions);
|
||||
}
|
||||
|
||||
@@ -59,8 +59,8 @@ class Api {
|
||||
*/
|
||||
async request(param: any = null, options: any = {}): Promise<any> {
|
||||
const { execute, data } = this.useApi(param, options);
|
||||
await execute();
|
||||
return data.value;
|
||||
const res = await execute();
|
||||
return data.value || res;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
@@ -1,10 +1,12 @@
|
||||
import { i18n } from '@/i18n';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
/**
|
||||
* 不符合业务断言错误
|
||||
*/
|
||||
class AssertError extends Error {
|
||||
constructor(message: string) {
|
||||
ElMessage.error(message);
|
||||
super(message);
|
||||
// 错误类名
|
||||
this.name = 'AssertError';
|
||||
@@ -15,11 +17,11 @@ class AssertError extends Error {
|
||||
* 断言表达式为true
|
||||
*
|
||||
* @param condition 条件表达式
|
||||
* @param msg 错误消息
|
||||
* @param msgOrI18nKey 错误消息 或者 i18n key
|
||||
*/
|
||||
export function isTrue(condition: boolean, msg: string) {
|
||||
export function isTrue(condition: boolean, msgOrI18nKey: string) {
|
||||
if (!condition) {
|
||||
throw new AssertError(msg);
|
||||
throw new AssertError(i18n.global.t(msgOrI18nKey));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,12 +9,19 @@ 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(),
|
||||
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(),
|
||||
};
|
||||
|
||||
// 标签关联的资源类型
|
||||
@@ -24,9 +31,10 @@ export const TagResourceTypeEnum = {
|
||||
|
||||
Machine: ResourceTypeEnum.Machine,
|
||||
DbInstance: ResourceTypeEnum.Db,
|
||||
EsInstance: ResourceTypeEnum.Es,
|
||||
Redis: ResourceTypeEnum.Redis,
|
||||
Mongo: ResourceTypeEnum.Mongo,
|
||||
AuthCert: EnumValue.of(5, '授权凭证').setExtra({ icon: 'Ticket', iconColor: 'var(--el-color-success)' }),
|
||||
AuthCert: ResourceTypeEnum.AuthCert,
|
||||
|
||||
Db: EnumValue.of(22, '数据库').setExtra({ icon: 'Coin' }),
|
||||
};
|
||||
@@ -37,4 +45,33 @@ export const TagResourceTypePath = {
|
||||
|
||||
DbInstanceAuthCert: `${TagResourceTypeEnum.DbInstance.value}/${TagResourceTypeEnum.AuthCert.value}`,
|
||||
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',
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ const config = {
|
||||
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
|
||||
|
||||
// 系统版本
|
||||
version: 'v1.9.4',
|
||||
version: 'v1.10.2',
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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()}`;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -42,4 +42,5 @@ export function exportFile(filename: string, content: string) {
|
||||
link.setAttribute('download', `${filename}`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link); // 下载完成后移除元素
|
||||
}
|
||||
|
||||
@@ -30,6 +30,18 @@ export function formatByteSize(size: number, fixed = 2) {
|
||||
return parseFloat((size / Math.pow(base, exponent)).toFixed(fixed)) + units[exponent];
|
||||
}
|
||||
|
||||
export function formatDocSize(size: number, fixed = 2) {
|
||||
if (size === 0) {
|
||||
return '0';
|
||||
}
|
||||
|
||||
const units = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'];
|
||||
const base = 1000;
|
||||
const exponent = Math.floor(Math.log(size) / Math.log(base));
|
||||
|
||||
return parseFloat((size / Math.pow(base, exponent)).toFixed(fixed)) + units[exponent];
|
||||
}
|
||||
|
||||
/**
|
||||
* 容量转为对应的字节大小,如 1KB转为 1024
|
||||
* @param sizeString 1kb 1gb等
|
||||
@@ -86,8 +98,8 @@ export function formatTime(time: number, unit: string = 's') {
|
||||
let result = '';
|
||||
|
||||
const timeUnits = Object.entries(units).map(([unit, duration]) => {
|
||||
const value = Math.floor(seconds / duration);
|
||||
seconds %= duration;
|
||||
const value = Math.floor(seconds / (duration as any));
|
||||
seconds %= duration as any;
|
||||
return { value, unit };
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
// https://www.npmjs.com/package/mitt
|
||||
import mitt, { Emitter } from 'mitt';
|
||||
|
||||
// 类型
|
||||
const emitter: Emitter<any> = mitt<any>();
|
||||
|
||||
// 导出
|
||||
export default emitter;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
:style="`top: ${state.dropdown.y + 5}px;left: ${state.dropdown.x}px;`"
|
||||
:key="Math.random()"
|
||||
v-show="state.isShow && !allHide"
|
||||
@contextmenu="headerContextmenuClick"
|
||||
>
|
||||
<ul class="el-dropdown-menu">
|
||||
<template v-for="(v, k) in state.dropdownList">
|
||||
@@ -125,6 +126,10 @@ const onCurrentContextmenuClick = (ci: ContextmenuItem) => {
|
||||
emit('currentContextmenuClick', { id: ci.clickId, item: state.item });
|
||||
};
|
||||
|
||||
const headerContextmenuClick = (event: any, data: any) => {
|
||||
event.preventDefault(); // 阻止默认的右击菜单行为
|
||||
};
|
||||
|
||||
// 打开右键菜单:判断是否固定,固定则不显示关闭按钮
|
||||
const openContextmenu = (item: any) => {
|
||||
state.item = item;
|
||||
|
||||
@@ -37,9 +37,9 @@
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<div class="flex-align-center w100">
|
||||
<el-radio v-model="radioValue" :label="7" class="mr5"> {{ $t('components.crontab.appoint') }} </el-radio>
|
||||
<el-select @click="radioValue = 7" class="w100" clearable v-model="checkboxList" multiple>
|
||||
<div class="flex items-center w-full">
|
||||
<el-radio v-model="radioValue" :label="7" class="mr-1"> {{ $t('components.crontab.appoint') }} </el-radio>
|
||||
<el-select @click="radioValue = 7" class="!w-full" clearable v-model="checkboxList" multiple>
|
||||
<el-option v-for="item in 31" :key="item" :value="`${item}`">{{ item }}</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
@@ -22,9 +22,9 @@
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<div class="flex-align-center w100">
|
||||
<el-radio v-model="radioValue" :label="4" class="mr5"> {{ $t('components.crontab.appoint') }} </el-radio>
|
||||
<el-select @click="radioValue = 4" class="w100" clearable v-model="checkboxList" multiple>
|
||||
<div class="flex items-center w-full">
|
||||
<el-radio v-model="radioValue" :label="4" class="mr-1"> {{ $t('components.crontab.appoint') }} </el-radio>
|
||||
<el-select @click="radioValue = 4" class="!w-full" clearable v-model="checkboxList" multiple>
|
||||
<el-option v-for="item in 60" :key="item" :value="`${item - 1}`">{{ item - 1 }}</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
@@ -22,9 +22,9 @@
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<div class="flex-align-center w100">
|
||||
<el-radio v-model="radioValue" :label="4" class="mr5"> {{ $t('components.crontab.appoint') }} </el-radio>
|
||||
<el-select @click="radioValue = 4" class="w100" clearable v-model="checkboxList" multiple>
|
||||
<div class="flex items-center w-full">
|
||||
<el-radio v-model="radioValue" :label="4" class="mr-1"> {{ $t('components.crontab.appoint') }} </el-radio>
|
||||
<el-select @click="radioValue = 4" class="!w-full" clearable v-model="checkboxList" multiple>
|
||||
<el-option v-for="item in 60" :key="item" :value="`${item - 1}`">{{ item - 1 }}</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
@@ -22,9 +22,9 @@
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<div class="flex-align-center w100">
|
||||
<el-radio v-model="radioValue" :label="4" class="mr5"> {{ $t('components.crontab.appoint') }} </el-radio>
|
||||
<el-select @click="radioValue = 4" class="w100" clearable v-model="checkboxList" multiple>
|
||||
<div class="flex items-center w-full">
|
||||
<el-radio v-model="radioValue" :label="4" class="mr-1"> {{ $t('components.crontab.appoint') }} </el-radio>
|
||||
<el-select @click="radioValue = 4" class="!w-full" clearable v-model="checkboxList" multiple>
|
||||
<el-option v-for="item in 12" :key="item" :value="`${item}`">{{ item }}</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
@@ -22,9 +22,9 @@
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<div class="flex-align-center w100">
|
||||
<el-radio v-model="radioValue" :label="4" class="mr5"> {{ $t('components.crontab.appoint') }} </el-radio>
|
||||
<el-select @click="radioValue = 4" class="w100" clearable v-model="checkboxList" multiple>
|
||||
<div class="flex items-center w-full">
|
||||
<el-radio v-model="radioValue" :label="4" class="mr-1"> {{ $t('components.crontab.appoint') }} </el-radio>
|
||||
<el-select @click="radioValue = 4" class="!w-full" clearable v-model="checkboxList" multiple>
|
||||
<el-option v-for="item in 60" :key="item" :value="`${item - 1}`">{{ item - 1 }}</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
@@ -32,9 +32,9 @@
|
||||
</el-form-item> -->
|
||||
|
||||
<el-form-item>
|
||||
<div class="flex-align-center w100">
|
||||
<el-radio v-model="radioValue" :label="6" class="mr5"> {{ $t('components.crontab.appoint') }} </el-radio>
|
||||
<el-select @click="radioValue = 6" class="w100" clearable v-model="checkboxList" multiple>
|
||||
<div class="flex items-center w-full">
|
||||
<el-radio v-model="radioValue" :label="6" class="mr-1"> {{ $t('components.crontab.appoint') }} </el-radio>
|
||||
<el-select @click="radioValue = 6" class="!w-full" clearable v-model="checkboxList" multiple>
|
||||
<el-option v-for="(item, index) of weekList" :label="item" :key="index" :value="`${index + 1}`">{{ $t(item) }}</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
@@ -26,9 +26,9 @@
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<div class="flex-align-center w100">
|
||||
<el-radio v-model="radioValue" :label="5" class="mr5"> {{ $t('components.crontab.appoint') }} </el-radio>
|
||||
<el-select @click="radioValue = 5" class="w100" clearable v-model="checkboxList" multiple>
|
||||
<div class="flex items-center w-full">
|
||||
<el-radio v-model="radioValue" :label="5" class="mr-1"> {{ $t('components.crontab.appoint') }} </el-radio>
|
||||
<el-select @click="radioValue = 5" class="!w-full" clearable v-model="checkboxList" multiple>
|
||||
<el-option v-for="item in 9" :key="item" :value="`${item - 1 + fullYear}`" :label="item - 1 + fullYear" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="dynamic-form-edit w100">
|
||||
<el-table :data="formItems" stripe class="w100">
|
||||
<div class="dynamic-form-edit !w-full">
|
||||
<el-table :data="formItems" stripe class="!w-full">
|
||||
<el-table-column prop="name" label="model" min-width="100px">
|
||||
<template #header>
|
||||
<el-button class="ml0" type="primary" circle size="small" icon="Plus" @click="addItem()"> </el-button>
|
||||
<span class="ml10">model field</span>
|
||||
<span class="ml-2">model field</span>
|
||||
</template>
|
||||
<template #default="scope">
|
||||
<el-input v-model="scope.row['model']" :placeholder="$t('components.df.fieldModelPlaceholder')" clearable> </el-input>
|
||||
|
||||
@@ -12,8 +12,9 @@ const props = defineProps({
|
||||
required: true,
|
||||
},
|
||||
value: {
|
||||
type: [Object, String, Number],
|
||||
type: [Object, String, Number, null],
|
||||
required: true,
|
||||
default: () => null,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -40,7 +41,7 @@ onMounted(() => {
|
||||
});
|
||||
|
||||
const convert = (value: any) => {
|
||||
const enumValue = EnumValue.getEnumByValue(props.enums, value) as any;
|
||||
const enumValue = EnumValue.getEnumByValue(props.enums, value);
|
||||
if (!enumValue) {
|
||||
state.enumLabel = '-';
|
||||
state.type = 'danger';
|
||||
@@ -50,8 +51,8 @@ const convert = (value: any) => {
|
||||
|
||||
state.enumLabel = enumValue?.label || '';
|
||||
if (enumValue.tag) {
|
||||
state.color = enumValue.tag.color;
|
||||
state.type = enumValue.tag.type;
|
||||
state.color = enumValue.tag.color || '';
|
||||
state.type = enumValue.tag.type || defaultType;
|
||||
} else {
|
||||
state.type = defaultType;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="icon-selector w100 h100">
|
||||
<div class="icon-selector !w-full !h-full">
|
||||
<el-input
|
||||
v-model="state.fontIconSearch"
|
||||
:placeholder="state.fontIconPlaceholder"
|
||||
@@ -12,7 +12,7 @@
|
||||
@blur="onIconBlur"
|
||||
>
|
||||
<template #prepend>
|
||||
<SvgIcon :name="state.fontIconPrefix === '' ? prepend : state.fontIconPrefix" class="font14" />
|
||||
<SvgIcon :name="state.fontIconPrefix === '' ? prepend : state.fontIconPrefix" class="!text-[14px]" />
|
||||
</template>
|
||||
</el-input>
|
||||
<el-popover
|
||||
@@ -25,7 +25,7 @@
|
||||
virtual-triggering
|
||||
>
|
||||
<template #default>
|
||||
<div class="ml5 mt5">{{ $t(title) }}</div>
|
||||
<div class="ml-1 mt-1">{{ $t(title) }}</div>
|
||||
<div class="icon-selector-warp">
|
||||
<el-tabs v-model="state.fontIconTabActive" @tab-click="onIconClick">
|
||||
<el-tab-pane lazy label="ele" name="ele">
|
||||
|
||||
129
frontend/src/components/message/message.ts
Normal 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 || '');
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -1,15 +1,15 @@
|
||||
<template>
|
||||
<div class="monaco-editor" style="border: 1px solid var(--el-border-color-light, #ebeef5); height: 100%">
|
||||
<div class="monaco-editor-custom relative h-full">
|
||||
<div class="monaco-editor-content" ref="monacoTextareaRef" :style="{ height: height }"></div>
|
||||
<el-select v-if="canChangeMode" class="code-mode-select" v-model="languageMode" @change="changeLanguage" filterable>
|
||||
<el-option v-for="mode in languageArr" :key="mode.value" :label="mode.label" :value="mode.value"> </el-option>
|
||||
<el-option v-for="mode in languageArr" :key="mode.value" :label="mode.label" :value="mode.value" />
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch, toRefs, reactive, onMounted, onBeforeUnmount, useTemplateRef, Ref } from 'vue';
|
||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
import * as monaco from 'monaco-editor';
|
||||
// 相关语言
|
||||
import 'monaco-editor/esm/vs/basic-languages/shell/shell.contribution.js';
|
||||
import 'monaco-editor/esm/vs/basic-languages/yaml/yaml.contribution.js';
|
||||
@@ -31,7 +31,6 @@ import 'monaco-editor/esm/vs/editor/contrib/format//browser/formatActions.js';
|
||||
// 提示
|
||||
import 'monaco-editor/esm/vs/editor/contrib/suggest/browser/suggestController.js';
|
||||
import 'monaco-editor/esm/vs/editor/contrib/suggest/browser/suggestInlineCompletions.js';
|
||||
|
||||
import { editor, languages } from 'monaco-editor';
|
||||
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker.js?worker';
|
||||
import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
|
||||
@@ -134,6 +133,7 @@ const defaultOptions = {
|
||||
theme: 'SolarizedLight',
|
||||
automaticLayout: true, //自适应宽高布局
|
||||
foldingStrategy: 'indentation', //代码可分小段折叠
|
||||
folding: true,
|
||||
roundedSelection: false, // 禁用选择文本背景的圆角
|
||||
matchBrackets: 'near',
|
||||
linkedEditing: true,
|
||||
@@ -149,7 +149,13 @@ const defaultOptions = {
|
||||
minimap: {
|
||||
enabled: false, // 不要小地图
|
||||
},
|
||||
};
|
||||
renderLineHighlight: 'all',
|
||||
selectOnLineNumbers: false,
|
||||
readOnly: false,
|
||||
scrollBeyondLastLine: false,
|
||||
lineNumbers: 'on',
|
||||
lineNumbersMinChars: 3,
|
||||
} as editor.IStandaloneEditorConstructionOptions;
|
||||
|
||||
const monacoTextareaRef: Ref<any> = useTemplateRef('monacoTextareaRef');
|
||||
|
||||
@@ -221,7 +227,8 @@ const initMonacoEditorIns = () => {
|
||||
monaco.editor.defineTheme('SolarizedLight', SolarizedLight);
|
||||
defaultOptions.language = state.languageMode;
|
||||
defaultOptions.theme = themeConfig.value.editorTheme;
|
||||
monacoEditorIns = monaco.editor.create(monacoTextareaRef.value, Object.assign(defaultOptions, props.options as any));
|
||||
let options = Object.assign(defaultOptions, props.options as any);
|
||||
monacoEditorIns = monaco.editor.create(monacoTextareaRef.value, options);
|
||||
|
||||
// 监听内容改变,双向绑定
|
||||
monacoEditorIns.onDidChangeModelContent(() => {
|
||||
@@ -309,7 +316,7 @@ defineExpose({ getEditor, format, focus });
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.monaco-editor {
|
||||
.monaco-editor-custom {
|
||||
.code-mode-select {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
@@ -317,5 +324,8 @@ defineExpose({ getEditor, format, focus });
|
||||
top: 10px;
|
||||
max-width: 130px;
|
||||
}
|
||||
|
||||
border: 1px solid var(--el-border-color-light, #ebeef5);
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<template>
|
||||
<div>
|
||||
<transition name="el-zoom-in-top">
|
||||
<div class="h-full flex flex-col flex-1 overflow-hidden">
|
||||
<transition name="page-table-search-form">
|
||||
<!-- 查询表单 -->
|
||||
<SearchForm v-if="isShowSearch" :items="tableSearchItems" v-model="queryForm" :search="search"
|
||||
:reset="reset" :search-col="searchCol">
|
||||
<SearchForm v-if="isShowSearch" :items="tableSearchItems" v-model="queryForm" :search="search" :reset="reset" :search-col="searchCol">
|
||||
<!-- 遍历父组件传入的 solts 透传给子组件 -->
|
||||
<template v-for="(_, key) in useSlots()" v-slot:[key]>
|
||||
<slot :name="key"></slot>
|
||||
@@ -11,83 +10,104 @@
|
||||
</SearchForm>
|
||||
</transition>
|
||||
|
||||
<div class="card">
|
||||
<div class="table-main">
|
||||
<!-- 表格头部 操作按钮 -->
|
||||
<div class="table-header">
|
||||
<div class="header-button-lf">
|
||||
<slot name="tableHeader" />
|
||||
</div>
|
||||
|
||||
<div v-if="toolButton" class="header-button-ri">
|
||||
<slot name="toolButton">
|
||||
<div class="tool-button">
|
||||
<!-- 简易单个搜索项 -->
|
||||
<div v-if="nowSearchItem" class="simple-search-form">
|
||||
<el-dropdown v-if="searchItems?.length > 1">
|
||||
<SvgIcon :size="16" name="CaretBottom" class="mr4 mt6 simple-search-form-btn" />
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item v-for="searchItem in searchItems"
|
||||
:key="searchItem.prop" @click="changeSimpleFormItem(searchItem)">
|
||||
{{ $t(searchItem.label) }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
|
||||
<div class="simple-search-form-label mt5">
|
||||
<el-text truncated tag="b">{{ `${$t(nowSearchItem?.label)} : ` }}</el-text>
|
||||
</div>
|
||||
|
||||
<el-form-item style="width: 200px" :key="nowSearchItem.prop">
|
||||
<SearchFormItem @keyup.enter.native="searchFormItemKeyUpEnter"
|
||||
v-if="!nowSearchItem.slot" :item="nowSearchItem"
|
||||
v-model="queryForm[nowSearchItem.prop]" />
|
||||
|
||||
<slot @keyup.enter.native="searchFormItemKeyUpEnter" v-else
|
||||
:name="nowSearchItem.slot">
|
||||
</slot>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<el-button v-if="showToolButton('search') && searchItems?.length" icon="Search"
|
||||
circle @click="search" />
|
||||
|
||||
<!-- <el-button v-if="showToolButton('refresh')" icon="Refresh" circle @click="execQuery()" /> -->
|
||||
|
||||
<el-button v-if="showToolButton('search') && searchItems?.length > 1"
|
||||
:icon="isShowSearch ? 'ArrowDown' : 'ArrowUp'" circle
|
||||
@click="isShowSearch = !isShowSearch" />
|
||||
|
||||
<el-popover placement="bottom" title="表格配置"
|
||||
popper-style="max-height: 550px; overflow: auto; max-width: 450px" width="auto"
|
||||
trigger="click">
|
||||
<div v-for="(item, index) in tableColumns" :key="index">
|
||||
<el-checkbox v-model="item.show" :label="item.label" :true-value="true"
|
||||
:false-value="false" />
|
||||
</div>
|
||||
<template #reference>
|
||||
<el-button icon="Operation" circle :size="props.size"></el-button>
|
||||
</template>
|
||||
</el-popover>
|
||||
</div>
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
<el-card class="h-full" body-class="h-full flex flex-col">
|
||||
<!-- 表格头部 操作按钮 -->
|
||||
<div class="flex justify-between">
|
||||
<div>
|
||||
<slot name="tableHeader" />
|
||||
</div>
|
||||
|
||||
<el-table ref="tableRef" v-bind="$attrs" :max-height="tableMaxHeight"
|
||||
@selection-change="handleSelectionChange" :data="tableData" highlight-current-row
|
||||
v-loading="loading" :size="props.size as any" :border="border">
|
||||
<slot v-if="toolButton" name="toolButton">
|
||||
<div class="flex">
|
||||
<!-- 简易单个搜索项 -->
|
||||
<div v-if="nowSearchItem" class="flex">
|
||||
<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>
|
||||
<el-dropdown-item v-for="searchItem in searchItems" :key="searchItem.prop" @click="changeSimpleFormItem(searchItem)">
|
||||
{{ $t(searchItem.label) }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
|
||||
<div class="text-right mr-1.5 mt-1">
|
||||
<el-text truncated tag="b">{{ `${$t(nowSearchItem?.label)} : ` }}</el-text>
|
||||
</div>
|
||||
|
||||
<el-form-item class="w-[200px]" :key="nowSearchItem.prop">
|
||||
<SearchFormItem
|
||||
@keyup.enter.native="searchFormItemKeyUpEnter"
|
||||
v-if="!nowSearchItem.slot"
|
||||
:item="nowSearchItem"
|
||||
v-model="queryForm[nowSearchItem.prop]"
|
||||
/>
|
||||
|
||||
<slot @keyup.enter.native="searchFormItemKeyUpEnter" v-else :name="nowSearchItem.slot"> </slot>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="ml-2">
|
||||
<el-button v-if="showToolButton('search') && searchItems?.length" icon="Search" circle @click="search" />
|
||||
|
||||
<!-- <el-button v-if="showToolButton('refresh')" icon="Refresh" circle @click="execQuery()" /> -->
|
||||
|
||||
<el-button
|
||||
v-if="showToolButton('search') && props.searchItems?.length > 1"
|
||||
:icon="isShowSearch ? 'ArrowDown' : 'ArrowUp'"
|
||||
circle
|
||||
@click="isShowSearch = !isShowSearch"
|
||||
/>
|
||||
|
||||
<el-popover
|
||||
placement="bottom"
|
||||
title="表格配置"
|
||||
popper-style="max-height: 550px; overflow: auto; max-width: 450px"
|
||||
width="auto"
|
||||
trigger="click"
|
||||
>
|
||||
<div v-for="(item, index) in tableColumns" :key="index">
|
||||
<el-checkbox v-model="item.show" :label="$t(item.label)" :true-value="1" :false-value="0" />
|
||||
</div>
|
||||
<template #reference>
|
||||
<el-button icon="Operation" circle :size="props.size"></el-button>
|
||||
</template>
|
||||
</el-popover>
|
||||
</div>
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-auto">
|
||||
<el-table
|
||||
v-show="showTable"
|
||||
ref="tableRef"
|
||||
v-bind="$attrs"
|
||||
height="100%"
|
||||
@selection-change="handleSelectionChange"
|
||||
:data="tableData"
|
||||
highlight-current-row
|
||||
v-loading="loading"
|
||||
:size="props.size as any"
|
||||
:border="border"
|
||||
>
|
||||
<el-table-column v-if="props.showSelection" :selectable="selectable" type="selection" width="40" />
|
||||
|
||||
<template v-for="(item, index) in tableColumns">
|
||||
<el-table-column :key="index" v-if="item.show" :prop="item.prop" :label="$t(item.label)"
|
||||
:fixed="item.fixed" :align="item.align" :show-overflow-tooltip="item.showOverflowTooltip"
|
||||
:min-width="item.minWidth" :sortable="item.sortable || false" :type="item.type"
|
||||
:width="item.width">
|
||||
<el-table-column
|
||||
:key="index"
|
||||
v-if="item.show"
|
||||
:prop="item.prop"
|
||||
:label="$t(item.label)"
|
||||
:fixed="item.fixed"
|
||||
:align="item.align"
|
||||
:show-overflow-tooltip="item.showOverflowTooltip"
|
||||
:min-width="item.minWidth"
|
||||
:sortable="item.sortable || false"
|
||||
:type="item.type"
|
||||
:width="item.width"
|
||||
>
|
||||
<!-- 插槽:预留功能 -->
|
||||
<template #default="scope" v-if="item.slot">
|
||||
<slot :name="item.slotName ? item.slotName : item.prop" :data="scope.row"></slot>
|
||||
@@ -95,21 +115,29 @@
|
||||
|
||||
<!-- 枚举类型使用tab展示 -->
|
||||
<template #default="scope" v-else-if="item.type == 'tag'">
|
||||
<enum-tag :size="props.size" :enums="item.typeParam"
|
||||
:value="item.getValueByData(scope.row)"></enum-tag>
|
||||
<enum-tag :size="props.size" :enums="item.typeParam" :value="item.getValueByData(scope.row)"></enum-tag>
|
||||
</template>
|
||||
|
||||
<template #default="scope" v-else>
|
||||
<!-- 配置了美化文本按钮以及文本内容大于指定长度,则显示美化按钮 -->
|
||||
<el-popover v-if="item.isBeautify && item.getValueByData(scope.row)?.length > 35"
|
||||
effect="light" trigger="click" placement="top" width="600px">
|
||||
<el-popover
|
||||
v-if="item.isBeautify && item.getValueByData(scope.row)?.length > 35"
|
||||
effect="light"
|
||||
trigger="click"
|
||||
placement="top"
|
||||
width="600px"
|
||||
>
|
||||
<template #default>
|
||||
<el-input :autosize="{ minRows: 3, maxRows: 15 }" disabled v-model="formatVal"
|
||||
type="textarea" />
|
||||
<el-input :autosize="{ minRows: 3, maxRows: 15 }" disabled v-model="formatVal" type="textarea" />
|
||||
</template>
|
||||
<template #reference>
|
||||
<el-link @click="formatText(item.getValueByData(scope.row))" :underline="false"
|
||||
type="success" icon="MagicStick" class="mr5"></el-link>
|
||||
<el-link
|
||||
@click="formatText(item.getValueByData(scope.row))"
|
||||
underline="never"
|
||||
type="success"
|
||||
icon="MagicStick"
|
||||
class="mr-1"
|
||||
></el-link>
|
||||
</template>
|
||||
</el-popover>
|
||||
|
||||
@@ -120,38 +148,42 @@
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<el-row v-if="props.pageable" class="mt20" type="flex" justify="end">
|
||||
<el-pagination :small="props.size == 'small'" @current-change="pageNumChange"
|
||||
@size-change="pageSizeChange" style="text-align: right" layout="prev, pager, next, total, sizes"
|
||||
:total="total" v-model:current-page="queryForm.pageNum" v-model:page-size="queryForm.pageSize"
|
||||
:page-sizes="pageSizes" />
|
||||
<el-row v-if="props.pageable" class="mt-4" type="flex" justify="end">
|
||||
<el-pagination
|
||||
:small="props.size == 'small'"
|
||||
@current-change="pageNumChange"
|
||||
@size-change="pageSizeChange"
|
||||
layout="prev, pager, next, total, sizes"
|
||||
:total="total"
|
||||
v-model:current-page="queryForm.pageNum"
|
||||
v-model:page-size="queryForm.pageSize"
|
||||
:page-sizes="pageSizes"
|
||||
/>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, watch, reactive, onMounted, Ref, ref, useSlots, toValue } from 'vue';
|
||||
import { toRefs, watch, reactive, onMounted, Ref, ref, useSlots, toValue, h } from 'vue';
|
||||
import { TableColumn } from './index';
|
||||
import EnumTag from '@/components/enumtag/EnumTag.vue';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useEventListener } from '@vueuse/core';
|
||||
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 { ElTable } from 'element-plus';
|
||||
|
||||
import { ElInput, ElTable } from 'element-plus';
|
||||
|
||||
const emit = defineEmits(['update:selectionData', 'pageSizeChange', 'pageNumChange']);
|
||||
|
||||
export interface PageTableProps {
|
||||
size?: string;
|
||||
pageApi?: Api; // 请求表格数据的 api
|
||||
columns: TableColumn[]; // 列配置项 ==> 必传
|
||||
columns: TableColumn[] | any[]; // 列配置项 ==> 必传
|
||||
showSelection?: boolean;
|
||||
selectable?: (row: any) => boolean; // 是否可选
|
||||
pageable?: boolean;
|
||||
@@ -208,6 +240,10 @@ const showToolButton = (key: 'setting' | 'search') => {
|
||||
|
||||
const nowSearchItem: Ref<SearchItem> = ref(null) as any;
|
||||
|
||||
// 是否已经计算列宽度
|
||||
const isCalculatedWidth: Ref<boolean> = ref(false);
|
||||
const showTable: Ref<boolean> = ref(false);
|
||||
|
||||
/**
|
||||
* 改变当前的搜索项
|
||||
* @param searchItem 当前点击的搜索项
|
||||
@@ -239,24 +275,35 @@ const state = reactive({
|
||||
pageSizes: [] as any, // 可选每页显示的数据量
|
||||
// 输入框宽度
|
||||
formatVal: '', // 格式化后的值
|
||||
tableMaxHeight: '500px',
|
||||
});
|
||||
|
||||
const { pageSizes, formatVal, tableMaxHeight } = toRefs(state);
|
||||
const { pageSizes, formatVal } = toRefs(state);
|
||||
|
||||
watch(tableData, (newValue: any) => {
|
||||
if (newValue && newValue.length > 0) {
|
||||
props.columns.forEach((item) => {
|
||||
if (item.autoWidth && item.show) {
|
||||
item.autoCalculateMinWidth(tableData.value);
|
||||
}
|
||||
});
|
||||
calculateTableColumnMinWidth();
|
||||
// 需要计算完才能显示表格,否则会有表格闪烁的问题
|
||||
if (!showTable.value) {
|
||||
showTable.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
watch(isShowSearch, () => {
|
||||
calcuTableHeight();
|
||||
});
|
||||
/**
|
||||
* 计算表格列宽
|
||||
*/
|
||||
const calculateTableColumnMinWidth = () => {
|
||||
if (isCalculatedWidth.value || !tableData.value || tableData.value.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算表格列宽
|
||||
props.columns.forEach((item) => {
|
||||
if (item.autoWidth && item.show) {
|
||||
item.autoCalculateMinWidth(tableData.value);
|
||||
}
|
||||
});
|
||||
|
||||
isCalculatedWidth.value = true;
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
@@ -266,9 +313,6 @@ watch(
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
calcuTableHeight();
|
||||
useEventListener(window, 'resize', calcuTableHeight);
|
||||
|
||||
if (props.searchItems.length > 0) {
|
||||
nowSearchItem.value = props.searchItems[0];
|
||||
}
|
||||
@@ -292,11 +336,6 @@ onMounted(async () => {
|
||||
}
|
||||
});
|
||||
|
||||
const calcuTableHeight = () => {
|
||||
const headerHeight = isShowSearch.value ? 330 : 250;
|
||||
state.tableMaxHeight = window.innerHeight - headerHeight + 'px';
|
||||
};
|
||||
|
||||
const searchFormItemKeyUpEnter = (event: any) => {
|
||||
event.preventDefault();
|
||||
search();
|
||||
@@ -327,113 +366,21 @@ defineExpose({
|
||||
});
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.table-box,
|
||||
.table-main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.page-table-search-form-enter-active {
|
||||
transition: all 0.3s ease-out;
|
||||
}
|
||||
|
||||
// 表格 header 样式
|
||||
.table-header {
|
||||
width: 100%;
|
||||
.page-table-search-form-leave-active {
|
||||
transition: all 0.3s ease-in;
|
||||
}
|
||||
|
||||
.header-button-lf {
|
||||
float: left;
|
||||
}
|
||||
.page-table-search-form-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(-30px) scale(0.95);
|
||||
}
|
||||
|
||||
.header-button-ri {
|
||||
float: right;
|
||||
|
||||
.tool-button {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.simple-search-form {
|
||||
margin-right: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
::v-deep(.el-form-item__content > *) {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.simple-search-form-label {
|
||||
text-align: right;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.simple-search-form-btn:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-button {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
// el-table 表格样式
|
||||
.el-table {
|
||||
flex: 1;
|
||||
|
||||
// 修复 safari 浏览器表格错位 https://github.com/HalseySpicy/Geeker-Admin/issues/83
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// .el-table__header th {
|
||||
// height: 45px;
|
||||
// font-size: 15px;
|
||||
// font-weight: bold;
|
||||
// color: var(--el-text-color-primary);
|
||||
// background: var(--el-fill-color-light);
|
||||
// }
|
||||
|
||||
// .el-table__row {
|
||||
// height: 45px;
|
||||
// font-size: 14px;
|
||||
|
||||
// .move {
|
||||
// cursor: move;
|
||||
|
||||
// .el-icon {
|
||||
// cursor: move;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// 设置 el-table 中 header 文字不换行,并省略
|
||||
.el-table__header .el-table__cell>.cell {
|
||||
// white-space: nowrap;
|
||||
white-space: wrap;
|
||||
}
|
||||
|
||||
// 解决表格数据为空时样式不居中问题(仅在element-plus中)
|
||||
// .el-table__empty-block {
|
||||
// position: absolute;
|
||||
// top: 50%;
|
||||
// left: 50%;
|
||||
// transform: translate(-50%, -50%);
|
||||
|
||||
// .table-empty {
|
||||
// line-height: 30px;
|
||||
// }
|
||||
// }
|
||||
|
||||
// table 中 image 图片样式
|
||||
.table-image {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep(.el-form-item__label) {
|
||||
font-weight: bold;
|
||||
}
|
||||
.page-table-search-form-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-30px) scale(0.95);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -107,7 +107,6 @@ const handleItemKeyupEnter = (item: SearchItem) => {
|
||||
margin-bottom: 10px;
|
||||
|
||||
box-sizing: border-box;
|
||||
overflow-x: hidden;
|
||||
background-color: var(--el-bg-color);
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 6px;
|
||||
@@ -38,7 +38,7 @@ export class TableColumn {
|
||||
/**
|
||||
* 插槽名,
|
||||
*/
|
||||
private slotName: string = '';
|
||||
slotName: string = '';
|
||||
|
||||
showOverflowTooltip: boolean = true;
|
||||
|
||||
@@ -71,14 +71,14 @@ export class TableColumn {
|
||||
formatFunc: Function;
|
||||
|
||||
/**
|
||||
* 是否显示该列
|
||||
* 是否显示该列,1显示 0不显示
|
||||
*/
|
||||
private show: boolean = true;
|
||||
show: number = 1;
|
||||
|
||||
/**
|
||||
* 是否展示美化按钮(主要用于美化json文本等)
|
||||
*/
|
||||
private isBeautify: boolean = false;
|
||||
isBeautify: boolean = false;
|
||||
|
||||
constructor(prop: string, label: string) {
|
||||
this.prop = prop;
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
<div ref="viewportRef" class="viewport" :style="{ width: state.size.width + 'px', height: state.size.height + 'px' }">
|
||||
<div ref="displayRef" class="display" tabindex="0" />
|
||||
<div class="btn-box">
|
||||
<SvgIcon name="DocumentCopy" @click="openPaste" :size="20" class="pointer-icon mr10" title="剪贴板" />
|
||||
<SvgIcon name="FolderOpened" @click="openFilesystem" :size="20" class="pointer-icon mr10" title="文件管理" />
|
||||
<SvgIcon name="FullScreen" @click="state.fullscreen ? closeFullScreen() : openFullScreen()" :size="20" class="pointer-icon mr10" title="全屏" />
|
||||
<SvgIcon name="DocumentCopy" @click="openPaste" :size="20" class="pointer-icon mr-2" title="剪贴板" />
|
||||
<SvgIcon name="FolderOpened" @click="openFilesystem" :size="20" class="pointer-icon mr-2" title="文件管理" />
|
||||
<SvgIcon name="FullScreen" @click="state.fullscreen ? closeFullScreen() : openFullScreen()" :size="20" class="pointer-icon mr-2" title="全屏" />
|
||||
|
||||
<el-dropdown>
|
||||
<SvgIcon name="Monitor" :size="20" class="pointer-icon mr10" title="发送快捷键" style="color: #fff" />
|
||||
<SvgIcon name="Monitor" :size="20" class="pointer-icon mr-2" title="发送快捷键" style="color: #fff" />
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="openSendKeyboard(['65507', '65513', '65535'])"> Ctrl + Alt + Delete </el-dropdown-item>
|
||||
@@ -21,7 +21,7 @@
|
||||
</template>
|
||||
</el-dropdown>
|
||||
|
||||
<SvgIcon name="Refresh" @click="connect(0, 0)" :size="20" class="pointer-icon mr10" title="重新连接" />
|
||||
<SvgIcon name="Refresh" @click="connect(0, 0)" :size="20" class="pointer-icon mr-2" title="重新连接" />
|
||||
</div>
|
||||
<clipboard-dialog ref="clipboardRef" v-model:visible="state.clipboardDialog.visible" @close="closePaste" @submit="onsubmitClipboard" />
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<div class="title-right-fixed">
|
||||
<el-popconfirm @confirm="connect(true)" title="确认重新连接?">
|
||||
<template #reference>
|
||||
<div class="mr10 pointer">
|
||||
<div class="mr-2 cursor-pointer">
|
||||
<el-tag v-if="state.status == TerminalStatus.Connected" type="success" effect="light" round> 已连接 </el-tag>
|
||||
<el-tag v-else type="danger" effect="light" round> 未连接,点击重连 </el-tag>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div id="terminal-body" :style="{ height }">
|
||||
<div ref="terminalRef" class="terminal" />
|
||||
<div class="h-full w-full flex">
|
||||
<div ref="terminalRef" class="h-full w-full" :style="{ background: getTerminalTheme().background }" />
|
||||
|
||||
<TerminalSearch ref="terminalSearchRef" :search-addon="state.addon.search" @close="focus" />
|
||||
</div>
|
||||
@@ -18,10 +18,11 @@ import { useThemeConfig } from '@/store/themeConfig';
|
||||
import { ref, nextTick, reactive, onMounted, onBeforeUnmount, watch } from 'vue';
|
||||
import TerminalSearch from './TerminalSearch.vue';
|
||||
import { TerminalStatus } from './common';
|
||||
import { useDebounceFn, useEventListener } from '@vueuse/core';
|
||||
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();
|
||||
|
||||
@@ -41,13 +42,6 @@ const props = defineProps({
|
||||
socketUrl: {
|
||||
type: String,
|
||||
},
|
||||
/**
|
||||
* 高度
|
||||
*/
|
||||
height: {
|
||||
type: [String, Number],
|
||||
default: '100%',
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['statusChange']);
|
||||
@@ -60,7 +54,6 @@ const { themeConfig } = storeToRefs(useThemeConfig());
|
||||
// 终端实例
|
||||
let term: Terminal;
|
||||
let socket: WebSocket;
|
||||
let pingInterval: any;
|
||||
|
||||
const state = reactive({
|
||||
// 插件
|
||||
@@ -89,7 +82,9 @@ watch(
|
||||
watch(
|
||||
() => themeConfig.value.terminalTheme,
|
||||
() => {
|
||||
term.options.theme = getTerminalTheme();
|
||||
if (term) {
|
||||
term.options.theme = getTerminalTheme();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -97,7 +92,7 @@ onBeforeUnmount(() => {
|
||||
close();
|
||||
});
|
||||
|
||||
function init() {
|
||||
const init = () => {
|
||||
state.status = TerminalStatus.NoConnected;
|
||||
if (term) {
|
||||
console.log('重新连接...');
|
||||
@@ -106,9 +101,9 @@ function init() {
|
||||
nextTick(() => {
|
||||
initTerm();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
async function initTerm() {
|
||||
const initTerm = async () => {
|
||||
term = new Terminal({
|
||||
fontSize: themeConfig.value.terminalFontSize || 15,
|
||||
fontWeight: themeConfig.value.terminalFontWeight || 'normal',
|
||||
@@ -130,7 +125,7 @@ async function initTerm() {
|
||||
// 注册窗口大小监听器
|
||||
useEventListener('resize', useDebounceFn(fitTerminal, 400));
|
||||
|
||||
initSocket();
|
||||
await initSocket();
|
||||
// 注册其他插件
|
||||
loadAddon();
|
||||
|
||||
@@ -144,42 +139,41 @@ async function initTerm() {
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function initSocket() {
|
||||
const initSocket = async () => {
|
||||
if (!props.socketUrl) {
|
||||
return;
|
||||
}
|
||||
socket = new WebSocket(`${props.socketUrl}&rows=${term?.rows}&cols=${term?.cols}`);
|
||||
// 监听socket连接
|
||||
socket.onopen = () => {
|
||||
// 注册心跳
|
||||
pingInterval = setInterval(sendPing, 15000);
|
||||
state.status = TerminalStatus.Connected;
|
||||
|
||||
focus();
|
||||
fitTerminal();
|
||||
|
||||
// 如果有初始要执行的命令,则发送执行命令
|
||||
if (props.cmd) {
|
||||
sendCmd(props.cmd + ' \r');
|
||||
}
|
||||
};
|
||||
|
||||
// 监听socket错误信息
|
||||
socket.onerror = (e: Event) => {
|
||||
try {
|
||||
socket = await createWebSocket(`${props.socketUrl}?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);
|
||||
state.status = TerminalStatus.Disconnected;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
function loadAddon() {
|
||||
const loadAddon = () => {
|
||||
// 注册搜索组件
|
||||
const searchAddon = new SearchAddon();
|
||||
state.addon.search = searchAddon;
|
||||
@@ -196,7 +190,7 @@ function loadAddon() {
|
||||
// write the server output to the terminal
|
||||
writeToTerminal: (data: any) => term.write(typeof data === 'string' ? data : new Uint8Array(data)),
|
||||
// send the user input to the server
|
||||
sendToServer: sendCmd,
|
||||
sendToServer: sendData,
|
||||
// the terminal columns
|
||||
terminalColumns: term.cols,
|
||||
// there is a windows shell
|
||||
@@ -222,7 +216,7 @@ function loadAddon() {
|
||||
.then(() => console.log('upload success'))
|
||||
.catch((err: any) => console.log(err));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 写入内容至终端
|
||||
const write2Term = (data: any) => {
|
||||
@@ -270,30 +264,28 @@ enum MsgType {
|
||||
Ping = 3,
|
||||
}
|
||||
|
||||
const send = (msg: any) => {
|
||||
state.status == TerminalStatus.Connected && socket?.send(msg);
|
||||
const send2Socket = (data: any) => {
|
||||
state.status == TerminalStatus.Connected && socket?.send(data);
|
||||
};
|
||||
|
||||
const sendResize = (cols: number, rows: number) => {
|
||||
send(`${MsgType.Resize}|${rows}|${cols}`);
|
||||
send2Socket(`${MsgType.Resize}|${rows}|${cols}`);
|
||||
};
|
||||
|
||||
const sendPing = () => {
|
||||
send(`${MsgType.Ping}|ping`);
|
||||
send2Socket(`${MsgType.Ping}|ping`);
|
||||
};
|
||||
|
||||
function sendCmd(key: any) {
|
||||
send(`${MsgType.Data}|${key}`);
|
||||
}
|
||||
const sendData = (key: any) => {
|
||||
send2Socket(`${MsgType.Data}|${key}`);
|
||||
};
|
||||
|
||||
function closeSocket() {
|
||||
const closeSocket = () => {
|
||||
// 关闭 websocket
|
||||
socket && socket.readyState === 1 && socket.close();
|
||||
// 清除 ping
|
||||
pingInterval && clearInterval(pingInterval);
|
||||
}
|
||||
};
|
||||
|
||||
function close() {
|
||||
const close = () => {
|
||||
console.log('in terminal body close');
|
||||
closeSocket();
|
||||
if (term) {
|
||||
@@ -302,7 +294,7 @@ function close() {
|
||||
state.addon.weblinks.dispose();
|
||||
term.dispose();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getStatus = (): TerminalStatus => {
|
||||
return state.status;
|
||||
@@ -310,17 +302,4 @@ const getStatus = (): TerminalStatus => {
|
||||
|
||||
defineExpose({ init, fitTerminal, focus, clear, close, getStatus, sendResize, write2Term, writeln2Term });
|
||||
</script>
|
||||
<style lang="scss">
|
||||
#terminal-body {
|
||||
width: 100%;
|
||||
|
||||
.terminal {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
// .xterm .xterm-viewport {
|
||||
// overflow-y: hidden;
|
||||
// }
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="scss"></style>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<div class="title-right-fixed">
|
||||
<el-popconfirm @confirm="reConnect(openTerminal.terminalId)" :title="$t('components.terminal.connConfirm')">
|
||||
<template #reference>
|
||||
<div class="mr15 pointer">
|
||||
<div class="mr-4 cursor-pointer">
|
||||
<el-tag v-if="openTerminal.status == TerminalStatus.Connected" type="success" effect="light" round>
|
||||
{{ $t('components.terminal.connected') }}
|
||||
</el-tag>
|
||||
@@ -39,10 +39,10 @@
|
||||
|
||||
<el-popover placement="bottom" :width="200" trigger="hover">
|
||||
<template #reference>
|
||||
<SvgIcon name="QuestionFilled" :size="20" class="pointer-icon mr10" />
|
||||
<SvgIcon name="QuestionFilled" :size="20" class="pointer-icon !mr-2" />
|
||||
</template>
|
||||
<div>ctrl | command + f ({{ $t('components.terminal.search') }})</div>
|
||||
<div class="mt5">{{ $t('components.terminal.reConnTips') }}</div>
|
||||
<div class="mt-1">{{ $t('components.terminal.reConnTips') }}</div>
|
||||
</el-popover>
|
||||
|
||||
<SvgIcon
|
||||
@@ -50,7 +50,7 @@
|
||||
v-if="props.visibleMinimize"
|
||||
@click="minimize(openTerminal.terminalId)"
|
||||
:size="20"
|
||||
class="pointer-icon mr10"
|
||||
class="pointer-icon mr-2"
|
||||
:title="$t('components.terminal.minimize')"
|
||||
/>
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
name="FullScreen"
|
||||
@click="handlerFullScreen(openTerminal)"
|
||||
:size="20"
|
||||
class="pointer-icon mr10"
|
||||
class="pointer-icon mr-2"
|
||||
:title="$t('components.terminal.fullScreenTitle')"
|
||||
/>
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
<el-card
|
||||
v-for="minimizeTerminal of minimizeTerminals"
|
||||
:key="minimizeTerminal.terminalId"
|
||||
:class="`terminal-minimize-item pointer ${minimizeTerminal.styleClass}`"
|
||||
:class="`terminal-minimize-item cursor-pointer ${minimizeTerminal.styleClass}`"
|
||||
size="small"
|
||||
@click="maximize(minimizeTerminal.terminalId)"
|
||||
>
|
||||
@@ -99,7 +99,7 @@
|
||||
</el-tooltip>
|
||||
|
||||
<!-- 关闭按钮 -->
|
||||
<SvgIcon name="CloseBold" @click.stop="closeMinimizeTerminal(minimizeTerminal.terminalId)" class="ml10 pointer-icon fr" :size="20" />
|
||||
<SvgIcon name="CloseBold" @click.stop="closeMinimizeTerminal(minimizeTerminal.terminalId)" class="ml-2 pointer-icon float-right" :size="20" />
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-drawer v-model="visible" :before-close="cancel" size="50%">
|
||||
<el-drawer v-model="visible" :before-close="cancel" size="50%" body-class="flex flex-col">
|
||||
<template #header>
|
||||
<DrawerHeader :header="props.title" :back="cancel">
|
||||
<template #extra>
|
||||
<EnumTag :enums="LogTypeEnum" :value="log?.type" class="mr20" />
|
||||
<EnumTag :enums="LogTypeEnum" :value="log?.type" class="mr-4.5" />
|
||||
</template>
|
||||
</DrawerHeader>
|
||||
</template>
|
||||
|
||||
<el-descriptions class="mb10" :column="1" border v-if="extra">
|
||||
<el-descriptions class="mb-2" :column="1" border v-if="extra">
|
||||
<el-descriptions-item v-for="(value, key) in extra" :key="key" :span="1" :label="key">{{ value }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<TerminalBody class="mb10" ref="terminalRef" height="calc(100vh - 220px)" />
|
||||
<TerminalBody class="mb-2 flex-1" ref="terminalRef" />
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -22,7 +22,7 @@ export const usePageTable = (
|
||||
) => {
|
||||
const state = reactive({
|
||||
// 表格数据
|
||||
tableData: [],
|
||||
tableData: [{}],
|
||||
// 总数量
|
||||
total: 0,
|
||||
// 查询参数,包含分页参数
|
||||
|
||||
@@ -2,11 +2,11 @@ import router from '@/router';
|
||||
import { clearUser, getClientId, getRefreshToken, getToken, saveRefreshToken, saveToken } from '@/common/utils/storage';
|
||||
import { templateResolve } from '@/common/utils/string';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { createFetch } from '@vueuse/core';
|
||||
import { createFetch, UseFetchReturn } from '@vueuse/core';
|
||||
import Api from '@/common/Api';
|
||||
import { Result, ResultEnum } from '@/common/request';
|
||||
import config from '@/common/config';
|
||||
import { unref } from 'vue';
|
||||
import { ref, unref } from 'vue';
|
||||
import { URL_401 } from '@/router/staticRouter';
|
||||
import openApi from '@/common/openApi';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
@@ -38,14 +38,19 @@ const useCustomFetch = createFetch({
|
||||
return { options };
|
||||
},
|
||||
async afterFetch(ctx) {
|
||||
const result: Result = await ctx.response.json();
|
||||
ctx.data = result;
|
||||
ctx.data = await ctx.response.json();
|
||||
return ctx;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export function useApiFetch<T>(api: Api, params: any = null, reqOptions: RequestInit = {}) {
|
||||
interface EsReq {
|
||||
esProxyReq: boolean;
|
||||
}
|
||||
|
||||
export interface RequestOptions extends RequestInit, EsReq {}
|
||||
|
||||
export function useApiFetch<T>(api: Api, params: any = null, reqOptions?: RequestOptions) {
|
||||
const uaf = useCustomFetch<T>(api.url, {
|
||||
async beforeFetch({ url, options }) {
|
||||
options.method = api.method;
|
||||
@@ -90,14 +95,24 @@ export function useApiFetch<T>(api: Api, params: any = null, reqOptions: Request
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
onFetchError: (ctx) => {
|
||||
if (reqOptions?.esProxyReq) {
|
||||
uaf.data = { value: JSON.parse(ctx.data) };
|
||||
return Promise.resolve(uaf.data);
|
||||
}
|
||||
return ctx;
|
||||
},
|
||||
}) as any;
|
||||
|
||||
// 统一处理后的返回结果,如果直接使用uaf.data,则数据会出现由{code: x, data: {}} -> data 的变化导致某些结果绑定报错
|
||||
const data = ref<T | null>(null);
|
||||
return {
|
||||
execute: async function () {
|
||||
return execCustomFetch(uaf);
|
||||
await execCustomFetch(uaf, reqOptions);
|
||||
data.value = uaf.data.value;
|
||||
},
|
||||
isFetching: uaf.isFetching,
|
||||
data: uaf.data,
|
||||
data: data,
|
||||
abort: uaf.abort,
|
||||
};
|
||||
}
|
||||
@@ -105,37 +120,44 @@ export function useApiFetch<T>(api: Api, params: any = null, reqOptions: Request
|
||||
let refreshingToken = false;
|
||||
let queue: any[] = [];
|
||||
|
||||
async function execCustomFetch(uaf: any) {
|
||||
async function execCustomFetch(uaf: UseFetchReturn<any>, reqOptions?: RequestOptions) {
|
||||
try {
|
||||
await uaf.execute(true);
|
||||
} catch (e: any) {
|
||||
const rejectPromise = Promise.reject(e);
|
||||
if (!reqOptions?.esProxyReq) {
|
||||
const rejectPromise = Promise.reject(e);
|
||||
|
||||
if (e?.name == 'AbortError') {
|
||||
console.log('请求已取消');
|
||||
if (e?.name == 'AbortError') {
|
||||
console.log('请求已取消');
|
||||
return rejectPromise;
|
||||
}
|
||||
|
||||
const respStatus = uaf.response.value?.status;
|
||||
if (respStatus == 404) {
|
||||
ElMessage.error('url not found');
|
||||
return rejectPromise;
|
||||
}
|
||||
if (respStatus == 500) {
|
||||
ElMessage.error('server error');
|
||||
return rejectPromise;
|
||||
}
|
||||
|
||||
console.error(e);
|
||||
ElMessage.error('network error');
|
||||
return rejectPromise;
|
||||
}
|
||||
|
||||
const respStatus = uaf.response.value?.status;
|
||||
if (respStatus == 404) {
|
||||
ElMessage.error('请求接口不存在');
|
||||
return rejectPromise;
|
||||
}
|
||||
if (respStatus == 500) {
|
||||
ElMessage.error('服务器响应异常');
|
||||
return rejectPromise;
|
||||
}
|
||||
|
||||
console.error(e);
|
||||
ElMessage.error('网络请求错误');
|
||||
return rejectPromise;
|
||||
}
|
||||
|
||||
const result: Result = uaf.data.value as any;
|
||||
const result: Result & { error: any; status: number } = uaf.data.value as any;
|
||||
if (!result) {
|
||||
ElMessage.error('网络请求失败');
|
||||
ElMessage.error('network request failed');
|
||||
return Promise.reject(result);
|
||||
}
|
||||
// es代理请求
|
||||
if (reqOptions?.esProxyReq) {
|
||||
uaf.data.value = result;
|
||||
return Promise.resolve(result);
|
||||
}
|
||||
|
||||
const resultCode = result.code;
|
||||
|
||||
@@ -151,7 +173,7 @@ async function execCustomFetch(uaf: any) {
|
||||
// 请求加入队列等待, 防止并发多次请求refreshToken
|
||||
return new Promise((resolve) => {
|
||||
queue.push(() => {
|
||||
resolve(execCustomFetch(uaf));
|
||||
resolve(execCustomFetch(uaf, reqOptions));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -175,13 +197,13 @@ async function execCustomFetch(uaf: any) {
|
||||
queue = [];
|
||||
}
|
||||
|
||||
await execCustomFetch(uaf);
|
||||
await execCustomFetch(uaf, reqOptions);
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果提示没有权限,则跳转至无权限页面
|
||||
if (resultCode === ResultEnum.NO_PERMISSION) {
|
||||
router.push({
|
||||
await router.push({
|
||||
path: URL_401,
|
||||
});
|
||||
return Promise.reject(result);
|
||||
|
||||
@@ -7,12 +7,16 @@ export default {
|
||||
detail: 'Details',
|
||||
add: 'Add',
|
||||
save: 'Save',
|
||||
close: 'Close',
|
||||
download: 'Download',
|
||||
upload: 'Upload',
|
||||
remove: 'Remove',
|
||||
confirm: 'Confirm',
|
||||
cancel: 'Cancel',
|
||||
submit: 'Submit',
|
||||
operation: 'Operations',
|
||||
name: 'Name',
|
||||
version: 'Version',
|
||||
code: 'Code',
|
||||
remark: 'Remark',
|
||||
status: 'Status',
|
||||
@@ -48,9 +52,11 @@ export default {
|
||||
previousStep: 'Previous Step',
|
||||
nextStep: 'Next Step',
|
||||
copy: 'Copy',
|
||||
copyCell: 'Copy Cell',
|
||||
search: 'Search',
|
||||
pleaseInput: 'Please enter {label}',
|
||||
pleaseSelect: 'Please select {label}',
|
||||
pleaseSelectOne: 'Please select Only One Data',
|
||||
formValidationError: 'Please check the form',
|
||||
createTitle: 'Create {name}',
|
||||
editTitle: 'Edit {name}',
|
||||
@@ -61,6 +67,9 @@ export default {
|
||||
deleteSuccess: 'delete successfully',
|
||||
operateSuccess: 'operate successfully',
|
||||
fieldNotEmpty: '{field} cannot be empty',
|
||||
selectAll: 'Select all',
|
||||
MultiPlaceholder: 'Multiple are separated by commas',
|
||||
appSlogan: 'Simple, efficient and secure',
|
||||
},
|
||||
layout: {
|
||||
user: {
|
||||
@@ -142,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',
|
||||
|
||||
@@ -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',
|
||||
@@ -219,4 +222,11 @@ export default {
|
||||
running: 'Running',
|
||||
waitRun: 'Wait Run',
|
||||
},
|
||||
es: {
|
||||
keywordPlaceholder: 'host / name / code',
|
||||
port: 'Port',
|
||||
acName: 'Credential',
|
||||
dbInst: 'Es Instance',
|
||||
connSuccess: 'be connected successfully',
|
||||
},
|
||||
};
|
||||
|
||||
121
frontend/src/i18n/en/es.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
export default {
|
||||
es: {
|
||||
keywordPlaceholder: 'host / name / code',
|
||||
port: 'Port',
|
||||
size: 'size',
|
||||
docs: 'docs',
|
||||
health: 'health',
|
||||
aliases: 'Aliases',
|
||||
addAlias: 'Add Alias',
|
||||
specifyIdAdd: 'Specify the ID added, if id exists, then update',
|
||||
addIndex: 'Add Index',
|
||||
editIndex: 'Edit Index',
|
||||
status: 'status',
|
||||
acName: 'Credential',
|
||||
emptyTable: 'data not fund',
|
||||
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',
|
||||
indexName: 'Index Name',
|
||||
requireIndexName: 'Index Name Is Required',
|
||||
indexDetail: 'Index Detail',
|
||||
indexMapping: 'Mappings',
|
||||
indexStats: 'Stats',
|
||||
opViewColumns: 'Option View Columns',
|
||||
opIndex: 'Index Management',
|
||||
opSearch: 'Search',
|
||||
searchParamsPreview: 'Search Params Preview',
|
||||
opBasicSearch: 'Basic Search',
|
||||
opSeniorSearch: 'Senior Search',
|
||||
sampleMappings: 'Sample Mappings',
|
||||
copyMappings: 'Copy Mappings',
|
||||
readonlyMsg: 'The content is readOnly',
|
||||
opDashboard: 'Dashboard',
|
||||
opSettings: 'Settings',
|
||||
templates: 'Templates',
|
||||
availableSettingFields: 'Available Setting Fields',
|
||||
Reindex: 'Reindex',
|
||||
ReindexTargetIdx: 'Target Index',
|
||||
ReindexIsSync: 'Sync Able',
|
||||
ReindexDescription:
|
||||
"If a field in Mapping has been defined, you can't modify the type of the field, and you can't change the number of shards, you can use the Reindex API to solve this problem.",
|
||||
ReindexSyncDescription: 'If the amount of index data is large, we recommend that you enable asynchronous data to avoid request timeouts.',
|
||||
ReindexToOtherInst: 'To other Instance',
|
||||
ReindexSyncTask: 'Sync Task',
|
||||
makeSearchParam: 'Make Search Params',
|
||||
filterColumn: 'Filter Columns',
|
||||
searchParams: 'Search',
|
||||
searchParamsDesc: 'If no field is selected or no condition value is set, it will not take effect',
|
||||
standardSearch: 'Standard Search',
|
||||
AggregationSearch: 'Aggregation Search',
|
||||
SqlSearch: 'Sql Search',
|
||||
searchError: 'Search Error',
|
||||
execError: 'Exec Error',
|
||||
docJsonError: 'Document JSON Format Error',
|
||||
sortParams: 'Sort',
|
||||
otherParams: 'Other',
|
||||
closeIndexConfirm: 'This operation will close index [{name}]. Do you want to continue?',
|
||||
openIndexConfirm: 'This operation will open index [{name}]. Do you want to continue?',
|
||||
clearCacheConfirm: 'This operation will clear index [{name}] cache. Do you want to continue?',
|
||||
page: {
|
||||
home: 'First Page',
|
||||
prev: 'Prev Page',
|
||||
next: 'Next Page',
|
||||
total: 'Total Count',
|
||||
changeSize: 'Change Page Size',
|
||||
},
|
||||
temp: {
|
||||
addTemp: 'Add template',
|
||||
view: 'Template Detail',
|
||||
name: 'name',
|
||||
priority: 'priority',
|
||||
index_patterns: 'patterns',
|
||||
content: 'content',
|
||||
showHide: 'show system templates',
|
||||
description: 'description',
|
||||
filter: 'filter name / description',
|
||||
versionAlert: 'Versions prior to 7.8 are not supported',
|
||||
note: `1、When creating a new index, if the index name matches the wildcard of the index template, the index template's settings (_setting, _mapping, etc.) are used。
|
||||
2、Templates take effect only when an index is created, and modifying a template does not affect existing indexes。
|
||||
3、You can specify the value of "priority", which was "order" before version 7.8, and if the new index name matches multiple templates, the one with the lowest priority will be used first.`,
|
||||
},
|
||||
dashboard: {
|
||||
instInfo: 'Instance Info',
|
||||
clusterHealth: 'Cluster Health',
|
||||
nodes: 'Nodes Info',
|
||||
sysMem: 'System Mem',
|
||||
jvmMem: 'JVM Mem',
|
||||
fileSystem: 'File System',
|
||||
analyze: 'Analyze',
|
||||
idxName: 'Index Name',
|
||||
field: 'Field',
|
||||
text: 'Text',
|
||||
startAnalyze: 'Start Analyze',
|
||||
},
|
||||
contextmenu: {
|
||||
index: {
|
||||
addIndex: 'Add Index',
|
||||
showSys: 'Show System Index',
|
||||
copyName: 'Copy Name',
|
||||
refresh: 'Refresh Index',
|
||||
flush: 'Flush Index',
|
||||
clearCache: 'Clear Index Cache',
|
||||
addAlias: 'Add Alias',
|
||||
Close: 'Close',
|
||||
Open: 'Open',
|
||||
Delete: 'Delete Index',
|
||||
edit: 'Edit Index',
|
||||
DeleteSelectLine: 'Copy Selected Line Json',
|
||||
BaseSearch: 'Base Search',
|
||||
SeniorSearch: 'Senior Search',
|
||||
copyLineJson: 'Copy Line Json',
|
||||
copySelectLineJson: 'Copy Selected Line Json',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -6,23 +6,23 @@ export default {
|
||||
triggeringCondition: 'Condition',
|
||||
triggeringConditionTips: 'go template syntax. If the output is 1, the approval process is triggered',
|
||||
conditionPlaceholder: 'Trigger condition, return value =1, means to trigger the approval process',
|
||||
conditionDefault: `{{/* DBMS- Run Sql rules The param parameter is described as follows */}}
|
||||
{{/* stmtType: select / read / insert / update / delete / ddl ; */}}
|
||||
{{ if eq .bizType "db_sql_exec_flow"}}
|
||||
{{/* Enable process approval when select and read statements are not available */}}
|
||||
{{ if and (ne .param.stmtType "select") (ne .param.stmtType "read") }}
|
||||
1
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
conditionDefault: `{'{{'}/* DBMS- Run Sql rules The param parameter is described as follows */{'}}'}
|
||||
{'{{'}/* stmtType: select / read / insert / update / delete / ddl ; */{'}}'}
|
||||
{'{{'} if eq .bizType "db_sql_exec_flow"{'}}'}
|
||||
{'{{'}/* Enable process approval when select and read statements are not available */{'}}'}
|
||||
{'{{'} if and (ne .param.stmtType "select") (ne .param.stmtType "read") {'}}'}
|
||||
1
|
||||
{'{{'} end {'}}'}
|
||||
{'{{'} end {'}}'}
|
||||
|
||||
{{/* Redis-Run Cmd rules; param: parameter is described as follows */}}
|
||||
{{/* cmdType: read(Read cmd) / write(Write cmd); */}}
|
||||
{{/* cmd: get/set/hset... */}}
|
||||
{{ if eq .bizType "redis_run_cmd_flow"}}
|
||||
{{ if eq .param.cmdType "write" }}
|
||||
1
|
||||
{{ end }}
|
||||
{{ end }}`,
|
||||
{'{{'}/* Redis-Run Cmd rules; param: parameter is described as follows */{'}}'}
|
||||
{'{{'}/* cmdType: read(Read cmd) / write(Write cmd); */{'}}'}
|
||||
{'{{'}/* cmd: get/set/hset... */{'}}'}
|
||||
{'{{'} if eq .bizType "redis_run_cmd_flow"{'}}'}
|
||||
{'{{'} if eq .param.cmdType "write" {'}}'}
|
||||
1
|
||||
{'{{'} end {'}}'}
|
||||
{'{{'} end {'}}'}`,
|
||||
nodeName: 'Node Name',
|
||||
nodeNameTips: 'Click the specified node to drag and drop sort',
|
||||
auditor: 'Auditor',
|
||||
@@ -32,6 +32,27 @@ export default {
|
||||
enable: 'Enable',
|
||||
disable: 'Disable',
|
||||
|
||||
todoTask: 'Pending Tasks',
|
||||
doneTask: 'Completed Tasks',
|
||||
flowDesign: 'Flow Design',
|
||||
clear: 'Clear',
|
||||
approvalMode: 'Approval Mode',
|
||||
andSign: 'All Approve (AND)',
|
||||
orSign: 'Any Approve (OR)',
|
||||
voteSign: 'Vote Approval',
|
||||
taskCandidate: 'Task Assignees',
|
||||
mustOneStartNode: 'There must be one start node in the flow',
|
||||
mustOneEndNode: 'There must be one end node in the flow',
|
||||
mustOneOutEdgeForStartNode: 'The start node must have at least one outgoing edge',
|
||||
mustOneInEdgeForEndNode: 'The end node must have at least one incoming edge',
|
||||
approvalRecord: 'Approval Records',
|
||||
start: 'Start',
|
||||
end: 'End',
|
||||
usertask: 'User Task', // 建议拼写修正为 userTask
|
||||
serial: 'Exclusive Gateway',
|
||||
parallel: 'Parallel Gateway',
|
||||
flowEdge: 'Sequence Flow',
|
||||
|
||||
// procinst
|
||||
startProcess: 'Start Process',
|
||||
cancelProcessConfirm: 'Confirm canceling the process?',
|
||||
@@ -80,15 +101,17 @@ export default {
|
||||
redisRunCmd: 'Redis-Run Cmd',
|
||||
|
||||
// task
|
||||
approveNode: 'Approve Node',
|
||||
approveForm: 'Approve Form',
|
||||
approveResult: 'Result',
|
||||
approveNode: 'Approval Node',
|
||||
approveForm: 'Approval Form',
|
||||
approveResult: 'Approval Result',
|
||||
approvalRemark: 'Approval Comments',
|
||||
approver: 'Approver',
|
||||
audit: 'Audit',
|
||||
procinstStatus: 'Process status',
|
||||
taskStatus: 'Task status',
|
||||
procinstStatus: 'Process Status',
|
||||
taskStatus: 'Task Status',
|
||||
taskName: 'Task Name',
|
||||
taskBeginTime: 'Begin Time',
|
||||
flowAudit: 'Approval Process',
|
||||
notify: 'Notification',
|
||||
taskBeginTime: 'Start Time',
|
||||
flowAudit: 'Process Audit',
|
||||
notify: 'Notify',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
@@ -87,6 +87,8 @@ export default {
|
||||
scriptResultEnumRealTime: 'Real-time',
|
||||
scriptTypeEnumPrivate: 'Private',
|
||||
scriptTypeEnumPublic: 'Public',
|
||||
category: 'Category',
|
||||
categoryTips: 'support input new category and selection',
|
||||
|
||||
// security
|
||||
cmdConfig: 'Command Config',
|
||||
@@ -136,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',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -25,8 +25,8 @@ export default {
|
||||
success: 'Success',
|
||||
menuCodeTips: `The menu type is the access path (if the menu path does not begin with '/', the access address will automatically concatenate the parent menu path), otherwise it is the unique code of the resource`,
|
||||
menuCodePlaceholder: `A menu that does not begin with '/' will automatically concatenate the parent menu path`,
|
||||
routerNameTips: 'For component caching to work, match the vue component name, such as ResourceLis',
|
||||
componentPathTips: 'Access path components, such as: ` system/resource/ResourceList `, default in ` views ` directory',
|
||||
routerNameTips:
|
||||
'For component caching to work, the key for route.ts in the frontend module should match the vue component name, such as ResourceList',
|
||||
isCacheTips: `If yes is selected, it will be 'keepalive' cached (reentering the page without refreshing the page and requesting data again), and needs the route name to match the vue component name`,
|
||||
isHideTips:
|
||||
'Select Hide and the route will not appear in the menu bar, but it will still be accessible. Disabled will not be able to access and operate',
|
||||
|
||||
@@ -7,12 +7,16 @@ export default {
|
||||
detail: '详情',
|
||||
add: '添加',
|
||||
save: '保存',
|
||||
close: '关闭',
|
||||
download: '下载',
|
||||
upload: '上传',
|
||||
remove: '移除',
|
||||
confirm: '确定',
|
||||
cancel: '取消',
|
||||
submit: '提交',
|
||||
operation: '操作',
|
||||
name: '名称',
|
||||
version: '版本',
|
||||
code: '编号',
|
||||
remark: '备注',
|
||||
status: '状态',
|
||||
@@ -48,9 +52,11 @@ export default {
|
||||
previousStep: '上一步',
|
||||
nextStep: '下一步',
|
||||
copy: '复制',
|
||||
copyCell: '复制单元格',
|
||||
search: '搜索',
|
||||
pleaseInput: '请输入{label}',
|
||||
pleaseSelect: '请选择{label}',
|
||||
pleaseSelectOne: '请选择一条数据',
|
||||
formValidationError: '信息填写有误,请检查',
|
||||
createTitle: '创建{name}',
|
||||
editTitle: '编辑{name}',
|
||||
@@ -61,6 +67,9 @@ export default {
|
||||
deleteSuccess: '删除成功',
|
||||
operateSuccess: '操作成功',
|
||||
fieldNotEmpty: '{field}不能为空',
|
||||
selectAll: '全选',
|
||||
MultiPlaceholder: '多个用逗号隔开',
|
||||
appSlogan: '简洁 · 高效 · 安全',
|
||||
},
|
||||
layout: {
|
||||
user: {
|
||||
@@ -144,8 +153,6 @@ export default {
|
||||
isUniqueOpened: '菜单手风琴',
|
||||
isFixedHeader: '固定 Header',
|
||||
isClassicSplitMenu: '经典布局分割菜单',
|
||||
isLockScreen: '开启锁屏',
|
||||
lockScreenTime: '自动锁屏(s/秒)',
|
||||
interfaceDisplay: '界面显示',
|
||||
isShowLogo: '侧边栏 Logo',
|
||||
isBreadcrumb: '开启 Breadcrumb',
|
||||
|
||||
@@ -16,6 +16,7 @@ export default {
|
||||
dbFilterPlaceholder: '库名: 输入可过滤',
|
||||
sqlRecord: 'SQL记录',
|
||||
dump: '导出',
|
||||
dbDumpFail: '数据库导出失败',
|
||||
dumpContent: '导出内容',
|
||||
structure: '结构',
|
||||
data: '数据',
|
||||
@@ -55,6 +56,8 @@ export default {
|
||||
execSuccess: '执行成功',
|
||||
execFail: '执行失败',
|
||||
sqlScriptRun: 'SQL脚本执行',
|
||||
sqlScriptRunSuccess: 'SQL脚本执行成功',
|
||||
sqlScriptRunFail: 'SQL脚本执行失败',
|
||||
saveSql: '保存SQL',
|
||||
execInfo: '执行信息',
|
||||
result: '结果',
|
||||
|
||||
120
frontend/src/i18n/zh-cn/es.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
export default {
|
||||
es: {
|
||||
keywordPlaceholder: 'host / 名称 / 编号',
|
||||
port: '端口',
|
||||
size: '存储大小',
|
||||
docs: '文档数',
|
||||
health: '健康',
|
||||
aliases: '别名',
|
||||
addAlias: '添加别名',
|
||||
specifyIdAdd: '可指定_id添加,如果_id已存在,则修改',
|
||||
addIndex: '添加索引',
|
||||
editIndex: '编辑索引',
|
||||
status: '状态',
|
||||
acName: '授权凭证',
|
||||
emptyTable: '无数据',
|
||||
connSuccess: '连接成功',
|
||||
shouldTestConn: '请先测试连接可用性',
|
||||
instance: 'ES实例',
|
||||
instanceSave: '实例保存',
|
||||
instanceDel: '实例删除',
|
||||
operation: '数据操作',
|
||||
dataSave: '数据保存',
|
||||
dataDel: '数据删除',
|
||||
indexName: '索引名',
|
||||
requireIndexName: '请填写索引名',
|
||||
indexDetail: '索引详情',
|
||||
indexMapping: '映射',
|
||||
indexStats: '统计信息',
|
||||
opViewColumns: '设置显示字段',
|
||||
opIndex: '索引管理',
|
||||
opSearch: '搜索',
|
||||
searchParamsPreview: '搜索条件预览',
|
||||
opBasicSearch: '基础搜索',
|
||||
opSeniorSearch: '高级搜索',
|
||||
sampleMappings: 'Mapping示例',
|
||||
copyMappings: '拷贝Mapping',
|
||||
readonlyMsg: '该内容不可修改',
|
||||
opDashboard: '仪表盘',
|
||||
opSettings: '设置',
|
||||
templates: '模板管理',
|
||||
availableSettingFields: '支持修改的字段',
|
||||
Reindex: '索引迁移',
|
||||
ReindexTargetIdx: '目标索引',
|
||||
ReindexIsSync: '是否异步',
|
||||
ReindexDescription: '如果 Mapping 中字段已经定义就不能修改其字段的类型等属性了,同时也不能改变分片的数量, 可以使用 Reindex API 来解决这个问题。',
|
||||
ReindexSyncDescription: '如果索引数据量较大,建议开启异步,以免造成请求超时。',
|
||||
ReindexToOtherInst: '迁移到其他实例',
|
||||
ReindexSyncTask: '异步任务',
|
||||
makeSearchParam: '组装搜索条件',
|
||||
filterColumn: '过滤列名',
|
||||
searchParams: '查询',
|
||||
searchParamsDesc: '未选择字段,或未设置条件值,则不生效',
|
||||
standardSearch: '标准查询',
|
||||
AggregationSearch: '聚合查询',
|
||||
SqlSearch: 'Sql查询',
|
||||
searchError: '查询错误',
|
||||
execError: '执行错误',
|
||||
docJsonError: '文档JSON格式错误',
|
||||
sortParams: '排序',
|
||||
otherParams: '其他',
|
||||
closeIndexConfirm: '将会关闭索引:[{name}]。 确认继续吗?',
|
||||
openIndexConfirm: '将会打开索引:[{name}]。 确认继续吗?',
|
||||
clearCacheConfirm: '将会清除索引:[{name}]缓存。 确认继续吗?',
|
||||
page: {
|
||||
home: '首页',
|
||||
prev: '上一页',
|
||||
next: '下一页',
|
||||
total: '点击切换总条数',
|
||||
changeSize: '修改每页条数',
|
||||
},
|
||||
temp: {
|
||||
addTemp: '添加模板',
|
||||
view: '模板详情',
|
||||
name: '模板名',
|
||||
priority: '优先级',
|
||||
index_patterns: '匹配模式',
|
||||
content: '模板内容',
|
||||
showHide: '显示隐藏模板',
|
||||
description: '描述信息',
|
||||
filter: '模糊过滤名字和描述',
|
||||
versionAlert: '暂不支持 7.8 以前的版本',
|
||||
note: `1、在新建索引时,如果索引名与索引模板的通配符匹配,那么就使用索引模板的设置(_setting、_mapping等)。
|
||||
2、模板仅在索引创建时才会生效,而且修改模板不会影响现有的索引。
|
||||
3、可以指定"priority"的数值,7.8版本前是"order",如果新建的索引名匹配到了多个模板,则优先使用priority最小的那个。`,
|
||||
},
|
||||
dashboard: {
|
||||
instInfo: '实例信息',
|
||||
clusterHealth: '集群健康',
|
||||
nodes: '节点信息',
|
||||
sysMem: '系统内存',
|
||||
jvmMem: 'JVM内存',
|
||||
fileSystem: '文件系统',
|
||||
analyze: '字段分析',
|
||||
idxName: '索引名',
|
||||
field: '字段名',
|
||||
text: '文本',
|
||||
startAnalyze: '开始分析',
|
||||
},
|
||||
contextmenu: {
|
||||
index: {
|
||||
addIndex: '添加索引',
|
||||
showSys: '显示系统索引',
|
||||
copyName: '复制名字',
|
||||
refresh: '刷新索引',
|
||||
flush: 'flush索引',
|
||||
clearCache: '清除索引缓存',
|
||||
addAlias: '添加别名',
|
||||
Close: '关闭索引',
|
||||
Open: '打开索引',
|
||||
Delete: '删除索引',
|
||||
edit: '编辑索引',
|
||||
DeleteSelectLine: '删除选中行',
|
||||
BaseSearch: '基本搜索',
|
||||
SeniorSearch: '高级搜索',
|
||||
copyLineJson: '复制整行JSON',
|
||||
copySelectLineJson: '复制选中行JSON',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -6,23 +6,23 @@ export default {
|
||||
triggeringCondition: '触发条件',
|
||||
triggeringConditionTips: 'go template语法。若输出结果为1,则表示触发该审批流程',
|
||||
conditionPlaceholder: '触发条件, 返回值=1, 则表示触发该审批流程',
|
||||
conditionDefault: `{{/* DBMS-执行sql规则; param参数描述如下 */}}
|
||||
{{/* stmtType: select / read / insert / update / delete / ddl ; */}}
|
||||
{{ if eq .bizType "db_sql_exec_flow"}}
|
||||
{{/* 不是select和read语句时,开启流程审批 */}}
|
||||
{{ if and (ne .param.stmtType "select") (ne .param.stmtType "read") }}
|
||||
conditionDefault: `{'{{'}/* DBMS-执行sql规则; param参数描述如下 */{'}}'}
|
||||
{'{{'}/* stmtType: select / read / insert / update / delete / ddl ; */{'}}'}
|
||||
{'{{'} if eq .bizType "db_sql_exec_flow"{'}}'}
|
||||
{'{{'}/* 不是select和read语句时,开启流程审批 */{'}}'}
|
||||
{'{{'} if and (ne .param.stmtType "select") (ne .param.stmtType "read"){'}}'}
|
||||
1
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{'{{'} end {'}}'}
|
||||
{'{{'} end {'}}'}
|
||||
|
||||
{{/* Redis-执行命令规则; param参数描述如下 */}}
|
||||
{{/* cmdType: read(读命令) / write(写命令); */}}
|
||||
{{/* cmd: get/set/hset...等 */}}
|
||||
{{ if eq .bizType "redis_run_cmd_flow"}}
|
||||
{{ if eq .param.cmdType "write" }}
|
||||
{'{{'}/* Redis-执行命令规则; param参数描述如下 */{'}}'}
|
||||
{'{{'}/* cmdType: read(读命令) / write(写命令); */{'}}'}
|
||||
{'{{'}/* cmd: get/set/hset...等 */{'}}'}
|
||||
{'{{'} if eq .bizType "redis_run_cmd_flow"{'}}'}
|
||||
{'{{'} if eq .param.cmdType "write" {'}}'}
|
||||
1
|
||||
{{ end }}
|
||||
{{ end }}`,
|
||||
{'{{'} end {'}}'}
|
||||
{'{{'} end {'}}'}`,
|
||||
nodeName: '节点名称',
|
||||
nodeNameTips: '点击指定节点可进行拖拽排序',
|
||||
auditor: '审核人员',
|
||||
@@ -32,6 +32,27 @@ export default {
|
||||
enable: '启用',
|
||||
disable: '禁用',
|
||||
|
||||
todoTask: '待办任务',
|
||||
doneTask: '已办任务',
|
||||
flowDesign: '流程设计',
|
||||
clear: '清空',
|
||||
approvalMode: '审批模式',
|
||||
andSign: '会签',
|
||||
orSign: '或签',
|
||||
voteSign: '票签',
|
||||
taskCandidate: '处理候选人',
|
||||
mustOneStartNode: '流程必须要有一个开始节点',
|
||||
mustOneEndNode: '流程必须要有一个结束节点',
|
||||
mustOneOutEdgeForStartNode: '开始节点必须有出线',
|
||||
mustOneInEdgeForEndNode: '结束节点必须有入线',
|
||||
approvalRecord: '审批记录',
|
||||
start: '开始',
|
||||
end: '结束',
|
||||
usertask: '用户任务',
|
||||
serial: '互斥网关',
|
||||
parallel: '并行网关',
|
||||
flowEdge: '流程线',
|
||||
|
||||
// procinst
|
||||
startProcess: '发起流程',
|
||||
cancelProcessConfirm: '确认取消该流程?',
|
||||
@@ -57,7 +78,7 @@ export default {
|
||||
selectRedisPlaceholder: '请选择Redis实例与库',
|
||||
cmdPlaceholder: `如: SET 'key' 'value'; 多条命令;分割`,
|
||||
// ProcinstStatusEnum
|
||||
active: '执行中',
|
||||
active: '审批中',
|
||||
completed: '完成',
|
||||
suspended: '挂起',
|
||||
terminated: '终止',
|
||||
@@ -83,10 +104,12 @@ export default {
|
||||
approveNode: '审批节点',
|
||||
approveForm: '审批表单',
|
||||
approveResult: '审批结果',
|
||||
approvalRemark: '审批意见',
|
||||
approver: '审批人',
|
||||
audit: '审核',
|
||||
procinstStatus: '流程状态',
|
||||
taskStatus: '任务状态',
|
||||
taskName: '当前节点',
|
||||
taskName: '任务名',
|
||||
taskBeginTime: '开始时间',
|
||||
flowAudit: '流程审批',
|
||||
notify: '通知',
|
||||
|
||||
@@ -66,7 +66,7 @@ export default {
|
||||
processName: '进程名',
|
||||
selectSortType: '请选择排序类型',
|
||||
selectProcessNum: '请选择进程个数',
|
||||
cpuDesc: 'CUP降序',
|
||||
cpuDesc: 'CPU降序',
|
||||
memDesc: '内存降序',
|
||||
virtualMemory: '虚拟内存',
|
||||
fixedMemory: '固定内存',
|
||||
@@ -88,6 +88,8 @@ export default {
|
||||
scriptResultEnumRealTime: '实时交互',
|
||||
scriptTypeEnumPrivate: '私有',
|
||||
scriptTypeEnumPublic: '公共',
|
||||
category: '分类',
|
||||
categoryTips: '支持输入新分类并选择',
|
||||
|
||||
// security
|
||||
cmdConfig: '命令配置',
|
||||
@@ -137,5 +139,7 @@ export default {
|
||||
fileTooLargeTips: '文件太大, 请下载使用',
|
||||
uploadSuccess: '上传成功',
|
||||
fileExceedsSysConf: '上传的文件超过系统配置的【{uploadMaxFileSize}】',
|
||||
fileUploadSuccess: '机器文件上传成功',
|
||||
fileUploadFail: '机器文件上传失败',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -25,8 +25,7 @@ export default {
|
||||
success: '成功',
|
||||
menuCodeTips: `菜单类型则为访问路径(若菜单路径不以'/'开头则访问地址会自动拼接父菜单路径)、否则为资源唯一编码`,
|
||||
menuCodePlaceholder: `菜单不以'/'开头则自动拼接父菜单路径`,
|
||||
routerNameTips: '与vue的组件名一致才可使组件缓存生效,如ResourceList',
|
||||
componentPathTips: '访问的组件路径,如:`system/resource/ResourceList`,默认在`views`目录下',
|
||||
routerNameTips: '前端模块下route.ts中对应的key,与vue的组件名一致才可使组件缓存生效,如ResourceList',
|
||||
isCacheTips: '选择是则会被`keep-alive`缓存(重新进入页面不会刷新页面及重新请求数据),需要路由名与vue的组件名一致',
|
||||
isHideTips: '选择隐藏则路由将不会出现在菜单栏中,但仍然可以访问。禁用则不可访问与操作',
|
||||
externalLinkTips: '内嵌: 以iframe展示、外链: 新标签打开',
|
||||
|
||||
@@ -9,6 +9,7 @@ export default {
|
||||
tagTips3: '3. 拥有父标签的团队成员可访问操作其自身或子标签关联的资源',
|
||||
machine: '机器',
|
||||
db: '数据库',
|
||||
es: 'ES',
|
||||
code: '编号',
|
||||
createSubTag: '创建子标签',
|
||||
createSubTagTitle: '创建【{codePath}】的子标签',
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<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" />
|
||||
</el-scrollbar>
|
||||
</el-aside>
|
||||
<el-drawer v-model="themeConfig.isCollapse" :with-header="false" direction="ltr" size="220px" v-else>
|
||||
<el-aside class="layout-aside w100 h100">
|
||||
<el-aside class="layout-aside !w-full !h-full">
|
||||
<Logo v-if="setShowLogo" />
|
||||
<el-scrollbar class="flex-auto" ref="layoutAsideScrollbarRef">
|
||||
<Vertical :menuList="state.menuList" />
|
||||
@@ -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>
|
||||
|
||||
@@ -1,69 +1,96 @@
|
||||
<template>
|
||||
<div class="layout-columns-aside">
|
||||
<div class="w-[64px] h-full bg-[var(--bg-columnsMenuBar)]">
|
||||
<el-scrollbar>
|
||||
<ul>
|
||||
<li v-for="(v, k) in state.columnsAsideList" :key="k" @click="onColumnsAsideMenuClick(v, k)" :ref="(el) => {
|
||||
if (el) columnsAsideOffsetTopRefs[k] = el;
|
||||
}
|
||||
" :class="{ 'layout-columns-active': state.liIndex === k }" :title="$t(v.meta.title)">
|
||||
<div class="layout-columns-aside-li-box"
|
||||
v-if="!v.meta.link || (v.meta.link && v.meta.linkType == 1)">
|
||||
<ul class="relative">
|
||||
<li
|
||||
v-for="(v, k) in state.columnsAsideList"
|
||||
:key="k"
|
||||
@click="onColumnsAsideMenuClick(v, k)"
|
||||
:ref="
|
||||
(el) => {
|
||||
if (el) columnsAsideOffsetTopRefs[k] = el;
|
||||
}
|
||||
"
|
||||
: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="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 font12">
|
||||
{{ $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 font12">
|
||||
{{ $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) => {
|
||||
@@ -76,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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
<template>
|
||||
<el-main class="layout-main">
|
||||
<el-scrollbar ref="layoutScrollbarRef" 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="h100" :meta="state.currentRouteMeta" v-if="state.currentRouteMeta.link && state.currentRouteMeta.linkType == 2" />
|
||||
|
||||
<Iframes
|
||||
class="h100"
|
||||
: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,61 +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({
|
||||
headerHeight: '',
|
||||
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);
|
||||
};
|
||||
// 设置 main 的高度
|
||||
const initHeaderHeight = () => {
|
||||
let { isTagsview } = themeConfig.value;
|
||||
if (isTagsview) return (state.headerHeight = `77px`);
|
||||
else return (state.headerHeight = `50px`);
|
||||
};
|
||||
// 页面加载前
|
||||
onBeforeMount(() => {
|
||||
initCurrentRouteMeta(route.meta);
|
||||
initHeaderHeight();
|
||||
});
|
||||
// 监听 themeConfig 配置文件的变化,更新菜单 el-scrollbar 的高度
|
||||
watch(themeConfig.value, (val) => {
|
||||
state.headerHeight = val.isTagsview ? '77px' : '50px';
|
||||
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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,24 +1,29 @@
|
||||
<template>
|
||||
<el-container class="layout-container flex-center">
|
||||
<Header />
|
||||
<el-container class="layout-mian-height-50">
|
||||
<el-container class="flex-1 overflow-auto">
|
||||
<Aside />
|
||||
<div class="flex-center layout-backtop">
|
||||
<TagsView v-if="themeConfig.isTagsview" />
|
||||
<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>
|
||||
|
||||
@@ -5,24 +5,26 @@
|
||||
<Aside />
|
||||
<el-container class="flex-center layout-backtop">
|
||||
<Header v-if="isFixedHeader" />
|
||||
<el-scrollbar>
|
||||
<Header v-if="!isFixedHeader" />
|
||||
<Main />
|
||||
</el-scrollbar>
|
||||
<Header v-if="!isFixedHeader" />
|
||||
<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;
|
||||
});
|
||||
|
||||
@@ -3,23 +3,21 @@
|
||||
<Aside />
|
||||
<el-container class="flex-center layout-backtop">
|
||||
<Header v-if="isFixedHeader" />
|
||||
<el-scrollbar ref="layoutDefaultsScrollbarRef">
|
||||
<Header v-if="!isFixedHeader" />
|
||||
<Main />
|
||||
</el-scrollbar>
|
||||
<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(() => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" mode="out-in">
|
||||
<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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
</el-icon>
|
||||
</template>
|
||||
<template #default="{ item }">
|
||||
<div><SvgIcon :name="item.meta.icon" class="mr5" />{{ $t(item.meta.title) }}</div>
|
||||
<div><SvgIcon :name="item.meta.icon" class="mr-1" />{{ $t(item.meta.title) }}</div>
|
||||
</template>
|
||||
</el-autocomplete>
|
||||
</el-dialog>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="themeConfig.terminalTheme == 'custom'">
|
||||
<div class="layout-breadcrumb-seting-bar-flex mt10">
|
||||
<div class="layout-breadcrumb-seting-bar-flex !mt-2">
|
||||
<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('layout.config.fontColor') }}</div>
|
||||
<div class="layout-breadcrumb-seting-bar-flex-value">
|
||||
<el-color-picker v-model="themeConfig.terminalForeground" size="small" @change="onColorPickerChange('terminalForeground')">
|
||||
@@ -37,14 +37,14 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="layout-breadcrumb-seting-bar-flex mt10">
|
||||
<div class="layout-breadcrumb-seting-bar-flex !mt-2">
|
||||
<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('layout.config.fontSize') }}</div>
|
||||
<div class="layout-breadcrumb-seting-bar-flex-value">
|
||||
<el-input-number v-model="themeConfig.terminalFontSize" controls-position="right" :min="12" :max="24" size="small" style="width: 90px">
|
||||
</el-input-number>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout-breadcrumb-seting-bar-flex mt10">
|
||||
<div class="layout-breadcrumb-seting-bar-flex !mt-2">
|
||||
<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('layout.config.fontWeight') }}</div>
|
||||
<div class="layout-breadcrumb-seting-bar-flex-value">
|
||||
<el-select v-model="themeConfig.terminalFontWeight" size="small" style="width: 90px">
|
||||
@@ -68,7 +68,7 @@
|
||||
|
||||
<!-- 全局设置 -->
|
||||
<el-divider content-position="left">{{ $t('layout.config.globalSetting') }}</el-divider>
|
||||
<div class="layout-breadcrumb-seting-bar-flex mt15">
|
||||
<div class="layout-breadcrumb-seting-bar-flex !mt-3.5">
|
||||
<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('layout.config.pagesize') }}</div>
|
||||
<div class="layout-breadcrumb-seting-bar-flex-value">
|
||||
<el-input-number
|
||||
@@ -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') }}
|
||||
@@ -185,7 +176,7 @@
|
||||
</el-color-picker>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout-breadcrumb-seting-bar-flex mt10">
|
||||
<div class="layout-breadcrumb-seting-bar-flex !mt-2">
|
||||
<div class="layout-breadcrumb-seting-bar-flex-label">顶栏背景渐变</div>
|
||||
<div class="layout-breadcrumb-seting-bar-flex-value">
|
||||
<el-switch v-model="themeConfig.isTopBarColorGradual"
|
||||
@@ -211,7 +202,7 @@
|
||||
<el-switch v-model="themeConfig.isCollapse" @change="onThemeConfigChange"></el-switch>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout-breadcrumb-seting-bar-flex mt15">
|
||||
<div class="layout-breadcrumb-seting-bar-flex !mt-3.5">
|
||||
<div class="layout-breadcrumb-seting-bar-flex-label">
|
||||
{{ $t('layout.config.isUniqueOpened') }}
|
||||
</div>
|
||||
@@ -219,7 +210,7 @@
|
||||
<el-switch v-model="themeConfig.isUniqueOpened"></el-switch>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout-breadcrumb-seting-bar-flex mt15">
|
||||
<div class="layout-breadcrumb-seting-bar-flex !mt-3.5">
|
||||
<div class="layout-breadcrumb-seting-bar-flex-label">
|
||||
{{ $t('layout.config.isFixedHeader') }}
|
||||
</div>
|
||||
@@ -227,7 +218,7 @@
|
||||
<el-switch v-model="themeConfig.isFixedHeader" @change="onIsFixedHeaderChange"></el-switch>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout-breadcrumb-seting-bar-flex mt15" :style="{ opacity: themeConfig.layout !== 'classic' ? 0.5 : 1 }">
|
||||
<div class="layout-breadcrumb-seting-bar-flex !mt-3.5" :style="{ opacity: themeConfig.layout !== 'classic' ? 0.5 : 1 }">
|
||||
<div class="layout-breadcrumb-seting-bar-flex-label">
|
||||
{{ $t('layout.config.isClassicSplitMenu') }}
|
||||
</div>
|
||||
@@ -236,33 +227,16 @@
|
||||
</el-switch>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout-breadcrumb-seting-bar-flex mt15">
|
||||
<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>
|
||||
<div class="layout-breadcrumb-seting-bar-flex mt15">
|
||||
<div class="layout-breadcrumb-seting-bar-flex !mt-3.5">
|
||||
<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('layout.config.isShowLogo') }}</div>
|
||||
<div class="layout-breadcrumb-seting-bar-flex-value">
|
||||
<el-switch v-model="themeConfig.isShowLogo" @change="onIsShowLogoChange"></el-switch>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout-breadcrumb-seting-bar-flex mt15" :style="{ opacity: themeConfig.layout === 'transverse' ? 0.5 : 1 }">
|
||||
<div class="layout-breadcrumb-seting-bar-flex !mt-3.5" :style="{ opacity: themeConfig.layout === 'transverse' ? 0.5 : 1 }">
|
||||
<div class="layout-breadcrumb-seting-bar-flex-label">
|
||||
{{ $t('layout.config.isBreadcrumb') }}
|
||||
</div>
|
||||
@@ -274,7 +248,7 @@
|
||||
></el-switch>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout-breadcrumb-seting-bar-flex mt15">
|
||||
<div class="layout-breadcrumb-seting-bar-flex !mt-3.5">
|
||||
<div class="layout-breadcrumb-seting-bar-flex-label">
|
||||
{{ $t('layout.config.isBreadcrumbIcon') }}
|
||||
</div>
|
||||
@@ -282,13 +256,13 @@
|
||||
<el-switch v-model="themeConfig.isBreadcrumbIcon"></el-switch>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout-breadcrumb-seting-bar-flex mt15">
|
||||
<div class="layout-breadcrumb-seting-bar-flex !mt-3.5">
|
||||
<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('layout.config.isTagsview') }}</div>
|
||||
<div class="layout-breadcrumb-seting-bar-flex-value">
|
||||
<el-switch v-model="themeConfig.isTagsview"></el-switch>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout-breadcrumb-seting-bar-flex mt15">
|
||||
<div class="layout-breadcrumb-seting-bar-flex !mt-3.5">
|
||||
<div class="layout-breadcrumb-seting-bar-flex-label">
|
||||
{{ $t('layout.config.isTagsviewIcon') }}
|
||||
</div>
|
||||
@@ -296,7 +270,7 @@
|
||||
<el-switch v-model="themeConfig.isTagsviewIcon"></el-switch>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout-breadcrumb-seting-bar-flex mt15">
|
||||
<div class="layout-breadcrumb-seting-bar-flex !mt-3.5">
|
||||
<div class="layout-breadcrumb-seting-bar-flex-label">
|
||||
{{ $t('layout.config.isCacheTagsView') }}
|
||||
</div>
|
||||
@@ -304,27 +278,27 @@
|
||||
<el-switch v-model="themeConfig.isCacheTagsView"></el-switch>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout-breadcrumb-seting-bar-flex mt15">
|
||||
<div class="layout-breadcrumb-seting-bar-flex !mt-3.5">
|
||||
<div class="layout-breadcrumb-seting-bar-flex-label">
|
||||
{{ $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 mt15">
|
||||
<div class="layout-breadcrumb-seting-bar-flex !mt-3.5">
|
||||
<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('layout.config.isFooter') }}</div>
|
||||
<div class="layout-breadcrumb-seting-bar-flex-value">
|
||||
<el-switch v-model="themeConfig.isFooter"></el-switch>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout-breadcrumb-seting-bar-flex mt15">
|
||||
<div class="layout-breadcrumb-seting-bar-flex !mt-3.5">
|
||||
<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('layout.config.isGrayscale') }}</div>
|
||||
<div class="layout-breadcrumb-seting-bar-flex-value">
|
||||
<el-switch v-model="themeConfig.isGrayscale" @change="onAddFilterChange('grayscale')"></el-switch>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout-breadcrumb-seting-bar-flex mt15">
|
||||
<div class="layout-breadcrumb-seting-bar-flex !mt-3.5">
|
||||
<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('layout.config.isInvert') }}</div>
|
||||
<div class="layout-breadcrumb-seting-bar-flex-value">
|
||||
<el-switch v-model="themeConfig.isInvert" @change="onAddFilterChange('invert')"></el-switch>
|
||||
@@ -333,7 +307,7 @@
|
||||
|
||||
<!-- 其它设置 -->
|
||||
<el-divider content-position="left">{{ $t('layout.config.otherSetting') }}</el-divider>
|
||||
<div class="layout-breadcrumb-seting-bar-flex mt15">
|
||||
<div class="layout-breadcrumb-seting-bar-flex !mt-3.5">
|
||||
<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('layout.config.tagsStyle') }}</div>
|
||||
<div class="layout-breadcrumb-seting-bar-flex-value">
|
||||
<el-select v-model="themeConfig.tagsStyle" placeholder="请选择" size="small" style="width: 90px">
|
||||
@@ -343,7 +317,7 @@
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout-breadcrumb-seting-bar-flex mt15">
|
||||
<div class="layout-breadcrumb-seting-bar-flex !mt-3.5">
|
||||
<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('layout.config.animation') }}</div>
|
||||
<div class="layout-breadcrumb-seting-bar-flex-value">
|
||||
<el-select v-model="themeConfig.animation" size="small" style="width: 90px">
|
||||
@@ -353,7 +327,7 @@
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout-breadcrumb-seting-bar-flex mt15 mb28">
|
||||
<div class="layout-breadcrumb-seting-bar-flex !mt-3.5 !mb-5.5">
|
||||
<div class="layout-breadcrumb-seting-bar-flex-label">
|
||||
{{ $t('layout.config.columnsAsideStyle') }}
|
||||
</div>
|
||||
@@ -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,23 @@ 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 = () => {
|
||||
const oldLayout = getLocal('oldLayout');
|
||||
if (!oldLayout) {
|
||||
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">
|
||||
|
||||
@@ -41,28 +41,30 @@
|
||||
<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 class="layout-navbars-breadcrumb-user-icon mr10" @click="onScreenfullClick">
|
||||
</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" style="cursor: pointer">
|
||||
<img :src="userInfo.photo" class="layout-navbars-breadcrumb-user-link-photo mr5" />
|
||||
<span class="layout-navbars-breadcrumb-user-link cursor-pointer">
|
||||
<img :src="userInfo.photo" class="layout-navbars-breadcrumb-user-link-photo mr-1" />
|
||||
{{ userInfo.name || userInfo.username }}
|
||||
<i class="el-icon-arrow-down el-icon--right"></i>
|
||||
</span>
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -16,18 +16,17 @@
|
||||
}
|
||||
"
|
||||
>
|
||||
<SvgIcon name="icon layout/tag-view-active" class="layout-navbars-tagsview-ul-li-iconfont font14" v-if="isActive(v)" />
|
||||
<SvgIcon :name="v.icon" class="layout-navbars-tagsview-ul-li-iconfont" v-if="!isActive(v) && themeConfig.isTagsviewIcon" />
|
||||
<SvgIcon :name="v.icon" class="layout-navbars-tagsview-ul-li-iconfont" v-if="themeConfig.isTagsviewIcon" />
|
||||
<span>{{ $t(v.title) }}</span>
|
||||
<template v-if="isActive(v)">
|
||||
<SvgIcon
|
||||
name="RefreshRight"
|
||||
class="font14 ml5 layout-navbars-tagsview-ul-li-refresh"
|
||||
class="!text-[14px] ml-1 layout-navbars-tagsview-ul-li-refresh"
|
||||
@click.stop="refreshCurrentTagsView($route.fullPath)"
|
||||
/>
|
||||
<SvgIcon
|
||||
name="Close"
|
||||
class="font14 layout-navbars-tagsview-ul-li-icon layout-icon-active"
|
||||
class="!text-[14px] layout-navbars-tagsview-ul-li-icon layout-icon-active"
|
||||
v-if="!v.isAffix"
|
||||
@click.stop="closeCurrentTagsView(themeConfig.isShareTagsView ? v.path : v.path)"
|
||||
/>
|
||||
@@ -35,7 +34,7 @@
|
||||
|
||||
<SvgIcon
|
||||
name="Close"
|
||||
class="font14 layout-navbars-tagsview-ul-li-icon layout-icon-three"
|
||||
class="!text-[14px] layout-navbars-tagsview-ul-li-icon layout-icon-three"
|
||||
v-if="!v.isAffix"
|
||||
@click.stop="closeCurrentTagsView(themeConfig.isShareTagsView ? v.path : v.path)"
|
||||
/>
|
||||
@@ -47,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';
|
||||
@@ -186,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) => {
|
||||
@@ -376,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 = [];
|
||||
@@ -521,7 +516,8 @@ onBeforeRouteUpdate((to) => {
|
||||
align-items: flex-end;
|
||||
|
||||
.tgs-style-three-svg {
|
||||
-webkit-mask-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNzAiIGhlaWdodD0iNzAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgZmlsbD0ibm9uZSI+CgogPGc+CiAgPHRpdGxlPkxheWVyIDE8L3RpdGxlPgogIDxwYXRoIHRyYW5zZm9ybT0icm90YXRlKC0wLjEzMzUwNiA1MC4xMTkyIDUwKSIgaWQ9InN2Z18xIiBkPSJtMTAwLjExOTE5LDEwMGMtNTUuMjI4LDAgLTEwMCwtNDQuNzcyIC0xMDAsLTEwMGwwLDEwMGwxMDAsMHoiIG9wYWNpdHk9InVuZGVmaW5lZCIgc3Ryb2tlPSJudWxsIiBmaWxsPSIjRjhFQUU3Ii8+CiAgPHBhdGggZD0ibS0wLjYzNzY2LDcuMzEyMjhjMC4xMTkxOSwwIDAuMjE3MzcsMC4wNTc5NiAwLjQ3Njc2LDAuMTE5MTljMC4yMzIsMC4wNTQ3NyAwLjI3MzI5LDAuMDM0OTEgMC4zNTc1NywwLjExOTE5YzAuMDg0MjgsMC4wODQyOCAwLjM1NzU3LDAgMC40NzY3NiwwbDAuMTE5MTksMGwwLjIzODM4LDAiIGlkPSJzdmdfMiIgc3Ryb2tlPSJudWxsIiBmaWxsPSJub25lIi8+CiAgPHBhdGggZD0ibTI4LjkyMTM0LDY5LjA1MjQ0YzAsMC4xMTkxOSAwLDAuMjM4MzggMCwwLjM1NzU3bDAsMC4xMTkxOWwwLDAuMTE5MTkiIGlkPSJzdmdfMyIgc3Ryb2tlPSJudWxsIiBmaWxsPSJub25lIi8+CiAgPHJlY3QgaWQ9InN2Z180IiBoZWlnaHQ9IjAiIHdpZHRoPSIxLjMxMTA4IiB5PSI2LjgzNTUyIiB4PSItMC4wNDE3MSIgc3Ryb2tlPSJudWxsIiBmaWxsPSJub25lIi8+CiAgPHJlY3QgaWQ9InN2Z181IiBoZWlnaHQ9IjEuNzg3ODQiIHdpZHRoPSIwLjExOTE5IiB5PSI2OC40NTY1IiB4PSIyOC45MjEzNCIgc3Ryb2tlPSJudWxsIiBmaWxsPSJub25lIi8+CiAgPHJlY3QgaWQ9InN2Z182IiBoZWlnaHQ9IjQuODg2NzciIHdpZHRoPSIxOS4wNzAzMiIgeT0iNTEuMjkzMjEiIHg9IjM2LjY2ODY2IiBzdHJva2U9Im51bGwiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+'),
|
||||
-webkit-mask-image:
|
||||
url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNzAiIGhlaWdodD0iNzAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgZmlsbD0ibm9uZSI+CgogPGc+CiAgPHRpdGxlPkxheWVyIDE8L3RpdGxlPgogIDxwYXRoIHRyYW5zZm9ybT0icm90YXRlKC0wLjEzMzUwNiA1MC4xMTkyIDUwKSIgaWQ9InN2Z18xIiBkPSJtMTAwLjExOTE5LDEwMGMtNTUuMjI4LDAgLTEwMCwtNDQuNzcyIC0xMDAsLTEwMGwwLDEwMGwxMDAsMHoiIG9wYWNpdHk9InVuZGVmaW5lZCIgc3Ryb2tlPSJudWxsIiBmaWxsPSIjRjhFQUU3Ii8+CiAgPHBhdGggZD0ibS0wLjYzNzY2LDcuMzEyMjhjMC4xMTkxOSwwIDAuMjE3MzcsMC4wNTc5NiAwLjQ3Njc2LDAuMTE5MTljMC4yMzIsMC4wNTQ3NyAwLjI3MzI5LDAuMDM0OTEgMC4zNTc1NywwLjExOTE5YzAuMDg0MjgsMC4wODQyOCAwLjM1NzU3LDAgMC40NzY3NiwwbDAuMTE5MTksMGwwLjIzODM4LDAiIGlkPSJzdmdfMiIgc3Ryb2tlPSJudWxsIiBmaWxsPSJub25lIi8+CiAgPHBhdGggZD0ibTI4LjkyMTM0LDY5LjA1MjQ0YzAsMC4xMTkxOSAwLDAuMjM4MzggMCwwLjM1NzU3bDAsMC4xMTkxOWwwLDAuMTE5MTkiIGlkPSJzdmdfMyIgc3Ryb2tlPSJudWxsIiBmaWxsPSJub25lIi8+CiAgPHJlY3QgaWQ9InN2Z180IiBoZWlnaHQ9IjAiIHdpZHRoPSIxLjMxMTA4IiB5PSI2LjgzNTUyIiB4PSItMC4wNDE3MSIgc3Ryb2tlPSJudWxsIiBmaWxsPSJub25lIi8+CiAgPHJlY3QgaWQ9InN2Z181IiBoZWlnaHQ9IjEuNzg3ODQiIHdpZHRoPSIwLjExOTE5IiB5PSI2OC40NTY1IiB4PSIyOC45MjEzNCIgc3Ryb2tlPSJudWxsIiBmaWxsPSJub25lIi8+CiAgPHJlY3QgaWQ9InN2Z182IiBoZWlnaHQ9IjQuODg2NzciIHdpZHRoPSIxOS4wNzAzMiIgeT0iNTEuMjkzMjEiIHg9IjM2LjY2ODY2IiBzdHJva2U9Im51bGwiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+'),
|
||||
url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNzAiIGhlaWdodD0iNzAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgZmlsbD0ibm9uZSI+CiA8Zz4KICA8dGl0bGU+TGF5ZXIgMTwvdGl0bGU+CiAgPHBhdGggdHJhbnNmb3JtPSJyb3RhdGUoLTg5Ljc2MjQgNy4zMzAxNCA1NS4xMjUyKSIgc3Ryb2tlPSJudWxsIiBpZD0ic3ZnXzEiIGZpbGw9IiNGOEVBRTciIGQ9Im02Mi41NzQ0OSwxMTcuNTIwODZjLTU1LjIyOCwwIC0xMDAsLTQ0Ljc3MiAtMTAwLC0xMDBsMCwxMDBsMTAwLDB6IiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPgogIDxwYXRoIGQ9Im0tMC42Mzc2Niw3LjMxMjI4YzAuMTE5MTksMCAwLjIxNzM3LDAuMDU3OTYgMC40NzY3NiwwLjExOTE5YzAuMjMyLDAuMDU0NzcgMC4yNzMyOSwwLjAzNDkxIDAuMzU3NTcsMC4xMTkxOWMwLjA4NDI4LDAuMDg0MjggMC4zNTc1NywwIDAuNDc2NzYsMGwwLjExOTE5LDBsMC4yMzgzOCwwIiBpZD0ic3ZnXzIiIHN0cm9rZT0ibnVsbCIgZmlsbD0ibm9uZSIvPgogIDxwYXRoIGQ9Im0yOC45MjEzNCw2OS4wNTI0NGMwLDAuMTE5MTkgMCwwLjIzODM4IDAsMC4zNTc1N2wwLDAuMTE5MTlsMCwwLjExOTE5IiBpZD0ic3ZnXzMiIHN0cm9rZT0ibnVsbCIgZmlsbD0ibm9uZSIvPgogIDxyZWN0IGlkPSJzdmdfNCIgaGVpZ2h0PSIwIiB3aWR0aD0iMS4zMTEwOCIgeT0iNi44MzU1MiIgeD0iLTAuMDQxNzEiIHN0cm9rZT0ibnVsbCIgZmlsbD0ibm9uZSIvPgogIDxyZWN0IGlkPSJzdmdfNSIgaGVpZ2h0PSIxLjc4Nzg0IiB3aWR0aD0iMC4xMTkxOSIgeT0iNjguNDU2NSIgeD0iMjguOTIxMzQiIHN0cm9rZT0ibnVsbCIgZmlsbD0ibm9uZSIvPgogIDxyZWN0IGlkPSJzdmdfNiIgaGVpZ2h0PSI0Ljg4Njc3IiB3aWR0aD0iMTkuMDcwMzIiIHk9IjUxLjI5MzIxIiB4PSIzNi42Njg2NiIgc3Ryb2tlPSJudWxsIiBmaWxsPSJub25lIi8+CiA8L2c+Cjwvc3ZnPg=='),
|
||||
url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'><rect rx='8' width='100%' height='100%' fill='%23F8EAE7'/></svg>");
|
||||
-webkit-mask-size:
|
||||
|
||||
@@ -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,16 @@ 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;
|
||||
max-width: 160px;
|
||||
min-width: 100px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,59 +1,114 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="layout-view-bg-white flex h100" 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -9,12 +9,10 @@ import { registElSvgIcon } from '@/common/utils/svgIcons';
|
||||
import ElementPlus from 'element-plus';
|
||||
import 'element-plus/dist/index.css';
|
||||
import 'element-plus/theme-chalk/dark/css-vars.css';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { i18n } from '@/i18n/index';
|
||||
|
||||
import 'splitpanes/dist/splitpanes.css';
|
||||
|
||||
import '@/theme/index.scss';
|
||||
import '@/theme/tailwind.css';
|
||||
import '@/assets/font/font.css';
|
||||
import '@/assets/icon/icon.js';
|
||||
import { getThemeConfig } from './common/utils/storage';
|
||||
@@ -30,12 +28,3 @@ app.use(pinia).use(router).use(i18n).use(ElementPlus, { size: getThemeConfig()?.
|
||||
|
||||
// 屏蔽警告信息
|
||||
app.config.warnHandler = () => null;
|
||||
// 全局error处理
|
||||
app.config.errorHandler = function (err: any, vm, info) {
|
||||
// 如果是断言错误,则进行提示即可
|
||||
if (err.name == 'AssertError') {
|
||||
ElMessage.error(err.message);
|
||||
} else {
|
||||
console.error(err, info);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -7,59 +7,60 @@ 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');
|
||||
|
||||
/**
|
||||
* 获取目录下的 .vue、.tsx 全部文件
|
||||
* 获取目录下的 route.ts 全部文件
|
||||
* @method import.meta.glob
|
||||
* @link 参考:https://cn.vitejs.dev/guide/features.html#json
|
||||
*/
|
||||
const viewsModules: Record<string, Function> = import.meta.glob(['../views/**/*.{vue,tsx}']);
|
||||
const dynamicViewsModules: Record<string, Function> = Object.assign({}, { ...viewsModules });
|
||||
const routeModules: Record<string, any> = import.meta.glob(['../views/**/route.{ts,js}'], { eager: true });
|
||||
|
||||
// 后端控制路由:执行路由数据初始化
|
||||
export async function initBackendRoutes() {
|
||||
const token = getToken(); // 获取浏览器缓存 token 值
|
||||
// 合并所有模块路由
|
||||
const allModuleRoutes = Object.values(routeModules).reduce((acc: any, module: any) => {
|
||||
return { ...acc, ...module.default };
|
||||
}, {});
|
||||
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
// 无 token 停止执行下一步
|
||||
return false;
|
||||
}
|
||||
|
||||
useUserInfo().setUserInfo({});
|
||||
// 获取路由
|
||||
let menuRoute = await getBackEndControlRoutes();
|
||||
|
||||
const cacheList: Array<string> = [];
|
||||
// 处理路由(component)
|
||||
const routes = backEndRouterConverter(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();
|
||||
@@ -77,9 +78,8 @@ type RouterConvCallbackFunc = (router: any) => void;
|
||||
* @param name ==> title,路由标题 相当于route.meta.title
|
||||
*
|
||||
* @param meta ==> 路由菜单元信息
|
||||
* @param meta.routeName ==> route.name -> 路由 name (对应页面组件 name, 可用作 KeepAlive 缓存标识 && 按钮权限筛选)
|
||||
* @param meta.routeName ==> route.name -> 路由 name (对应页面组件 name, 可用作 KeepAlive 缓存标识 && 按钮权限筛选) -> 对应模块下route.ts字段key
|
||||
* @param meta.redirect ==> route.redirect -> 路由重定向地址
|
||||
* @param meta.component ==> 文件路径
|
||||
* @param meta.icon ==> 菜单和面包屑对应的图标
|
||||
* @param meta.isHide ==> 是否在菜单中隐藏 (通常列表详情页需要隐藏)
|
||||
* @param meta.isFull ==> 菜单是否全屏 (示例:数据大屏页面)
|
||||
@@ -88,77 +88,52 @@ type RouterConvCallbackFunc = (router: any) => void;
|
||||
* @param meta.linkType ==> 外链类型, 内嵌: 以iframe展示、外链: 新标签打开
|
||||
* @param meta.link ==> 外链地址
|
||||
* */
|
||||
export function backEndRouterConverter(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);
|
||||
|
||||
// 将meta.comoponet 解析为route.component
|
||||
if (item.meta.component) {
|
||||
item.component = dynamicImport(dynamicViewsModules, item.meta.component);
|
||||
delete item.meta['component'];
|
||||
}
|
||||
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;
|
||||
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(item.children, callbackFunc, item.path);
|
||||
routeItems.push(item);
|
||||
}
|
||||
|
||||
return routeItems;
|
||||
}
|
||||
// 处理重定向
|
||||
if (meta.redirect) {
|
||||
routeItem.redirect = meta.redirect;
|
||||
}
|
||||
|
||||
/**
|
||||
* 后端路由 component 转换函数
|
||||
* @param dynamicViewsModules 获取目录下的 .vue、.tsx 全部文件
|
||||
* @param component 当前要处理项 component
|
||||
* @returns 返回处理成函数后的 component
|
||||
*/
|
||||
export function dynamicImport(dynamicViewsModules: Record<string, Function>, component: string) {
|
||||
const keys = Object.keys(dynamicViewsModules);
|
||||
const matchKeys = keys.filter((key) => {
|
||||
const k = key.replace(/..\/views|../, '');
|
||||
return k.startsWith(`${component}`) || k.startsWith(`/${component}`);
|
||||
// 处理子路由
|
||||
if (item.children) {
|
||||
routeItem.children = backEndRouterConverter(allModuleRoutes, item.children, callbackFunc, path);
|
||||
}
|
||||
|
||||
// 执行回调
|
||||
callbackFunc?.(routeItem);
|
||||
|
||||
return routeItem;
|
||||
});
|
||||
|
||||
if (matchKeys?.length === 1) {
|
||||
return dynamicViewsModules[matchKeys[0]];
|
||||
}
|
||||
|
||||
if (matchKeys?.length > 1) {
|
||||
console.error('匹配到多个相似组件路径, 可添加后缀.vue或.tsx进行区分或者重命名组件名, 请调整...', matchKeys);
|
||||
return null;
|
||||
}
|
||||
|
||||
console.warn(`未匹配到[${component}]组件名对应的组件文件`);
|
||||
return null;
|
||||
}
|
||||
|
||||