Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57361d8241 | ||
|
|
b347bd7ef5 | ||
|
|
070c8ac0da | ||
|
|
e221c2f42e | ||
|
|
c7bab3a71b | ||
|
|
82c17a51a2 | ||
|
|
e4447e6bc2 | ||
|
|
b9570d9a5f | ||
|
|
01e8a2c14d | ||
|
|
64bd51c3b0 | ||
|
|
54ab34df3f | ||
|
|
206490ba3e | ||
|
|
16612d2c9c | ||
|
|
6b65605360 | ||
|
|
bb37ed3b95 | ||
|
|
d102cc8c08 | ||
|
|
a6df74d63d | ||
|
|
f79760943e | ||
|
|
a40ec21a05 | ||
|
|
43230267b6 | ||
|
|
0ae99cdaf9 | ||
|
|
f234c72514 | ||
|
|
76527d95bd | ||
|
|
27c53385f2 | ||
|
|
a1b25e9766 | ||
|
|
abad0ed481 | ||
|
|
eddda41291 | ||
|
|
d9adf0fd25 | ||
|
|
0ce82b41ba | ||
|
|
37026f3269 | ||
|
|
3155380f16 | ||
|
|
f2b0f294d8 | ||
|
|
12f63ef3dd | ||
|
|
a1303b52eb | ||
|
|
10f6b03fb5 | ||
|
|
45d2449221 | ||
|
|
9e5f146e05 | ||
|
|
2b91bbe185 | ||
|
|
747ea6404d | ||
|
|
ccfc6bd1df | ||
|
|
361eafedae | ||
|
|
a64b894b08 | ||
|
|
0ad805c170 | ||
|
|
ba82b5b516 | ||
|
|
f04b82c933 | ||
|
|
23b137ab9b | ||
|
|
a4d3a4627a | ||
|
|
77ae6e3bab | ||
|
|
e0f1f40ba0 | ||
|
|
d300f604f1 | ||
|
|
2c2c0ff40b | ||
|
|
b4ddbbd38f | ||
|
|
7544288451 | ||
|
|
41443dccc0 | ||
|
|
22e218fc5f | ||
|
|
4da0d1abaa | ||
|
|
6563b53436 | ||
|
|
fac71a4794 | ||
|
|
92dff6fdc3 | ||
|
|
a1eca3d691 | ||
|
|
6681dc1057 | ||
|
|
829a68feaa | ||
|
|
72677e270d |
@@ -18,7 +18,7 @@ WORKDIR /mayfly
|
||||
# Copy the go source for building server
|
||||
COPY server .
|
||||
|
||||
RUN go mod download
|
||||
RUN go mod tidy && go mod download
|
||||
|
||||
COPY --from=fe-builder /mayfly/dist /mayfly/static/static
|
||||
|
||||
|
||||
18
README.md
@@ -1,19 +1,19 @@
|
||||
# 🌈mayfly-go
|
||||
|
||||
<p align="center">
|
||||
<a href="https://gitee.com/objs/mayfly-go" target="_blank">
|
||||
<img src="https://gitee.com/objs/mayfly-go/badge/star.svg?theme=white" alt="star"/>
|
||||
<img src="https://gitee.com/objs/mayfly-go/badge/fork.svg" alt="fork"/>
|
||||
<a href="https://gitee.com/dromara/mayfly-go" target="_blank">
|
||||
<img src="https://gitee.com/dromara/mayfly-go/badge/star.svg?theme=white" alt="star"/>
|
||||
<img src="https://gitee.com/dromara/mayfly-go/badge/fork.svg" alt="fork"/>
|
||||
</a>
|
||||
<a href="https://github.com/may-fly/mayfly-go" target="_blank">
|
||||
<img src="https://img.shields.io/github/stars/may-fly/mayfly-go.svg?style=social" alt="github star"/>
|
||||
<img src="https://img.shields.io/github/forks/may-fly/mayfly-go.svg?style=social" alt="github fork"/>
|
||||
<a href="https://github.com/dromara/mayfly-go" target="_blank">
|
||||
<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://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.20%2B-yellow.svg" alt="golang"/>
|
||||
<img src="https://img.shields.io/badge/Golang-1.21%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">
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
### 介绍
|
||||
|
||||
web 版 **linux(终端[终端回放] 文件 脚本 进程)、数据库(mysql postgres)、redis(单机 哨兵 集群)、mongo 统一管理操作平台**
|
||||
web 版 **linux(终端[终端回放] 文件 脚本 进程 计划任务)、数据库(mysql postgres)、redis(单机 哨兵 集群)、mongo 统一管理操作平台**
|
||||
|
||||
### 开发语言与主要框架
|
||||
|
||||
@@ -100,4 +100,4 @@ http://go.mayfly.run
|
||||
|
||||
#### 💌 支持作者
|
||||
|
||||
如果觉得项目不错,或者已经在使用了,希望你可以去 <a target="_blank" href="https://github.com/may-fly/mayfly-go">Github</a> 或者 <a target="_blank" href="https://gitee.com/objs/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> 帮我点个 ⭐ Star,这将是对我极大的鼓励与支持。
|
||||
|
||||
@@ -74,12 +74,13 @@ function build() {
|
||||
# fi
|
||||
|
||||
if [ "${copyDocScript}" == "1" ] ; then
|
||||
echo_green "拷贝脚本等资源文件[config.yml.example、mayfly-go.sql、readme.txt、startup.sh、shutdown.sh]"
|
||||
echo_green "拷贝脚本等资源文件[config.yml.example、mayfly-go.sql、mayfly-go.sqlite、readme.txt、startup.sh、shutdown.sh]"
|
||||
cp ${server_folder}/config.yml.example ${toFolder}
|
||||
cp ${server_folder}/mayfly-go.sql ${toFolder}
|
||||
cp ${server_folder}/readme.txt ${toFolder}
|
||||
cp ${server_folder}/startup.sh ${toFolder}
|
||||
cp ${server_folder}/shutdown.sh ${toFolder}
|
||||
cp ${server_folder}/resources/script/startup.sh ${toFolder}
|
||||
cp ${server_folder}/resources/script/shutdown.sh ${toFolder}
|
||||
cp ${server_folder}/resources/script/sql/mayfly-go.sql ${toFolder}
|
||||
cp ${server_folder}/resources/data/mayfly-go.sqlite ${toFolder}
|
||||
fi
|
||||
|
||||
echo_yellow ">>>>>>>>>>>>>>>>>>>${os}-${arch}打包构建完成<<<<<<<<<<<<<<<<<<<<\n"
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
ENV = 'development'
|
||||
|
||||
# 本地环境接口地址
|
||||
VITE_API_URL = '/api'
|
||||
VITE_API_URL = '/api'
|
||||
|
||||
# 路由模式
|
||||
# Optional: hash | history
|
||||
VITE_ROUTER_MODE = hash
|
||||
@@ -2,4 +2,8 @@
|
||||
ENV = 'production'
|
||||
|
||||
# 线上环境接口地址
|
||||
VITE_API_URL = '/api'
|
||||
VITE_API_URL = '/api'
|
||||
|
||||
# 路由模式
|
||||
# Optional: hash | history
|
||||
VITE_ROUTER_MODE = hash
|
||||
@@ -1,76 +1,76 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
node: true,
|
||||
},
|
||||
parser: 'vue-eslint-parser',
|
||||
parserOptions: {
|
||||
ecmaVersion: 12,
|
||||
parser: '@typescript-eslint/parser',
|
||||
sourceType: 'module',
|
||||
},
|
||||
extends: ['plugin:vue/vue3-essential', 'plugin:vue/essential', 'eslint:recommended'],
|
||||
plugins: ['vue', '@typescript-eslint'],
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.ts', '*.tsx', '*.vue'],
|
||||
rules: {
|
||||
'no-undef': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
rules: {
|
||||
// http://eslint.cn/docs/rules/
|
||||
// https://eslint.vuejs.org/rules/
|
||||
// https://typescript-eslint.io/rules/no-unused-vars/
|
||||
'@typescript-eslint/ban-ts-ignore': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/no-use-before-define': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'@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],
|
||||
'vue/custom-event-name-casing': 'off',
|
||||
'vue/attributes-order': 'off',
|
||||
'vue/one-component-per-file': 'off',
|
||||
'vue/html-closing-bracket-newline': 'off',
|
||||
'vue/max-attributes-per-line': 'off',
|
||||
'vue/multiline-html-element-content-newline': 'off',
|
||||
'vue/singleline-html-element-content-newline': 'off',
|
||||
'vue/attribute-hyphenation': 'off',
|
||||
'vue/html-self-closing': 'off',
|
||||
'vue/no-multiple-template-root': 'off',
|
||||
'vue/require-default-prop': 'off',
|
||||
'vue/no-v-model-argument': 'off',
|
||||
'vue/no-arrow-functions-in-watch': 'off',
|
||||
'vue/no-template-key': 'off',
|
||||
'vue/no-v-html': 'off',
|
||||
'vue/comment-directive': 'off',
|
||||
'vue/no-parsing-error': 'off',
|
||||
'vue/no-deprecated-v-on-native-modifier': 'off',
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'no-useless-escape': 'off',
|
||||
'no-sparse-arrays': 'off',
|
||||
'no-prototype-builtins': 'off',
|
||||
'no-constant-condition': 'off',
|
||||
'no-use-before-define': 'off',
|
||||
'no-restricted-globals': 'off',
|
||||
'no-restricted-syntax': 'off',
|
||||
'generator-star-spacing': 'off',
|
||||
'no-unreachable': 'off',
|
||||
'no-multiple-template-root': 'off',
|
||||
'no-unused-vars': 'error',
|
||||
'no-v-model-argument': 'off',
|
||||
'no-case-declarations': 'off',
|
||||
'no-console': 'error',
|
||||
'no-redeclare': 'off',
|
||||
},
|
||||
};
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
node: true,
|
||||
},
|
||||
parser: 'vue-eslint-parser',
|
||||
parserOptions: {
|
||||
ecmaVersion: 12,
|
||||
parser: '@typescript-eslint/parser',
|
||||
sourceType: 'module',
|
||||
},
|
||||
extends: ['plugin:vue/vue3-essential', 'plugin:vue/essential', 'eslint:recommended'],
|
||||
plugins: ['vue', '@typescript-eslint'],
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.ts', '*.tsx', '*.vue'],
|
||||
rules: {
|
||||
'no-undef': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
rules: {
|
||||
// http://eslint.cn/docs/rules/
|
||||
// https://eslint.vuejs.org/rules/
|
||||
// https://typescript-eslint.io/rules/no-unused-vars/
|
||||
'@typescript-eslint/ban-ts-ignore': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/no-use-before-define': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'@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],
|
||||
'vue/custom-event-name-casing': 'off',
|
||||
'vue/attributes-order': 'off',
|
||||
'vue/one-component-per-file': 'off',
|
||||
'vue/html-closing-bracket-newline': 'off',
|
||||
'vue/max-attributes-per-line': 'off',
|
||||
'vue/multiline-html-element-content-newline': 'off',
|
||||
'vue/singleline-html-element-content-newline': 'off',
|
||||
'vue/attribute-hyphenation': 'off',
|
||||
'vue/html-self-closing': 'off',
|
||||
'vue/no-multiple-template-root': 'off',
|
||||
'vue/require-default-prop': 'off',
|
||||
'vue/no-v-model-argument': 'off',
|
||||
'vue/no-arrow-functions-in-watch': 'off',
|
||||
'vue/no-template-key': 'off',
|
||||
'vue/no-v-html': 'off',
|
||||
'vue/comment-directive': 'off',
|
||||
'vue/no-parsing-error': 'off',
|
||||
'vue/no-deprecated-v-on-native-modifier': 'off',
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'no-useless-escape': 'off',
|
||||
'no-sparse-arrays': 'off',
|
||||
'no-prototype-builtins': 'off',
|
||||
'no-constant-condition': 'off',
|
||||
'no-use-before-define': 'off',
|
||||
'no-restricted-globals': 'off',
|
||||
'no-restricted-syntax': 'off',
|
||||
'generator-star-spacing': 'off',
|
||||
'no-unreachable': 'off',
|
||||
'no-multiple-template-root': 'off',
|
||||
'no-unused-vars': 'error',
|
||||
'no-v-model-argument': 'off',
|
||||
'no-case-declarations': 'off',
|
||||
// 'no-console': 'error',
|
||||
'no-redeclare': 'off',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -10,27 +10,28 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.1.0",
|
||||
"asciinema-player": "^3.5.0",
|
||||
"axios": "^1.5.0",
|
||||
"asciinema-player": "^3.6.2",
|
||||
"axios": "^1.6.2",
|
||||
"clipboard": "^2.0.11",
|
||||
"countup.js": "^2.7.0",
|
||||
"cropperjs": "^1.5.11",
|
||||
"echarts": "^5.4.0",
|
||||
"element-plus": "^2.3.12",
|
||||
"jsencrypt": "^3.3.1",
|
||||
"echarts": "^5.4.3",
|
||||
"element-plus": "^2.4.3",
|
||||
"jsencrypt": "^3.3.2",
|
||||
"lodash": "^4.17.21",
|
||||
"mitt": "^3.0.1",
|
||||
"monaco-editor": "^0.43.0",
|
||||
"monaco-editor": "^0.44.0",
|
||||
"monaco-sql-languages": "^0.11.0",
|
||||
"monaco-themes": "^0.4.4",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^2.1.6",
|
||||
"qrcode.vue": "^3.4.0",
|
||||
"pinia": "^2.1.7",
|
||||
"qrcode.vue": "^3.4.1",
|
||||
"screenfull": "^6.0.2",
|
||||
"sortablejs": "^1.13.0",
|
||||
"sql-formatter": "^12.1.2",
|
||||
"vue": "^3.3.4",
|
||||
"vue-clipboard3": "^1.0.1",
|
||||
"vue-router": "^4.2.4",
|
||||
"sortablejs": "^1.15.0",
|
||||
"sql-formatter": "^14.0.0",
|
||||
"uuid": "^9.0.1",
|
||||
"vue": "^3.3.10",
|
||||
"vue-router": "^4.2.5",
|
||||
"xterm": "^5.3.0",
|
||||
"xterm-addon-fit": "^0.8.0",
|
||||
"xterm-addon-search": "^0.13.0",
|
||||
@@ -40,20 +41,19 @@
|
||||
"@types/lodash": "^4.14.178",
|
||||
"@types/node": "^15.6.0",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/sortablejs": "^1.10.6",
|
||||
"@typescript-eslint/eslint-plugin": "^4.23.0",
|
||||
"@typescript-eslint/parser": "^4.23.0",
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"@vue/compiler-sfc": "^3.0.11",
|
||||
"dotenv": "^10.0.0",
|
||||
"@types/sortablejs": "^1.15.3",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
||||
"@typescript-eslint/parser": "^6.7.4",
|
||||
"@vitejs/plugin-vue": "^4.4.0",
|
||||
"@vue/compiler-sfc": "^3.3.4",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint": "^8.35.0",
|
||||
"eslint-plugin-vue": "^8.2.0",
|
||||
"prettier": "^2.3.0",
|
||||
"sass": "^1.62.0",
|
||||
"sass-loader": "^13.2.0",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^4.4.9",
|
||||
"vue-eslint-parser": "^9.1.1"
|
||||
"eslint-plugin-vue": "^9.17.0",
|
||||
"prettier": "^3.0.3",
|
||||
"sass": "^1.69.0",
|
||||
"typescript": "^5.3.2",
|
||||
"vite": "^5.0.5",
|
||||
"vue-eslint-parser": "^9.3.1"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
|
||||
@@ -1,20 +1,33 @@
|
||||
<template>
|
||||
<router-view v-show="themeConfig.lockScreenTime !== 0" />
|
||||
<LockScreen v-if="themeConfig.isLockScreen" />
|
||||
<Setings ref="setingsRef" v-show="themeConfig.lockScreenTime !== 0" />
|
||||
<div class="h100">
|
||||
<el-watermark
|
||||
:zIndex="10000000"
|
||||
:width="210"
|
||||
v-if="themeConfig.isWatermark"
|
||||
:font="{ color: 'rgba(180, 180, 180, 0.5)' }"
|
||||
: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" />
|
||||
|
||||
<LockScreen v-if="themeConfig.isLockScreen" />
|
||||
<Setings ref="setingsRef" v-show="themeConfig.lockScreenTime !== 0" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="app">
|
||||
import { ref, onBeforeMount, onMounted, onUnmounted, nextTick, watch } from 'vue';
|
||||
import { ref, onMounted, onUnmounted, nextTick, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
// import { useTagsViewRoutes } from '@/store/tagsViewRoutes';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
import { getLocal } from '@/common/utils/storage';
|
||||
import LockScreen from '@/views/layout/lockScreen/index.vue';
|
||||
import Setings from '@/views/layout/navBars/breadcrumb/setings.vue';
|
||||
import Watermark from '@/common/utils/wartermark';
|
||||
import LockScreen from '@/layout/lockScreen/index.vue';
|
||||
import Setings from '@/layout/navBars/breadcrumb/setings.vue';
|
||||
import mittBus from '@/common/utils/mitt';
|
||||
import { getThemeConfig } from './common/utils/storage';
|
||||
import { useWatermark } from '@/common/sysconfig';
|
||||
|
||||
const setingsRef = ref();
|
||||
const route = useRoute();
|
||||
@@ -27,13 +40,11 @@ const openSetingsDrawer = () => {
|
||||
setingsRef.value.openDrawer();
|
||||
};
|
||||
|
||||
// 设置初始化,防止刷新时恢复默认
|
||||
onBeforeMount(() => {
|
||||
// 设置批量第三方 icon 图标
|
||||
// setIntroduction.cssCdn();
|
||||
// // 设置批量第三方 js
|
||||
// setIntroduction.jsCdn();
|
||||
});
|
||||
const prefers = matchMedia('(prefers-color-scheme: dark)');
|
||||
const switchDarkFollowOS = () => {
|
||||
// 跟随系统主题
|
||||
themeConfigStores.switchDark(prefers.matches);
|
||||
};
|
||||
|
||||
// 页面加载时
|
||||
onMounted(() => {
|
||||
@@ -42,16 +53,60 @@ onMounted(() => {
|
||||
mittBus.on('openSetingsDrawer', () => {
|
||||
openSetingsDrawer();
|
||||
});
|
||||
|
||||
// 获取缓存中的布局配置
|
||||
if (getLocal('themeConfig')) {
|
||||
themeConfigStores.setThemeConfig({ themeConfig: getLocal('themeConfig') });
|
||||
const tc = getThemeConfig();
|
||||
if (tc) {
|
||||
themeConfigStores.setThemeConfig({ themeConfig: tc });
|
||||
document.documentElement.style.cssText = getLocal('themeConfigStyle');
|
||||
}
|
||||
switchDarkFollowOS();
|
||||
|
||||
// 是否开启水印
|
||||
useWatermark().then((res) => {
|
||||
themeConfigStores.setWatermarkConfig(res);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 监听 themeConfig isWartermark配置文件的变化
|
||||
watch(
|
||||
() => themeConfig.value.isWatermark,
|
||||
(val) => {
|
||||
if (val) {
|
||||
setTimeout(() => {
|
||||
setWatermarkContent();
|
||||
refreshWatermarkTime();
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const setWatermarkContent = () => {
|
||||
themeConfigStores.setWatermarkUser();
|
||||
themeConfigStores.setWatermarkNowTime();
|
||||
};
|
||||
|
||||
let refreshWatermarkTimeInterval: any = null;
|
||||
/**
|
||||
* 刷新水印时间
|
||||
*/
|
||||
const refreshWatermarkTime = () => {
|
||||
if (refreshWatermarkTimeInterval) {
|
||||
clearInterval(refreshWatermarkTimeInterval);
|
||||
}
|
||||
refreshWatermarkTimeInterval = setInterval(() => {
|
||||
if (themeConfig.value.isWatermark) {
|
||||
themeConfigStores.setWatermarkNowTime();
|
||||
} else {
|
||||
clearInterval(refreshWatermarkTimeInterval);
|
||||
}
|
||||
}, 60000);
|
||||
};
|
||||
|
||||
// 页面销毁时,关闭监听布局配置
|
||||
onUnmounted(() => {
|
||||
clearInterval(refreshWatermarkTimeInterval);
|
||||
mittBus.off('openSetingsDrawer', () => {});
|
||||
});
|
||||
|
||||
@@ -60,8 +115,6 @@ watch(
|
||||
() => route.path,
|
||||
() => {
|
||||
nextTick(() => {
|
||||
// 路由变化更新水印
|
||||
Watermark.use();
|
||||
document.title = `${route.meta.title} - ${themeConfig.value.globalTitle}` || themeConfig.value.globalTitle;
|
||||
});
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 39 KiB |
1
mayfly_go_web/src/assets/image/401.svg
Normal file
|
After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 36 KiB |
1
mayfly_go_web/src/assets/image/404.svg
Normal file
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 458 KiB |
1
mayfly_go_web/src/assets/image/login-bg-main.svg
Normal file
|
After Width: | Height: | Size: 13 KiB |
19
mayfly_go_web/src/assets/image/login-bg-split.svg
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
9
mayfly_go_web/src/common/commonEnum.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import EnumValue from './Enum';
|
||||
|
||||
// 标签关联的资源类型
|
||||
export const TagResourceTypeEnum = {
|
||||
Machine: EnumValue.of(1, '机器'),
|
||||
Db: EnumValue.of(2, '数据库'),
|
||||
Redis: EnumValue.of(3, 'redis'),
|
||||
Mongo: EnumValue.of(4, 'mongo'),
|
||||
};
|
||||
@@ -3,6 +3,10 @@ function getBaseApiUrl() {
|
||||
if (path == '/') {
|
||||
return window.location.host;
|
||||
}
|
||||
if (path.endsWith('/')) {
|
||||
// 去除最后一个/
|
||||
return window.location.host + path.replace(/\/$/, '');
|
||||
}
|
||||
return window.location.host + path;
|
||||
}
|
||||
|
||||
@@ -11,7 +15,7 @@ const config = {
|
||||
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
|
||||
|
||||
// 系统版本
|
||||
version: 'v1.5.2',
|
||||
version: 'v1.6.0',
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import router from '../router';
|
||||
import Axios from 'axios';
|
||||
import config from './config';
|
||||
import { getSession } from './utils/storage';
|
||||
import { getClientId, getToken } from './utils/storage';
|
||||
import { templateResolve } from './utils/string';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
@@ -29,7 +29,7 @@ enum ResultEnum {
|
||||
}
|
||||
|
||||
const baseUrl: string = config.baseApiUrl;
|
||||
const baseWsUrl: string = config.baseWsUrl;
|
||||
// const baseWsUrl: string = config.baseWsUrl;
|
||||
|
||||
/**
|
||||
* 通知错误消息
|
||||
@@ -43,17 +43,18 @@ function notifyErrorMsg(msg: string) {
|
||||
// create an axios instance
|
||||
const service = Axios.create({
|
||||
baseURL: baseUrl, // url = base url + request url
|
||||
timeout: 20000, // request timeout
|
||||
timeout: 60000, // request timeout
|
||||
});
|
||||
|
||||
// request interceptor
|
||||
service.interceptors.request.use(
|
||||
(config: any) => {
|
||||
// do something before request is sent
|
||||
const token = getSession('token');
|
||||
const token = getToken();
|
||||
if (token) {
|
||||
// 设置token
|
||||
config.headers['Authorization'] = token;
|
||||
config.headers['ClientId'] = getClientId();
|
||||
}
|
||||
return config;
|
||||
},
|
||||
@@ -143,8 +144,8 @@ function request(method: string, url: string, params: any = null, headers: any =
|
||||
.request(query)
|
||||
.then((res) => res)
|
||||
.catch((e) => {
|
||||
// 如果返回的code不为成功,则会返回对应的错误msg,则直接统一通知即可
|
||||
if (e.msg) {
|
||||
// 如果返回的code不为成功,则会返回对应的错误msg,则直接统一通知即可。忽略登录超时或没有权限的提示(直接跳转至401页面)
|
||||
if (e.msg && e?.code != ResultEnum.NO_PERMISSION) {
|
||||
notifyErrorMsg(e.msg);
|
||||
}
|
||||
return Promise.reject(e);
|
||||
@@ -176,7 +177,12 @@ function del(url: string, params: any = null, headers: any = null, options: any
|
||||
|
||||
function getApiUrl(url: string) {
|
||||
// 只是返回api地址而不做请求,用在上传组件之类的
|
||||
return baseUrl + url + '?token=' + getSession('token');
|
||||
return baseUrl + url + '?' + joinClientParams();
|
||||
}
|
||||
|
||||
// 组装客户端参数,包括 token 和 clientId
|
||||
export function joinClientParams(): string {
|
||||
return `token=${getToken()}&clientId=${getClientId()}`;
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import Config from './config';
|
||||
import { ElNotification } from 'element-plus';
|
||||
import SocketBuilder from './SocketBuilder';
|
||||
import { getSession } from '@/common/utils/storage';
|
||||
|
||||
export default {
|
||||
/**
|
||||
* 全局系统消息websocket
|
||||
*/
|
||||
sysMsgSocket() {
|
||||
const token = getSession('token');
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
return SocketBuilder.builder(`${Config.baseWsUrl}/sysmsg?token=${token}`)
|
||||
.message((event: { data: string }) => {
|
||||
const message = JSON.parse(event.data);
|
||||
let mtype: string;
|
||||
switch (message.type) {
|
||||
case 0:
|
||||
mtype = 'error';
|
||||
break;
|
||||
case 2:
|
||||
mtype = 'info';
|
||||
break;
|
||||
case 1:
|
||||
mtype = 'success';
|
||||
break;
|
||||
default:
|
||||
mtype = 'info';
|
||||
}
|
||||
if (mtype == undefined) {
|
||||
return;
|
||||
}
|
||||
ElNotification({
|
||||
duration: 0,
|
||||
title: message.title,
|
||||
message: message.msg,
|
||||
type: mtype as any,
|
||||
});
|
||||
})
|
||||
.open((event: any) => console.log(event))
|
||||
.build();
|
||||
},
|
||||
};
|
||||
@@ -3,7 +3,7 @@ import openApi from './openApi';
|
||||
// 登录是否使用验证码配置key
|
||||
const AccountLoginSecurity = 'AccountLoginSecurity';
|
||||
const UseLoginCaptchaConfigKey = 'UseLoginCaptcha';
|
||||
const UseWartermarkConfigKey = 'UseWartermark';
|
||||
const UseWatermarkConfigKey = 'UseWatermark';
|
||||
|
||||
/**
|
||||
* 获取系统配置值
|
||||
@@ -53,12 +53,26 @@ export async function useLoginCaptcha(): Promise<boolean> {
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否启用水印
|
||||
* 是否启用水印信息配置
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
export async function useWartermark(): Promise<boolean> {
|
||||
return await getBoolConfigValue(UseWartermarkConfigKey, true);
|
||||
export async function useWatermark(): Promise<any> {
|
||||
const value = await getConfigValue(UseWatermarkConfigKey);
|
||||
const defaultValue = {
|
||||
isUse: true,
|
||||
};
|
||||
if (!value) {
|
||||
return defaultValue;
|
||||
}
|
||||
try {
|
||||
const jsonValue = JSON.parse(value);
|
||||
// 将字符串转为bool
|
||||
jsonValue.isUse = convertBool(jsonValue.isUse, true);
|
||||
return jsonValue;
|
||||
} catch (e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
function convertBool(value: string, defaultValue: boolean) {
|
||||
@@ -77,4 +91,3 @@ export async function getLdapEnabled(): Promise<any> {
|
||||
const value = await openApi.getLdapEnabled();
|
||||
return convertBool(value, false);
|
||||
}
|
||||
|
||||
|
||||
100
mayfly_go_web/src/common/syssocket.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import Config from './config';
|
||||
import { ElNotification } from 'element-plus';
|
||||
import SocketBuilder from './SocketBuilder';
|
||||
import { getToken } from '@/common/utils/storage';
|
||||
|
||||
import { joinClientParams } from './request';
|
||||
|
||||
class SysSocket {
|
||||
/**
|
||||
* socket连接
|
||||
*/
|
||||
socket: any;
|
||||
|
||||
/**
|
||||
* key -> 消息类别,value -> 消息对应的处理器函数
|
||||
*/
|
||||
categoryHandlers: Map<string, any> = new Map();
|
||||
|
||||
/**
|
||||
* 消息类型
|
||||
*/
|
||||
messageTypes = {
|
||||
0: 'error',
|
||||
1: 'success',
|
||||
2: 'info',
|
||||
};
|
||||
|
||||
/**
|
||||
* 初始化全局系统消息websocket
|
||||
*/
|
||||
init() {
|
||||
// 存在则不需要重新建立连接
|
||||
if (this.socket) {
|
||||
return;
|
||||
}
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
console.log('init system ws');
|
||||
const sysMsgUrl = `${Config.baseWsUrl}/sysmsg?${joinClientParams()}`;
|
||||
this.socket = SocketBuilder.builder(sysMsgUrl)
|
||||
.message((event: { data: string }) => {
|
||||
const message = JSON.parse(event.data);
|
||||
// 存在消息类别对应的处理器,则进行处理,否则进行默认通知处理
|
||||
const handler = this.categoryHandlers.get(message.category);
|
||||
if (handler) {
|
||||
handler(message);
|
||||
return;
|
||||
}
|
||||
|
||||
const type = this.getMsgType(message.type);
|
||||
ElNotification({
|
||||
duration: 0,
|
||||
title: message.title,
|
||||
message: message.msg,
|
||||
type: type,
|
||||
});
|
||||
})
|
||||
.open((event: any) => console.log(event))
|
||||
.close(() => {
|
||||
console.log('close sys socket');
|
||||
this.socket = null;
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
destory() {
|
||||
this.socket.close();
|
||||
this.socket = null;
|
||||
this.categoryHandlers.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册消息处理函数
|
||||
*
|
||||
* @param category 消息类别
|
||||
* @param handlerFunc 消息处理函数
|
||||
*/
|
||||
registerMsgHandler(category: any, handlerFunc: any) {
|
||||
this.init();
|
||||
if (this.categoryHandlers.has(category)) {
|
||||
console.log(`${category}该类别消息处理器已存在...`);
|
||||
return;
|
||||
}
|
||||
if (typeof handlerFunc != 'function') {
|
||||
throw new Error('message handler需为函数');
|
||||
}
|
||||
this.categoryHandlers.set(category, handlerFunc);
|
||||
}
|
||||
|
||||
getMsgType(msgType: any) {
|
||||
return this.messageTypes[msgType];
|
||||
}
|
||||
}
|
||||
|
||||
// 全局系统消息websocket;
|
||||
const sysSocket = new SysSocket();
|
||||
|
||||
export default sysSocket;
|
||||
17
mayfly_go_web/src/common/use.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
const vw = ref(document.documentElement.clientWidth);
|
||||
const vh = ref(document.documentElement.clientHeight);
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
vw.value = document.documentElement.clientWidth;
|
||||
vh.value = document.documentElement.clientHeight;
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取视图宽高
|
||||
* @returns 视图宽高
|
||||
*/
|
||||
export function useViewport() {
|
||||
return { vw, vh };
|
||||
}
|
||||
@@ -29,15 +29,19 @@ export function exportCsv(filename: string, columns: string[], datas: []) {
|
||||
cvsData.push(dataValueArr);
|
||||
}
|
||||
const csvString = cvsData.map((e) => e.join(',')).join('\n');
|
||||
exportFile(`${filename}.csv`, csvString);
|
||||
}
|
||||
|
||||
export function exportFile(filename: string, content: string) {
|
||||
// 导出
|
||||
let link = document.createElement('a');
|
||||
let exportContent = '\uFEFF';
|
||||
let blob = new Blob([exportContent + csvString], {
|
||||
let blob = new Blob([exportContent + content], {
|
||||
type: 'text/plain;charset=utf-8',
|
||||
});
|
||||
link.id = 'download-csv';
|
||||
link.id = 'download-file';
|
||||
link.setAttribute('href', URL.createObjectURL(blob));
|
||||
link.setAttribute('download', `${filename}.csv`);
|
||||
link.setAttribute('download', `${filename}`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
}
|
||||
|
||||
@@ -3,21 +3,16 @@
|
||||
* @param size byte size
|
||||
* @returns
|
||||
*/
|
||||
export function formatByteSize(size: any) {
|
||||
const value = Number(size);
|
||||
if (size && !isNaN(value)) {
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB', 'BB'];
|
||||
let index = 0;
|
||||
let k = value;
|
||||
if (value >= 1024) {
|
||||
while (k > 1024) {
|
||||
k = k / 1024;
|
||||
index++;
|
||||
}
|
||||
}
|
||||
return `${k.toFixed(2)}${units[index]}`;
|
||||
export function formatByteSize(size: number, fixed = 2) {
|
||||
if (size === 0) {
|
||||
return '0B';
|
||||
}
|
||||
return '-';
|
||||
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
const base = 1024;
|
||||
const exponent = Math.floor(Math.log(size) / Math.log(base));
|
||||
|
||||
return parseFloat((size / Math.pow(base, exponent)).toFixed(fixed)) + units[exponent];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,46 +1,46 @@
|
||||
import { nextTick } from 'vue';
|
||||
import loadingCss from '@/theme/loading.scss?inline';
|
||||
import '@/theme/loading.scss';
|
||||
|
||||
// 定义方法
|
||||
/**
|
||||
* 页面全局 Loading
|
||||
* @method start 创建 loading
|
||||
* @method done 移除 loading
|
||||
*/
|
||||
export const NextLoading = {
|
||||
// 载入 css
|
||||
setCss: () => {
|
||||
let link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = loadingCss;
|
||||
link.crossOrigin = 'anonymous';
|
||||
document.getElementsByTagName('head')[0].appendChild(link);
|
||||
},
|
||||
// 创建 loading
|
||||
start: () => {
|
||||
const bodys: any = document.body;
|
||||
const div = document.createElement('div');
|
||||
const bodys: Element = document.body;
|
||||
const div = <HTMLElement>document.createElement('div');
|
||||
div.setAttribute('class', 'loading-next');
|
||||
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 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>
|
||||
`;
|
||||
div.innerHTML = htmls;
|
||||
bodys.insertBefore(div, bodys.childNodes[0]);
|
||||
},
|
||||
// 移除 loading
|
||||
done: () => {
|
||||
done: (time: number = 1000) => {
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
const el = document.querySelector('.loading-next');
|
||||
el && el.parentNode?.removeChild(el);
|
||||
}, 1000);
|
||||
const el = <HTMLElement>document.querySelector('.loading-next');
|
||||
el?.parentNode?.removeChild(el);
|
||||
}, time);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export function sleep(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
@@ -1,17 +1,90 @@
|
||||
import { randomUuid } from './string';
|
||||
|
||||
const TokenKey = 'm-token';
|
||||
const UserKey = 'm-user';
|
||||
const TagViewsKey = 'm-tagViews';
|
||||
const ClientIdKey = 'm-clientId';
|
||||
|
||||
// 获取请求token
|
||||
export function getToken(): string {
|
||||
return getLocal(TokenKey);
|
||||
}
|
||||
|
||||
// 保存用户访问token
|
||||
export function saveToken(token: string) {
|
||||
setLocal(TokenKey, token);
|
||||
}
|
||||
|
||||
// 获取登录用户基础信息
|
||||
export function getUser() {
|
||||
return getLocal(UserKey);
|
||||
}
|
||||
|
||||
// 保存用户信息
|
||||
export function saveUser(userinfo: any) {
|
||||
setLocal(UserKey, userinfo);
|
||||
}
|
||||
|
||||
export function saveThemeConfig(themeConfig: any) {
|
||||
setLocal('themeConfig', themeConfig);
|
||||
}
|
||||
|
||||
export function getThemeConfig() {
|
||||
return getLocal('themeConfig');
|
||||
}
|
||||
|
||||
// 清除用户相关的用户信息
|
||||
export function clearUser() {
|
||||
removeLocal(TokenKey);
|
||||
removeLocal(UserKey);
|
||||
}
|
||||
|
||||
export function getTagViews() {
|
||||
return getSession(TagViewsKey);
|
||||
}
|
||||
|
||||
export function setTagViews(tagViews: Array<object>) {
|
||||
setSession(TagViewsKey, tagViews);
|
||||
}
|
||||
|
||||
export function removeTagViews() {
|
||||
removeSession(TagViewsKey);
|
||||
}
|
||||
|
||||
// 获取客户端UUID
|
||||
export function getClientId(): string {
|
||||
let uuid = getSession(ClientIdKey);
|
||||
if (uuid == null) {
|
||||
uuid = randomUuid();
|
||||
setSession(ClientIdKey, uuid);
|
||||
}
|
||||
return uuid;
|
||||
}
|
||||
|
||||
// 1. localStorage
|
||||
// 设置永久缓存
|
||||
export function setLocal(key: string, val: any) {
|
||||
window.localStorage.setItem(key, JSON.stringify(val));
|
||||
if (typeof val == 'object') {
|
||||
val = JSON.stringify(val);
|
||||
}
|
||||
window.localStorage.setItem(key, val);
|
||||
}
|
||||
|
||||
// 获取永久缓存
|
||||
export function getLocal(key: string) {
|
||||
let json: any = window.localStorage.getItem(key);
|
||||
return JSON.parse(json);
|
||||
let val: any = window.localStorage.getItem(key);
|
||||
try {
|
||||
return JSON.parse(val);
|
||||
} catch (e) {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
|
||||
// 移除永久缓存
|
||||
export function removeLocal(key: string) {
|
||||
window.localStorage.removeItem(key);
|
||||
}
|
||||
|
||||
// 移除全部永久缓存
|
||||
export function clearLocal() {
|
||||
window.localStorage.clear();
|
||||
@@ -20,35 +93,29 @@ export function clearLocal() {
|
||||
// 2. sessionStorage
|
||||
// 设置临时缓存
|
||||
export function setSession(key: string, val: any) {
|
||||
window.sessionStorage.setItem(key, JSON.stringify(val));
|
||||
if (typeof val == 'object') {
|
||||
val = JSON.stringify(val);
|
||||
}
|
||||
window.sessionStorage.setItem(key, val);
|
||||
}
|
||||
|
||||
// 获取临时缓存
|
||||
export function getSession(key: string) {
|
||||
let json: any = window.sessionStorage.getItem(key);
|
||||
return JSON.parse(json);
|
||||
let val: any = window.sessionStorage.getItem(key);
|
||||
try {
|
||||
return JSON.parse(val);
|
||||
} catch (e) {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
|
||||
// 移除临时缓存
|
||||
export function removeSession(key: string) {
|
||||
window.sessionStorage.removeItem(key);
|
||||
}
|
||||
|
||||
// 移除全部临时缓存
|
||||
export function clearSession() {
|
||||
clearUser();
|
||||
window.sessionStorage.clear();
|
||||
}
|
||||
|
||||
export function getUserInfo4Session() {
|
||||
return getSession('userInfo');
|
||||
}
|
||||
|
||||
export function setUserInfo2Session(userinfo: any) {
|
||||
setSession('userInfo', userinfo);
|
||||
}
|
||||
|
||||
// 获取是否开启水印
|
||||
export function getUseWatermark4Session() {
|
||||
return getSession('useWatermark');
|
||||
}
|
||||
|
||||
export function setUseWatermark2Session(useWatermark: boolean) {
|
||||
setSession('useWatermark', useWatermark);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import { v1 as uuidv1 } from 'uuid';
|
||||
import Clipboard from 'clipboard';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
/**
|
||||
* 模板字符串解析,如:template = 'hahaha{name}_{id}' ,param = {name: 'hh', id: 1}
|
||||
* 解析后为 hahahahh_1
|
||||
@@ -129,3 +133,49 @@ export function getContentWidth(content: any): number {
|
||||
// }
|
||||
return flexWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns uuid
|
||||
*/
|
||||
export function randomUuid() {
|
||||
return uuidv1();
|
||||
}
|
||||
|
||||
/**
|
||||
* 拷贝文本至剪贴板
|
||||
* @param txt 需要拷贝到剪贴板的文本
|
||||
* @param selector click事件对应的元素selector,默认为 #copyValue
|
||||
* @returns
|
||||
*/
|
||||
export async function copyToClipboard(txt: string, selector: string = '#copyValue') {
|
||||
// navigator clipboard 需要https等安全上下文
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
// navigator clipboard 向剪贴板写文本
|
||||
try {
|
||||
// 拷贝单元格数据
|
||||
await navigator.clipboard.writeText(txt);
|
||||
ElMessage.success('复制成功');
|
||||
} catch (e: any) {
|
||||
ElMessage.error('复制失败');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let clipboard = new Clipboard(selector, {
|
||||
text: function () {
|
||||
return txt;
|
||||
},
|
||||
});
|
||||
clipboard.on('success', () => {
|
||||
ElMessage.success('复制成功');
|
||||
// 释放内存
|
||||
clipboard.destroy();
|
||||
});
|
||||
clipboard.on('error', () => {
|
||||
// 不支持复制
|
||||
ElMessage.error('该浏览器不支持自动复制');
|
||||
// 释放内存
|
||||
clipboard.destroy();
|
||||
});
|
||||
}
|
||||
|
||||
13
mayfly_go_web/src/common/utils/url.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
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,65 +0,0 @@
|
||||
import { getUseWatermark4Session, getUserInfo4Session } from '@/common/utils/storage';
|
||||
import { dateFormat2 } from '@/common/utils/date';
|
||||
|
||||
// 页面添加水印效果
|
||||
const setWatermark = (str: any) => {
|
||||
const id = '1.23452384164.123412416';
|
||||
if (document.getElementById(id) !== null) document.body.removeChild(document.getElementById(id) as any);
|
||||
const can = document.createElement('canvas');
|
||||
can.width = 400;
|
||||
can.height = 250;
|
||||
const cans: any = can.getContext('2d');
|
||||
cans.rotate((-20 * Math.PI) / 180);
|
||||
cans.font = '14px Vedana';
|
||||
cans.fillStyle = 'rgba(200, 200, 200, 0.35)';
|
||||
cans.textAlign = 'left';
|
||||
cans.textBaseline = 'Middle';
|
||||
// cans.fillText('mayfly go', can.width / 4, can.height )
|
||||
cans.fillText(str, can.width / 8, can.height / 2);
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.id = id;
|
||||
div.style.pointerEvents = 'none';
|
||||
div.style.top = '30px';
|
||||
div.style.left = '0px';
|
||||
div.style.position = 'fixed';
|
||||
div.style.zIndex = '10000000';
|
||||
div.style.width = document.documentElement.clientWidth + 'px';
|
||||
div.style.height = document.documentElement.clientHeight + 'px';
|
||||
div.style.background = `url(${can.toDataURL('image/png')}) left top repeat`;
|
||||
document.body.appendChild(div);
|
||||
return id;
|
||||
};
|
||||
|
||||
function set(str: any) {
|
||||
let id = setWatermark(str);
|
||||
if (document.getElementById(id) === null) id = setWatermark(str);
|
||||
}
|
||||
|
||||
function del() {
|
||||
let id = '1.23452384164.123412416';
|
||||
if (document.getElementById(id) !== null) document.body.removeChild(document.getElementById(id) as any);
|
||||
}
|
||||
|
||||
const watermark = {
|
||||
use: () => {
|
||||
setTimeout(() => {
|
||||
const userinfo = getUserInfo4Session();
|
||||
if (userinfo && getUseWatermark4Session()) {
|
||||
set(`${userinfo.username} ${dateFormat2('yyyy-MM-dd HH:mm:ss', new Date())}`);
|
||||
} else {
|
||||
del();
|
||||
}
|
||||
}, 1500);
|
||||
},
|
||||
// 设置水印
|
||||
set: (str: any) => {
|
||||
set(str);
|
||||
},
|
||||
// 删除水印
|
||||
del: () => {
|
||||
del();
|
||||
},
|
||||
};
|
||||
|
||||
export default watermark;
|
||||
@@ -6,6 +6,9 @@ import { useUserInfo } from '@/store/userInfo';
|
||||
* @returns
|
||||
*/
|
||||
export function hasPerm(code: string) {
|
||||
if (!code) {
|
||||
return true;
|
||||
}
|
||||
return useUserInfo().userInfo.permissions.some((v: any) => v === code);
|
||||
}
|
||||
|
||||
|
||||
59
mayfly_go_web/src/components/contextmenu/index.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import Contextmenu from './index.vue';
|
||||
|
||||
class ContextmenuItem {
|
||||
clickId: any;
|
||||
|
||||
txt: string;
|
||||
|
||||
icon: string;
|
||||
|
||||
affix: boolean;
|
||||
|
||||
permission: string;
|
||||
|
||||
/**
|
||||
* 是否隐藏回调函数
|
||||
*/
|
||||
hideFunc: (data: any) => boolean;
|
||||
|
||||
onClickFunc: (data: any) => void;
|
||||
|
||||
constructor(clickId: any, txt: string) {
|
||||
this.clickId = clickId;
|
||||
this.txt = txt;
|
||||
}
|
||||
|
||||
withIcon(icon: string) {
|
||||
this.icon = icon;
|
||||
return this;
|
||||
}
|
||||
|
||||
withPermission(permission: string) {
|
||||
this.permission = permission;
|
||||
return this;
|
||||
}
|
||||
|
||||
withHideFunc(func: (data: any) => boolean) {
|
||||
this.hideFunc = func;
|
||||
return this;
|
||||
}
|
||||
|
||||
withOnClick(func: (data: any) => void) {
|
||||
this.onClickFunc = func;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否隐藏
|
||||
* @param data 点击数据项
|
||||
* @returns
|
||||
*/
|
||||
isHide(data: any) {
|
||||
if (this.hideFunc) {
|
||||
return this.hideFunc(data);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export { Contextmenu, ContextmenuItem };
|
||||
@@ -1,36 +1,41 @@
|
||||
<template>
|
||||
<transition name="el-zoom-in-center">
|
||||
<transition @enter="onEnter" name="el-zoom-in-center">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="el-dropdown__popper el-popper is-light is-pure custom-contextmenu"
|
||||
role="tooltip"
|
||||
data-popper-placement="bottom"
|
||||
:style="`top: ${dropdowns.y + 5}px;left: ${dropdowns.x}px;`"
|
||||
:style="`top: ${state.dropdown.y + 5}px;left: ${state.dropdown.x}px;`"
|
||||
:key="Math.random()"
|
||||
v-show="state.isShow"
|
||||
v-show="state.isShow && !allHide"
|
||||
>
|
||||
<ul class="el-dropdown-menu">
|
||||
<template v-for="(v, k) in state.dropdownList">
|
||||
<li
|
||||
:id="v.clickId"
|
||||
v-auth="v.permission"
|
||||
class="el-dropdown-menu__item"
|
||||
aria-disabled="false"
|
||||
tabindex="-1"
|
||||
:key="k"
|
||||
v-if="!v.affix"
|
||||
@click="onCurrentContextmenuClick(v.contextMenuClickId)"
|
||||
v-if="!v.affix && !v.isHide(state.item)"
|
||||
@click="onCurrentContextmenuClick(v)"
|
||||
>
|
||||
<SvgIcon :name="v.icon" />
|
||||
<span>{{ v.txt }}</span>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
<div class="el-popper__arrow" :style="{ left: `${state.arrowLeft}px` }"></div>
|
||||
<div v-if="state.arrowLeft > 0" class="el-popper__arrow" :style="{ left: `${state.arrowLeft}px` }"></div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="layoutTagsViewContextmenu">
|
||||
import { computed, reactive, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { ContextmenuItem } from './index';
|
||||
import { useViewport } from '@/common/use';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
|
||||
// 定义父组件传过来的值
|
||||
const props = defineProps({
|
||||
@@ -44,38 +49,82 @@ const props = defineProps({
|
||||
},
|
||||
},
|
||||
items: {
|
||||
type: Array,
|
||||
default: [],
|
||||
type: Array<ContextmenuItem>,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
// 定义子组件向父组件传值/事件
|
||||
const emit = defineEmits(['currentContextmenuClick']);
|
||||
|
||||
const { vw, vh } = useViewport();
|
||||
|
||||
// 定义变量内容
|
||||
const state = reactive({
|
||||
isShow: false,
|
||||
dropdownList: [{ contextMenuClickId: 0, txt: '刷新', affix: false, icon: 'RefreshRight' }],
|
||||
dropdownList: [] as ContextmenuItem[],
|
||||
item: {} as any,
|
||||
arrowLeft: 10,
|
||||
dropdown: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
});
|
||||
|
||||
// 父级传过来的坐标 x,y 值
|
||||
const dropdowns = computed(() => {
|
||||
// 117 为 `Dropdown 下拉菜单` 的宽度
|
||||
if (props.dropdown.x + 117 > document.documentElement.clientWidth) {
|
||||
return {
|
||||
x: document.documentElement.clientWidth - 117 - 5,
|
||||
y: props.dropdown.y,
|
||||
};
|
||||
} else {
|
||||
return props.dropdown;
|
||||
// 下拉菜单宽高
|
||||
let contextmenuWidth = 117;
|
||||
let contextmenuHeight = 117;
|
||||
// 下拉菜单元素
|
||||
let ele = null as any;
|
||||
|
||||
const onEnter = (el: any) => {
|
||||
if (ele || el.offsetHeight == 0) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
// 当前项菜单点击
|
||||
const onCurrentContextmenuClick = (contextMenuClickId: number) => {
|
||||
emit('currentContextmenuClick', { id: contextMenuClickId, item: state.item });
|
||||
|
||||
ele = el;
|
||||
contextmenuHeight = el.offsetHeight;
|
||||
contextmenuWidth = el.offsetWidth;
|
||||
setDropdowns(props.dropdown);
|
||||
};
|
||||
|
||||
const setDropdowns = (dropdown: any) => {
|
||||
let { x, y } = dropdown;
|
||||
|
||||
state.arrowLeft = 10;
|
||||
|
||||
// `Dropdown 下拉菜单` 的宽度
|
||||
if (x + contextmenuWidth > vw.value) {
|
||||
state.arrowLeft = contextmenuWidth - (vw.value - x);
|
||||
x = vw.value - contextmenuWidth - 5;
|
||||
}
|
||||
if (y + contextmenuHeight > vh.value) {
|
||||
y = vh.value - contextmenuHeight - 5;
|
||||
state.arrowLeft = 0;
|
||||
}
|
||||
|
||||
state.dropdown.x = x;
|
||||
state.dropdown.y = y;
|
||||
};
|
||||
|
||||
const allHide = computed(() => {
|
||||
for (let item of state.dropdownList) {
|
||||
if (!item.isHide(state.item)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// 当前项菜单点击
|
||||
const onCurrentContextmenuClick = (ci: ContextmenuItem) => {
|
||||
// 存在点击事件,则触发该事件函数
|
||||
if (ci.onClickFunc) {
|
||||
ci.onClickFunc(state.item);
|
||||
}
|
||||
emit('currentContextmenuClick', { id: ci.clickId, item: state.item });
|
||||
};
|
||||
|
||||
// 打开右键菜单:判断是否固定,固定则不显示关闭按钮
|
||||
const openContextmenu = (item: any) => {
|
||||
state.item = item;
|
||||
@@ -84,29 +133,34 @@ const openContextmenu = (item: any) => {
|
||||
state.isShow = true;
|
||||
}, 10);
|
||||
};
|
||||
|
||||
// 关闭右键菜单
|
||||
const closeContextmenu = () => {
|
||||
state.isShow = false;
|
||||
};
|
||||
|
||||
// 监听页面监听进行右键菜单的关闭
|
||||
onMounted(() => {
|
||||
document.body.addEventListener('click', closeContextmenu);
|
||||
state.dropdownList = props.items;
|
||||
});
|
||||
|
||||
// 页面卸载时,移除右键菜单监听事件
|
||||
onUnmounted(() => {
|
||||
document.body.removeEventListener('click', closeContextmenu);
|
||||
});
|
||||
// 监听下拉菜单位置
|
||||
|
||||
watch(
|
||||
() => props.dropdown,
|
||||
({ x }) => {
|
||||
if (x + 117 > document.documentElement.clientWidth) state.arrowLeft = 117 - (document.documentElement.clientWidth - x);
|
||||
else state.arrowLeft = 10;
|
||||
() => {
|
||||
// 元素置为空,重新在onEnter赋值元素,否则会造成堆栈溢出
|
||||
ele = null;
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.items,
|
||||
(x: any) => {
|
||||
@@ -140,3 +194,4 @@ defineExpose({
|
||||
}
|
||||
}
|
||||
</style>
|
||||
.
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, toRefs, reactive, onMounted, onBeforeUnmount } from 'vue';
|
||||
// import * as monaco from 'monaco-editor';
|
||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
// 相关语言
|
||||
import 'monaco-editor/esm/vs/basic-languages/shell/shell.contribution.js';
|
||||
@@ -133,7 +132,7 @@ const languageArr = [
|
||||
},
|
||||
];
|
||||
|
||||
const options = {
|
||||
const defaultOptions = {
|
||||
language: 'shell',
|
||||
theme: 'SolarizedLight',
|
||||
automaticLayout: true, //自适应宽高布局
|
||||
@@ -155,6 +154,20 @@ const options = {
|
||||
},
|
||||
};
|
||||
|
||||
const monacoTextarea: any = ref();
|
||||
|
||||
let monacoEditorIns: editor.IStandaloneCodeEditor = null as any;
|
||||
let completionItemProvider: any = null;
|
||||
|
||||
self.MonacoEnvironment = {
|
||||
getWorker(_: any, label: string) {
|
||||
if (label === 'json') {
|
||||
return new JsonWorker();
|
||||
}
|
||||
return new EditorWorker();
|
||||
},
|
||||
};
|
||||
|
||||
const state = reactive({
|
||||
languageMode: 'shell',
|
||||
});
|
||||
@@ -173,6 +186,7 @@ onBeforeUnmount(() => {
|
||||
monacoEditorIns.dispose();
|
||||
}
|
||||
if (completionItemProvider) {
|
||||
console.log('unmount=> dispose completion item provider');
|
||||
completionItemProvider.dispose();
|
||||
}
|
||||
});
|
||||
@@ -203,28 +217,14 @@ watch(
|
||||
}
|
||||
);
|
||||
|
||||
const monacoTextarea: any = ref(null);
|
||||
|
||||
let monacoEditorIns: editor.IStandaloneCodeEditor = null as any;
|
||||
let completionItemProvider: any = null;
|
||||
|
||||
self.MonacoEnvironment = {
|
||||
getWorker(_: any, label: string) {
|
||||
if (label === 'json') {
|
||||
return new JsonWorker();
|
||||
}
|
||||
return new EditorWorker();
|
||||
},
|
||||
};
|
||||
|
||||
const initMonacoEditorIns = () => {
|
||||
console.log('初始化monaco编辑器');
|
||||
// options参数参考 https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.IStandaloneEditorConstructionOptions.html#language
|
||||
// 初始化一些主题
|
||||
monaco.editor.defineTheme('SolarizedLight', SolarizedLight);
|
||||
options.language = state.languageMode;
|
||||
options.theme = themeConfig.value.editorTheme;
|
||||
monacoEditorIns = monaco.editor.create(monacoTextarea.value, Object.assign(options, props.options as any));
|
||||
defaultOptions.language = state.languageMode;
|
||||
defaultOptions.theme = themeConfig.value.editorTheme;
|
||||
monacoEditorIns = monaco.editor.create(monacoTextarea.value, Object.assign(defaultOptions, props.options as any));
|
||||
|
||||
// 监听内容改变,双向绑定
|
||||
monacoEditorIns.onDidChangeModelContent(() => {
|
||||
@@ -260,6 +260,7 @@ const setEditorValue = (value: any) => {
|
||||
*/
|
||||
const registerCompletionItemProvider = () => {
|
||||
if (completionItemProvider) {
|
||||
console.log('exist competion item provider, dispose now');
|
||||
completionItemProvider.dispose();
|
||||
}
|
||||
if (state.languageMode == 'shell') {
|
||||
@@ -299,7 +300,11 @@ const format = () => {
|
||||
monacoEditorIns.trigger('', 'editor.action.formatDocument', '');
|
||||
};
|
||||
|
||||
defineExpose({ format });
|
||||
const getEditor = () => {
|
||||
return monacoEditorIns;
|
||||
};
|
||||
|
||||
defineExpose({ getEditor, format });
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
|
||||
/**
|
||||
* key: language, value: CompletionItemProvider
|
||||
*/
|
||||
const completionItemProviders: Map<string, any> = new Map();
|
||||
|
||||
export function registerCompletionItemProvider(language: string, completionItemProvider: any, replace: boolean = true) {
|
||||
const exist = completionItemProviders.get(language);
|
||||
if (exist) {
|
||||
if (!replace) {
|
||||
return;
|
||||
}
|
||||
exist.dispose();
|
||||
}
|
||||
completionItemProviders.set(language, monaco.languages.registerCompletionItemProvider(language, completionItemProvider));
|
||||
}
|
||||
|
||||
export function dispposeCompletionItemProvider(language: string) {
|
||||
const exist = completionItemProviders.get(language);
|
||||
if (exist) {
|
||||
exist.dispose();
|
||||
completionItemProviders.delete(language);
|
||||
}
|
||||
}
|
||||
@@ -26,14 +26,14 @@
|
||||
>
|
||||
<!-- 这里只获取指定个数的筛选条件 -->
|
||||
<el-input
|
||||
v-model="queryForm[item.prop]"
|
||||
v-model="queryForm_[item.prop]"
|
||||
:placeholder="'输入' + item.label + '关键字'"
|
||||
clearable
|
||||
v-if="item.type == 'text'"
|
||||
></el-input>
|
||||
|
||||
<el-select-v2
|
||||
v-model="queryForm[item.prop]"
|
||||
v-model="queryForm_[item.prop]"
|
||||
:options="item.options"
|
||||
clearable
|
||||
:placeholder="'选择' + item.label + '关键字'"
|
||||
@@ -41,7 +41,7 @@
|
||||
/>
|
||||
|
||||
<el-date-picker
|
||||
v-model="queryForm[item.prop]"
|
||||
v-model="queryForm_[item.prop]"
|
||||
clearable
|
||||
type="datetimerange"
|
||||
format="YYYY-MM-DD hh:mm:ss"
|
||||
@@ -185,8 +185,10 @@
|
||||
import { toRefs, watch, reactive, onMounted } from 'vue';
|
||||
import { TableColumn, TableQuery } from './index';
|
||||
import EnumTag from '@/components/enumtag/EnumTag.vue';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
const emit = defineEmits(['update:queryForm', 'update:pageNum', 'update:pageSize', 'update:selectionData', 'pageChange'])
|
||||
const emit = defineEmits(['update:queryForm', 'update:pageNum', 'update:pageSize', 'update:selectionData', 'pageChange']);
|
||||
|
||||
const props = defineProps({
|
||||
size: {
|
||||
@@ -195,7 +197,7 @@ const props = defineProps({
|
||||
},
|
||||
inputWidth: {
|
||||
type: [Number, String],
|
||||
default: 0,
|
||||
default: '200px',
|
||||
},
|
||||
// 是否显示选择列
|
||||
showSelection: {
|
||||
@@ -204,7 +206,7 @@ const props = defineProps({
|
||||
},
|
||||
// 当前选择的数据
|
||||
selectionData: {
|
||||
type: Array<any>
|
||||
type: Array<any>,
|
||||
},
|
||||
// 列信息
|
||||
columns: {
|
||||
@@ -236,16 +238,18 @@ const props = defineProps({
|
||||
type: Array<TableQuery>,
|
||||
default: function () {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
},
|
||||
// 绑定的查询表单
|
||||
queryForm: {
|
||||
type: Object,
|
||||
default: function () {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const { themeConfig } = storeToRefs(useThemeConfig());
|
||||
|
||||
const state = reactive({
|
||||
pageSizes: [] as any, // 可选每页显示的数据量
|
||||
@@ -253,140 +257,153 @@ const state = reactive({
|
||||
pageNum: 1,
|
||||
isOpenMoreQuery: false,
|
||||
defaultQueryCount: 2, // 默认显示的查询参数个数,展开后每行显示查询条件个数为该值加1。第一行用最后一列来占用按钮
|
||||
queryForm: {} as any,
|
||||
queryForm_: {} as any,
|
||||
loadingData: false,
|
||||
// 输入框宽度
|
||||
inputWidth: "200px" as any,
|
||||
inputWidth_: '200px' as any,
|
||||
formatVal: '', // 格式化后的值
|
||||
tableMaxHeight: window.innerHeight - 240 + 'px',
|
||||
})
|
||||
});
|
||||
|
||||
const {
|
||||
pageSizes,
|
||||
isOpenMoreQuery,
|
||||
defaultQueryCount,
|
||||
queryForm,
|
||||
loadingData,
|
||||
inputWidth,
|
||||
formatVal,
|
||||
tableMaxHeight,
|
||||
} = toRefs(state)
|
||||
const { pageSizes, isOpenMoreQuery, defaultQueryCount, queryForm_, inputWidth_, formatVal, loadingData, tableMaxHeight } = toRefs(state);
|
||||
|
||||
watch(() => props.queryForm, (newValue: any) => {
|
||||
state.queryForm = newValue;
|
||||
})
|
||||
|
||||
watch(() => props.pageNum, (newValue: any) => {
|
||||
state.pageNum = newValue;
|
||||
})
|
||||
|
||||
watch(() => props.pageSize, (newValue: any) => {
|
||||
state.pageSize = newValue;
|
||||
})
|
||||
|
||||
watch(() => props.data, (newValue: any) => {
|
||||
if (newValue && newValue.length > 0) {
|
||||
props.columns.forEach(item => {
|
||||
if (item.autoWidth && item.show) {
|
||||
item.autoCalculateMinWidth(props.data);
|
||||
}
|
||||
})
|
||||
watch(
|
||||
() => props.queryForm,
|
||||
(newValue: any) => {
|
||||
state.queryForm_ = newValue;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.pageNum,
|
||||
(newValue: any) => {
|
||||
state.pageNum = newValue;
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.pageSize,
|
||||
(newValue: any) => {
|
||||
state.pageSize = newValue;
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
(newValue: any) => {
|
||||
if (newValue && newValue.length > 0) {
|
||||
props.columns.forEach((item) => {
|
||||
if (item.autoWidth && item.show) {
|
||||
item.autoCalculateMinWidth(props.data);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
const pageSize = props.pageSize;
|
||||
let pageSize = props.pageSize;
|
||||
|
||||
// 如果pageSize设为0,则使用系统全局配置的pageSize
|
||||
if (!pageSize) {
|
||||
pageSize = themeConfig.value.defaultListPageSize;
|
||||
// 可能storage已经存在配置json,则可能没值,需要清storage重试
|
||||
if (!pageSize) {
|
||||
pageSize = 10;
|
||||
}
|
||||
emit('update:pageSize', pageSize);
|
||||
}
|
||||
|
||||
state.pageNum = props.pageNum;
|
||||
state.pageSize = pageSize;
|
||||
state.queryForm = props.queryForm;
|
||||
state.queryForm_ = props.queryForm;
|
||||
state.pageSizes = [pageSize, pageSize * 2, pageSize * 3, pageSize * 4, pageSize * 5];
|
||||
|
||||
// 如果没传输入框宽度,则根据组件size设置默认宽度
|
||||
if (!props.inputWidth) {
|
||||
state.inputWidth = props.size == 'small' ? '150px' : '200px';
|
||||
state.inputWidth_ = props.size == 'small' ? '150px' : '200px';
|
||||
} else {
|
||||
state.inputWidth = props.inputWidth;
|
||||
state.inputWidth_ = props.inputWidth;
|
||||
}
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
calcuTableHeight();
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
const calcuTableHeight = () => {
|
||||
state.tableMaxHeight = window.innerHeight - 240 + 'px';
|
||||
}
|
||||
};
|
||||
|
||||
const formatText = (data: any)=> {
|
||||
const formatText = (data: any) => {
|
||||
state.formatVal = '';
|
||||
try {
|
||||
state.formatVal = JSON.stringify(JSON.parse(data), null, 4);
|
||||
} catch (e) {
|
||||
} catch (e) {
|
||||
state.formatVal = data;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getRowQueryItem = (row: number) => {
|
||||
// 第一行需要加个查询等按钮列
|
||||
if (row === 1) {
|
||||
const res = props.query.slice(row - 1, defaultQueryCount.value);
|
||||
// 查询等按钮列
|
||||
res.push(TableQuery.slot("", "", "queryBtns"));
|
||||
return res
|
||||
res.push(TableQuery.slot('', '', 'queryBtns'));
|
||||
return res;
|
||||
}
|
||||
const columnCount = defaultQueryCount.value + 1;
|
||||
return props.query.slice((row - 1) * columnCount - 1, row * columnCount - 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectionChange = (val: any) => {
|
||||
emit('update:selectionData', val);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePageChange = () => {
|
||||
emit('update:pageNum', state.pageNum);
|
||||
execQuery();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSizeChange = () => {
|
||||
changePageNum(1);
|
||||
emit('update:pageSize', state.pageSize);
|
||||
execQuery();
|
||||
}
|
||||
};
|
||||
|
||||
const queryData = () => {
|
||||
changePageNum(1);
|
||||
execQuery();
|
||||
}
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
// 将查询参数绑定的值置空,并重新粗发查询接口
|
||||
for (let qi of props.query) {
|
||||
state.queryForm[qi.prop] = null;
|
||||
state.queryForm_[qi.prop] = null;
|
||||
}
|
||||
|
||||
changePageNum(1);
|
||||
emit('update:queryForm', state.queryForm);
|
||||
emit('update:queryForm', state.queryForm_);
|
||||
execQuery();
|
||||
}
|
||||
};
|
||||
|
||||
const changePageNum = (pageNum: number) => {
|
||||
state.pageNum = pageNum;
|
||||
emit('update:pageNum', state.pageNum);
|
||||
}
|
||||
};
|
||||
|
||||
const execQuery = () => {
|
||||
emit('pageChange');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 是否正在加载数据
|
||||
*/
|
||||
const loading = (loading: boolean) => {
|
||||
state.loadingData = loading;
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({ loading })
|
||||
defineExpose({ loading });
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.page-table {
|
||||
@@ -420,15 +437,15 @@ defineExpose({ loading })
|
||||
}
|
||||
|
||||
.el-select-v2 {
|
||||
width: v-bind(inputWidth);
|
||||
width: v-bind(inputWidth_);
|
||||
}
|
||||
|
||||
.el-input {
|
||||
width: v-bind(inputWidth);
|
||||
width: v-bind(inputWidth_);
|
||||
}
|
||||
|
||||
.el-select {
|
||||
width: v-bind(inputWidth);
|
||||
width: v-bind(inputWidth_);
|
||||
}
|
||||
|
||||
.el-date-editor {
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
export const buildProgressProps = (): any => {
|
||||
return {
|
||||
progress: {
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
executedStatements: {
|
||||
type: Number,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<el-descriptions border size="small" :title="`${progress.title}`">
|
||||
<el-descriptions-item label="时间">{{ state.elapsedTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="已处理">{{ progress.executedStatements }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, reactive } from 'vue';
|
||||
import { formatTime } from 'element-plus/es/components/countdown/src/utils';
|
||||
import { buildProgressProps } from './progress-notify';
|
||||
|
||||
const props = defineProps(buildProgressProps());
|
||||
|
||||
const state = reactive({
|
||||
elapsedTime: '00:00:00',
|
||||
});
|
||||
|
||||
let timer: any = undefined;
|
||||
const startTime = Date.now();
|
||||
|
||||
onMounted(async () => {
|
||||
timer = setInterval(() => {
|
||||
const elapsed = Date.now() - startTime;
|
||||
state.elapsedTime = formatTime(elapsed, 'HH:mm:ss');
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
onUnmounted(async () => {
|
||||
if (timer != undefined) {
|
||||
clearInterval(timer); // 在Vue实例销毁前,清除我们的定时器
|
||||
timer = undefined;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -52,13 +52,13 @@
|
||||
title="最小化"
|
||||
/>
|
||||
|
||||
<!-- <SvgIcon name="FullScreen" @click="handlerFullScreen(openTerminal)" :size="20" class="pointer-icon mr10" title="全屏|退出全屏" /> -->
|
||||
<SvgIcon name="FullScreen" @click="handlerFullScreen(openTerminal)" :size="20" class="pointer-icon mr10" title="全屏|退出全屏" />
|
||||
|
||||
<SvgIcon name="Close" class="pointer-icon" @click="close(openTerminal.terminalId)" title="关闭" :size="20" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="terminal-wrapper" style="height: calc(100vh - 215px)">
|
||||
<div class="terminal-wrapper" :style="{ height: `calc(100vh - ${openTerminal.fullscreen ? '47px' : '200px'})` }">
|
||||
<TerminalBody
|
||||
@status-change="terminalStatusChange(openTerminal.terminalId, $event)"
|
||||
:ref="(el) => setTerminalRef(el, openTerminal.terminalId)"
|
||||
@@ -78,7 +78,7 @@
|
||||
size="small"
|
||||
@click="maximize(minimizeTerminal.terminalId)"
|
||||
>
|
||||
<el-tooltip effect="customized" :content="minimizeTerminal.desc" placement="top">
|
||||
<el-tooltip :content="minimizeTerminal.desc" placement="top">
|
||||
<span>
|
||||
{{ minimizeTerminal.title }}
|
||||
</span>
|
||||
@@ -230,6 +230,16 @@ function maximize(terminalId: any) {
|
||||
}, 250);
|
||||
}
|
||||
|
||||
const handlerFullScreen = (terminal: any) => {
|
||||
terminal.fullscreen = !terminal.fullscreen;
|
||||
const terminalRef = openTerminalRefs[terminal.terminalId];
|
||||
// fit
|
||||
setTimeout(() => {
|
||||
terminalRef?.fitTerminal();
|
||||
terminalRef?.focus();
|
||||
}, 250);
|
||||
};
|
||||
|
||||
const closeMinimizeTerminal = (terminalId: any) => {
|
||||
delete state.minimizeTerminals[terminalId];
|
||||
close(terminalId);
|
||||
@@ -249,9 +259,11 @@ defineExpose({
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
// .terminal-dialog {
|
||||
// height: calc(100vh - 200px) !important;
|
||||
// }
|
||||
// 取消body最大高度,否则全屏有问题
|
||||
.el-dialog__body {
|
||||
max-height: 100% !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.el-overlay .el-overlay-dialog .el-dialog .el-dialog__body {
|
||||
padding: 0px !important;
|
||||
|
||||
@@ -21,8 +21,8 @@ import pinia from '@/store/index';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
import { useRoutesList } from '@/store/routesList';
|
||||
import Logo from '@/views/layout/logo/index.vue';
|
||||
import Vertical from '@/views/layout/navMenu/vertical.vue';
|
||||
import Logo from '@/layout/logo/index.vue';
|
||||
import Vertical from '@/layout/navMenu/vertical.vue';
|
||||
import mittBus from '@/common/utils/mitt';
|
||||
|
||||
const { proxy } = getCurrentInstance() as any;
|
||||
@@ -38,8 +38,7 @@ const state: any = reactive({
|
||||
// 设置菜单展开/收起时的宽度
|
||||
const setCollapseWidth = computed(() => {
|
||||
let { layout, isCollapse, menuBar } = themeConfig.value;
|
||||
let asideBrColor =
|
||||
menuBar === '#FFFFFF' || menuBar === '#FFF' || menuBar === '#fff' || menuBar === '#ffffff' ? 'layout-el-aside-br-color' : '';
|
||||
let asideBrColor = menuBar === '#FFFFFF' || menuBar === '#FFF' || menuBar === '#fff' || menuBar === '#ffffff' ? 'layout-el-aside-br-color' : '';
|
||||
if (layout === 'columns') {
|
||||
// 分栏布局,菜单收起时宽度给 1px
|
||||
if (isCollapse) {
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import NavBarsIndex from '@/views/layout/navBars/index.vue';
|
||||
import NavBarsIndex from '@/layout/navBars/index.vue';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
export default {
|
||||
name: 'layoutHeader',
|
||||
@@ -1,16 +1,25 @@
|
||||
<template>
|
||||
<el-main class="layout-main">
|
||||
<el-scrollbar class="layout-scrollbar" ref="layoutScrollbarRef"
|
||||
<el-scrollbar
|
||||
class="layout-scrollbar"
|
||||
ref="layoutScrollbarRef"
|
||||
v-show="!state.currentRouteMeta.link && state.currentRouteMeta.linkType != 1"
|
||||
:style="{ minHeight: `calc(100vh - ${state.headerHeight}` }">
|
||||
:style="{ minHeight: `calc(100vh - ${state.headerHeight}` }"
|
||||
>
|
||||
<LayoutParentView />
|
||||
<Footer v-if="themeConfig.isFooter" />
|
||||
</el-scrollbar>
|
||||
<Link :style="{ height: `calc(100vh - ${state.headerHeight}` }" :meta="state.currentRouteMeta"
|
||||
v-if="state.currentRouteMeta.link && state.currentRouteMeta.linkType == 2" />
|
||||
<Iframes :style="{ height: `calc(100vh - ${state.headerHeight}` }" :meta="state.currentRouteMeta"
|
||||
<Link
|
||||
:style="{ height: `calc(100vh - ${state.headerHeight}` }"
|
||||
:meta="state.currentRouteMeta"
|
||||
v-if="state.currentRouteMeta.link && state.currentRouteMeta.linkType == 2"
|
||||
/>
|
||||
<Iframes
|
||||
:style="{ height: `calc(100vh - ${state.headerHeight}` }"
|
||||
:meta="state.currentRouteMeta"
|
||||
v-if="state.currentRouteMeta.link && state.currentRouteMeta.linkType == 1 && state.isShowLink"
|
||||
@getCurrentRouteMeta="onGetCurrentRouteMeta" />
|
||||
@getCurrentRouteMeta="onGetCurrentRouteMeta"
|
||||
/>
|
||||
</el-main>
|
||||
</template>
|
||||
|
||||
@@ -19,10 +28,10 @@ import { reactive, getCurrentInstance, watch, onBeforeMount } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
import LayoutParentView from '@/views/layout/routerView/parent.vue';
|
||||
import Footer from '@/views/layout/footer/index.vue';
|
||||
import Link from '@/views/layout/routerView/link.vue';
|
||||
import Iframes from '@/views/layout/routerView/iframes.vue';
|
||||
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 { themeConfig } = storeToRefs(useThemeConfig());
|
||||
@@ -10,10 +10,10 @@ import { onBeforeMount, onUnmounted } from 'vue';
|
||||
import { getLocal, setLocal } from '@/common/utils/storage';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
import Defaults from '@/views/layout/main/defaults.vue';
|
||||
import Classic from '@/views/layout/main/classic.vue';
|
||||
import Transverse from '@/views/layout/main/transverse.vue';
|
||||
import Columns from '@/views/layout/main/columns.vue';
|
||||
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';
|
||||
|
||||
const { themeConfig } = storeToRefs(useThemeConfig());
|
||||
@@ -213,7 +213,7 @@ onUnmounted(() => {
|
||||
}
|
||||
.layout-lock-screen-img {
|
||||
@extend .layout-lock-screen-fixed;
|
||||
background: url('@/assets/image/bg-login.png') no-repeat;
|
||||
background: url('@/assets/image/login-bg-main.svg') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
z-index: 9999991;
|
||||
}
|
||||
@@ -15,10 +15,10 @@
|
||||
<script lang="ts" setup name="layoutClassic">
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
import Aside from '@/views/layout/component/aside.vue';
|
||||
import Header from '@/views/layout/component/header.vue';
|
||||
import Main from '@/views/layout/component/main.vue';
|
||||
import TagsView from '@/views/layout/navBars/tagsView/tagsView.vue';
|
||||
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';
|
||||
|
||||
const { themeConfig } = storeToRefs(useThemeConfig());
|
||||
</script>
|
||||
@@ -17,10 +17,10 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import Aside from '@/views/layout/component/aside.vue';
|
||||
import Header from '@/views/layout/component/header.vue';
|
||||
import Main from '@/views/layout/component/main.vue';
|
||||
import ColumnsAside from '@/views/layout/component/columnsAside.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 { useThemeConfig } from '@/store/themeConfig';
|
||||
export default {
|
||||
name: 'layoutColumns',
|
||||
@@ -15,9 +15,9 @@
|
||||
<script lang="ts">
|
||||
import { computed, getCurrentInstance, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import Aside from '@/views/layout/component/aside.vue';
|
||||
import Header from '@/views/layout/component/header.vue';
|
||||
import Main from '@/views/layout/component/main.vue';
|
||||
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';
|
||||
export default {
|
||||
name: 'layoutDefaults',
|
||||
@@ -7,8 +7,8 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Header from '@/views/layout/component/header.vue';
|
||||
import Main from '@/views/layout/component/main.vue';
|
||||
import Header from '@/layout/component/header.vue';
|
||||
import Main from '@/layout/component/main.vue';
|
||||
export default {
|
||||
name: 'layoutTransverse',
|
||||
components: { Header, Main },
|
||||
@@ -14,10 +14,10 @@ import pinia from '@/store/index';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
import { useRoutesList } from '@/store/routesList';
|
||||
import Breadcrumb from '@/views/layout/navBars/breadcrumb/breadcrumb.vue';
|
||||
import User from '@/views/layout/navBars/breadcrumb/user.vue';
|
||||
import Logo from '@/views/layout/logo/index.vue';
|
||||
import Horizontal from '@/views/layout/navMenu/horizontal.vue';
|
||||
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 { themeConfig } = storeToRefs(useThemeConfig());
|
||||
@@ -61,6 +61,24 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 全局设置 -->
|
||||
<el-divider content-position="left">全局设置</el-divider>
|
||||
<div class="layout-breadcrumb-seting-bar-flex mt15">
|
||||
<div class="layout-breadcrumb-seting-bar-flex-label">分页size</div>
|
||||
<div class="layout-breadcrumb-seting-bar-flex-value">
|
||||
<el-input-number
|
||||
v-model="themeConfig.defaultListPageSize"
|
||||
controls-position="right"
|
||||
:min="10"
|
||||
:max="50"
|
||||
@change="setLocalThemeConfig"
|
||||
size="small"
|
||||
style="width: 90px"
|
||||
>
|
||||
</el-input-number>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 全局主题 -->
|
||||
<el-divider content-position="left">全局主题</el-divider>
|
||||
<div class="layout-breadcrumb-seting-bar-flex">
|
||||
@@ -374,7 +392,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="copy-config">
|
||||
<!-- <div class="copy-config">
|
||||
<el-alert title="点击下方按钮,复制布局配置去 /src/store/modules/themeConfig.ts中修改" type="warning" :closable="false"> </el-alert>
|
||||
<el-button
|
||||
size="small"
|
||||
@@ -385,7 +403,7 @@
|
||||
@click="onCopyConfigClick($event.target)"
|
||||
>一键复制配置
|
||||
</el-button>
|
||||
</div>
|
||||
</div> -->
|
||||
</el-scrollbar>
|
||||
</el-drawer>
|
||||
</div>
|
||||
@@ -56,7 +56,7 @@
|
||||
<crop />
|
||||
</el-icon>
|
||||
</div>
|
||||
<el-dropdown :show-timeout="70" :hide-timeout="50" @command="onHandleCommandClick">
|
||||
<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" />
|
||||
{{ userInfo.name || userInfo.username }}
|
||||
@@ -75,7 +75,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="layoutBreadcrumbUser">
|
||||
import { ref, computed, reactive, onMounted, nextTick } from 'vue';
|
||||
import { ref, computed, reactive, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { ElMessageBox, ElMessage } from 'element-plus';
|
||||
import screenfull from 'screenfull';
|
||||
@@ -83,11 +83,12 @@ import { resetRoute } from '@/router/index';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useUserInfo } from '@/store/userInfo';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
import { clearSession, setLocal, getLocal, removeLocal } from '@/common/utils/storage';
|
||||
import UserNews from '@/views/layout/navBars/breadcrumb/userNews.vue';
|
||||
import SearchMenu from '@/views/layout/navBars/breadcrumb/search.vue';
|
||||
import { clearSession, removeLocal } 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 { saveThemeConfig, getThemeConfig } from '@/common/utils/storage';
|
||||
|
||||
const router = useRouter();
|
||||
const searchRef = ref();
|
||||
@@ -99,7 +100,8 @@ const state = reactive({
|
||||
disabledSize: '',
|
||||
});
|
||||
const { userInfo } = storeToRefs(useUserInfo());
|
||||
const { themeConfig } = storeToRefs(useThemeConfig());
|
||||
const themeConfigStore = useThemeConfig();
|
||||
const { themeConfig } = storeToRefs(themeConfigStore);
|
||||
|
||||
// 设置分割样式
|
||||
const layoutUserFlexNum = computed(() => {
|
||||
@@ -164,16 +166,8 @@ const onHandleCommandClick = (path: string) => {
|
||||
};
|
||||
|
||||
const switchDark = (isDark: boolean) => {
|
||||
themeConfig.value.isDark = isDark;
|
||||
setLocal('themeConfig', themeConfig.value);
|
||||
const body = document.documentElement as HTMLElement;
|
||||
if (isDark) {
|
||||
body.setAttribute('class', 'dark');
|
||||
themeConfig.value.editorTheme = 'vs-dark';
|
||||
} else {
|
||||
body.setAttribute('class', '');
|
||||
themeConfig.value.editorTheme = 'SolarizedLight';
|
||||
}
|
||||
themeConfigStore.switchDark(isDark);
|
||||
saveThemeConfig(themeConfig.value);
|
||||
};
|
||||
|
||||
// // 菜单搜索点击
|
||||
@@ -185,7 +179,7 @@ const onSearchClick = () => {
|
||||
const onComponentSizeChange = (size: string) => {
|
||||
removeLocal('themeConfig');
|
||||
themeConfig.value.globalComponentSize = size;
|
||||
setLocal('themeConfig', themeConfig.value);
|
||||
saveThemeConfig(themeConfig.value);
|
||||
// proxy.$ELEMENT.size = size;
|
||||
initComponentSize();
|
||||
window.location.reload();
|
||||
@@ -193,7 +187,7 @@ const onComponentSizeChange = (size: string) => {
|
||||
|
||||
// 初始化全局组件大小
|
||||
const initComponentSize = () => {
|
||||
switch (getLocal('themeConfig').globalComponentSize) {
|
||||
switch (getThemeConfig().globalComponentSize) {
|
||||
case '':
|
||||
state.disabledSize = '';
|
||||
break;
|
||||
@@ -211,12 +205,10 @@ const initComponentSize = () => {
|
||||
|
||||
// 页面加载时
|
||||
onMounted(() => {
|
||||
if (getLocal('themeConfig')) {
|
||||
const isDark = themeConfig.value.isDark;
|
||||
state.isDark = isDark;
|
||||
switchDark(isDark);
|
||||
|
||||
const themeConfig = getThemeConfig();
|
||||
if (themeConfig) {
|
||||
initComponentSize();
|
||||
state.isDark = themeConfig.isDark;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -8,8 +8,8 @@
|
||||
<script lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
import BreadcrumbIndex from '@/views/layout/navBars/breadcrumb/index.vue';
|
||||
import TagsView from '@/views/layout/navBars/tagsView/tagsView.vue';
|
||||
import BreadcrumbIndex from '@/layout/navBars/breadcrumb/index.vue';
|
||||
import TagsView from '@/layout/navBars/tagsView/tagsView.vue';
|
||||
export default {
|
||||
name: 'layoutNavBars',
|
||||
components: { BreadcrumbIndex, TagsView },
|
||||
@@ -3,7 +3,7 @@
|
||||
<el-scrollbar ref="scrollbarRef" @wheel.prevent="onHandleScroll">
|
||||
<ul class="layout-navbars-tagsview-ul" :class="setTagsStyle" ref="tagsUlRef">
|
||||
<li
|
||||
v-for="(v, k) in state.tagsViewList"
|
||||
v-for="(v, k) in tagsViews"
|
||||
:key="k"
|
||||
class="layout-navbars-tagsview-ul-li"
|
||||
:data-name="v.name"
|
||||
@@ -17,8 +17,8 @@
|
||||
"
|
||||
>
|
||||
<SvgIcon name="iconfont icon-tag-view-active" class="layout-navbars-tagsview-ul-li-iconfont font14" v-if="isActive(v)" />
|
||||
<SvgIcon :name="v.meta.icon" class="layout-navbars-tagsview-ul-li-iconfont" v-if="!isActive(v) && themeConfig.isTagsviewIcon" />
|
||||
<span>{{ v.meta.title }}</span>
|
||||
<SvgIcon :name="v.icon" class="layout-navbars-tagsview-ul-li-iconfont" v-if="!isActive(v) && themeConfig.isTagsviewIcon" />
|
||||
<span>{{ v.title }}</span>
|
||||
<template v-if="isActive(v)">
|
||||
<SvgIcon
|
||||
name="RefreshRight"
|
||||
@@ -28,7 +28,7 @@
|
||||
<SvgIcon
|
||||
name="Close"
|
||||
class="font14 layout-navbars-tagsview-ul-li-icon layout-icon-active"
|
||||
v-if="!v.meta.isAffix"
|
||||
v-if="!v.isAffix"
|
||||
@click.stop="closeCurrentTagsView(themeConfig.isShareTagsView ? v.path : v.path)"
|
||||
/>
|
||||
</template>
|
||||
@@ -36,42 +36,75 @@
|
||||
<SvgIcon
|
||||
name="Close"
|
||||
class="font14 layout-navbars-tagsview-ul-li-icon layout-icon-three"
|
||||
v-if="!v.meta.isAffix"
|
||||
v-if="!v.isAffix"
|
||||
@click.stop="closeCurrentTagsView(themeConfig.isShareTagsView ? v.path : v.path)"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</el-scrollbar>
|
||||
<Contextmenu :dropdown="state.dropdown" ref="contextmenuRef" @currentContextmenuClick="onCurrentContextmenuClick" />
|
||||
<Contextmenu :items="state.contextmenu.items" :dropdown="state.contextmenu.dropdown" ref="contextmenuRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="layoutTagsView">
|
||||
import { reactive, onMounted, computed, ref, nextTick, onBeforeUpdate, onBeforeMount, onUnmounted, getCurrentInstance, watch } from 'vue';
|
||||
import { reactive, onMounted, computed, ref, nextTick, onBeforeUpdate, onBeforeMount, onUnmounted, getCurrentInstance } from 'vue';
|
||||
import { useRoute, useRouter, onBeforeRouteUpdate } from 'vue-router';
|
||||
import screenfull from 'screenfull';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
import { getSession, setSession, removeSession } from '@/common/utils/storage';
|
||||
import mittBus from '@/common/utils/mitt';
|
||||
import Sortable from 'sortablejs';
|
||||
import Contextmenu from '@/views/layout/navBars/tagsView/contextmenu.vue';
|
||||
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
|
||||
import { getTagViews, setTagViews, removeTagViews } from '@/common/utils/storage';
|
||||
import { useTagsViews } from '@/store/tagsViews';
|
||||
import { useKeepALiveNames } from '@/store/keepAliveNames';
|
||||
|
||||
const { proxy } = getCurrentInstance() as any;
|
||||
const tagsRefs = ref([]) as any;
|
||||
const scrollbarRef = ref();
|
||||
const contextmenuRef = ref();
|
||||
const tagsUlRef = ref();
|
||||
|
||||
const { themeConfig } = storeToRefs(useThemeConfig());
|
||||
const { tagsViews } = storeToRefs(useTagsViews());
|
||||
|
||||
const keepAliveNamesStores = useKeepALiveNames();
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const contextmenuItems = [
|
||||
new ContextmenuItem(0, '刷新').withIcon('RefreshRight').withOnClick((data: any) => {
|
||||
// path为fullPath
|
||||
let { path } = data;
|
||||
let currentTag = tagsViews.value.find((v: any) => v.path === path);
|
||||
refreshCurrentTagsView(path);
|
||||
router.push({ path, query: currentTag?.query });
|
||||
}),
|
||||
|
||||
new ContextmenuItem(1, '关闭').withIcon('Close').withOnClick((data: any) => closeCurrentTagsView(data.path)),
|
||||
|
||||
new ContextmenuItem(2, '关闭其他').withIcon('CircleClose').withOnClick((data: any) => {
|
||||
let { path } = data;
|
||||
let currentTag = tagsViews.value.find((v: any) => v.path === path);
|
||||
router.push({ path, query: currentTag?.query });
|
||||
closeOtherTagsView(path);
|
||||
}),
|
||||
|
||||
new ContextmenuItem(3, '关闭所有').withIcon('FolderDelete').withOnClick((data: any) => closeAllTagsView(data.path)),
|
||||
|
||||
new ContextmenuItem(4, '当前页全屏').withIcon('full-screen').withOnClick((data: any) => openCurrenFullscreen(data.path)),
|
||||
];
|
||||
|
||||
const state = reactive({
|
||||
routePath: route.fullPath,
|
||||
dropdown: { x: '', y: '' },
|
||||
// dropdown: { x: '', y: '' },
|
||||
tagsRefsIndex: 0,
|
||||
tagsViewList: [] as any,
|
||||
sortable: '' as any,
|
||||
contextmenu: {
|
||||
items: contextmenuItems,
|
||||
dropdown: { x: '', y: '' },
|
||||
},
|
||||
});
|
||||
|
||||
// 动态设置 tagsView 风格样式
|
||||
@@ -81,163 +114,173 @@ const setTagsStyle = computed(() => {
|
||||
|
||||
// 存储 tagsViewList 到浏览器临时缓存中,页面刷新时,保留记录
|
||||
const addBrowserSetSession = (tagsViewList: Array<object>) => {
|
||||
setSession('tagsViewList', tagsViewList);
|
||||
setTagViews(tagsViewList);
|
||||
};
|
||||
|
||||
// 获取 vuex 中的 tagsViewRoutes 列表
|
||||
// 获取 tagsViewRoutes 列表
|
||||
const getTagsViewRoutes = () => {
|
||||
state.routePath = route.fullPath;
|
||||
state.tagsViewList = [];
|
||||
if (!themeConfig.value.isCacheTagsView) removeSession('tagsViewList');
|
||||
tagsViews.value = [];
|
||||
if (!themeConfig.value.isCacheTagsView) {
|
||||
removeTagViews();
|
||||
}
|
||||
initTagsView();
|
||||
};
|
||||
// vuex 中获取路由信息:如果是设置了固定的(isAffix),进行初始化显示
|
||||
// 获取路由信息:如果是设置了固定的(isAffix),进行初始化显示
|
||||
const initTagsView = () => {
|
||||
if (getSession('tagsViewList') && themeConfig.value.isCacheTagsView) {
|
||||
state.tagsViewList = getSession('tagsViewList');
|
||||
const tagViews = getTagViews();
|
||||
if (tagViews && themeConfig.value.isCacheTagsView) {
|
||||
tagsViews.value = tagViews;
|
||||
} else {
|
||||
state.tagsViewList?.map((v: any) => {
|
||||
if (v.meta.isAffix && !v.meta.isHide) state.tagsViewList.push({ ...v });
|
||||
tagsViews.value?.map((v: any) => {
|
||||
if (v.isAffix && !v.isHide) {
|
||||
tagsViews.value.push({ ...v });
|
||||
keepAliveNamesStores.setCacheKeepAlive(v);
|
||||
}
|
||||
});
|
||||
addTagsView(route.fullPath);
|
||||
}
|
||||
// 初始化当前元素(li)的下标
|
||||
getTagsRefsIndex(route.fullPath);
|
||||
setTagsRefsIndex(route.fullPath);
|
||||
// 添加初始化横向滚动条移动到对应位置
|
||||
tagsViewmoveToCurrentTag();
|
||||
};
|
||||
|
||||
// 1、添加 tagsView:未设置隐藏(isHide)也添加到在 tagsView 中
|
||||
// path为fullPath
|
||||
const addTagsView = (path: string, to: any = null) => {
|
||||
if (!to) {
|
||||
to = route;
|
||||
}
|
||||
|
||||
path = decodeURI(path);
|
||||
for (let tv of state.tagsViewList) {
|
||||
if (tv.fullPath === path) {
|
||||
return false;
|
||||
const addTagsView = (path: string, to: any = null, tagViewIndex: number = -1) => {
|
||||
nextTick(async () => {
|
||||
if (!to) {
|
||||
to = route;
|
||||
}
|
||||
}
|
||||
|
||||
const tagView = { ...to };
|
||||
// 防止Converting circular structure to JSON错误
|
||||
tagView.matched = null;
|
||||
tagView.redirectedFrom = null;
|
||||
state.tagsViewList.push(tagView);
|
||||
addBrowserSetSession(state.tagsViewList);
|
||||
for (let tv of tagsViews.value) {
|
||||
if (tv.path === path) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const tagView = {
|
||||
path: path,
|
||||
name: to.name,
|
||||
query: to.query,
|
||||
title: to.meta.title,
|
||||
icon: to.meta.icon,
|
||||
isAffix: to.meta.isAffix,
|
||||
isKeepAlive: to.meta.isKeepAlive,
|
||||
};
|
||||
|
||||
if (tagViewIndex != -1) {
|
||||
tagsViews.value.splice(tagViewIndex + 1, 0, tagView);
|
||||
} else {
|
||||
tagsViews.value.push(tagView);
|
||||
}
|
||||
await keepAliveNamesStores.addCachedView(tagView);
|
||||
addBrowserSetSession(tagsViews.value);
|
||||
});
|
||||
};
|
||||
|
||||
// 2、刷新当前 tagsView:
|
||||
// path为fullPath
|
||||
const refreshCurrentTagsView = (path: string) => {
|
||||
const refreshCurrentTagsView = async (path: string) => {
|
||||
const item = getTagsView(path);
|
||||
await keepAliveNamesStores.delCachedView(item);
|
||||
keepAliveNamesStores.addCachedView(item);
|
||||
mittBus.emit('onTagsViewRefreshRouterView', path);
|
||||
};
|
||||
|
||||
const getTagsView = (path: string) => {
|
||||
return tagsViews.value.find((v: any) => v.path === path);
|
||||
};
|
||||
|
||||
// 3、关闭当前 tagsView:如果是设置了固定的(isAffix),不可以关闭
|
||||
// path为fullPath
|
||||
const closeCurrentTagsView = (path: string) => {
|
||||
state.tagsViewList.map((v: any, k: number, arr: any) => {
|
||||
if (!v.meta.isAffix) {
|
||||
if (v.fullPath === path) {
|
||||
state.tagsViewList.splice(k, 1);
|
||||
tagsViews.value.map((v: TagsView, k: number, arr: any) => {
|
||||
if (!v.isAffix) {
|
||||
if (v.path === path) {
|
||||
keepAliveNamesStores.delCachedView(v);
|
||||
tagsViews.value.splice(k, 1);
|
||||
setTimeout(() => {
|
||||
if (state.routePath !== path) {
|
||||
return;
|
||||
}
|
||||
let next;
|
||||
let next: TagsView;
|
||||
// 最后一个且高亮时
|
||||
if (state.tagsViewList.length === k) {
|
||||
if (tagsViews.value.length === k) {
|
||||
next = k !== arr.length ? arr[k] : arr[arr.length - 1];
|
||||
} else {
|
||||
next = arr[k];
|
||||
}
|
||||
|
||||
if (next.meta.isDynamic) {
|
||||
router.push({ name: next.name, params: next.params });
|
||||
} else {
|
||||
if (next) {
|
||||
router.push({ path: next.path, query: next.query });
|
||||
} else {
|
||||
router.push({ path: '/' });
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
});
|
||||
addBrowserSetSession(state.tagsViewList);
|
||||
addBrowserSetSession(tagsViews.value);
|
||||
};
|
||||
|
||||
// 4、关闭其它 tagsView:如果是设置了固定的(isAffix),不进行关闭
|
||||
const closeOtherTagsView = (path: string) => {
|
||||
const oldTagViews = state.tagsViewList;
|
||||
state.tagsViewList = [];
|
||||
oldTagViews.map((v: any) => {
|
||||
if (v.meta.isAffix && !v.meta.isHide) state.tagsViewList.push({ ...v });
|
||||
const oldTagViews = tagsViews.value;
|
||||
tagsViews.value = [];
|
||||
oldTagViews.map((v: TagsView) => {
|
||||
if (v.isAffix && !v.isHide) {
|
||||
keepAliveNamesStores.delOthersCachedViews(v);
|
||||
tagsViews.value.push({ ...v });
|
||||
}
|
||||
});
|
||||
addTagsView(path);
|
||||
};
|
||||
|
||||
// 5、关闭全部 tagsView:如果是设置了固定的(isAffix),不进行关闭
|
||||
const closeAllTagsView = (path: string) => {
|
||||
const oldTagViews = state.tagsViewList;
|
||||
state.tagsViewList = [];
|
||||
keepAliveNamesStores.delAllCachedViews();
|
||||
const oldTagViews = tagsViews.value;
|
||||
tagsViews.value = [];
|
||||
oldTagViews.map((v: any) => {
|
||||
if (v.meta.isAffix && !v.meta.isHide) {
|
||||
state.tagsViewList.push({ ...v });
|
||||
if (state.tagsViewList.some((v: any) => v.path === path)) router.push({ path, query: route.query });
|
||||
else router.push({ path: v.path, query: route.query });
|
||||
if (v.isAffix && !v.isHide) {
|
||||
tagsViews.value.push({ ...v });
|
||||
if (tagsViews.value.some((v: any) => v.path === path)) {
|
||||
router.push({ path, query: route.query });
|
||||
}
|
||||
}
|
||||
});
|
||||
addBrowserSetSession(state.tagsViewList);
|
||||
if (tagsViews.value) {
|
||||
router.push({ path: '/' });
|
||||
}
|
||||
addBrowserSetSession(tagsViews.value);
|
||||
};
|
||||
// 6、开启当前页面全屏
|
||||
const openCurrenFullscreen = (path: string) => {
|
||||
const item = state.tagsViewList.find((v: any) => v.fullPath === path);
|
||||
const item = tagsViews.value.find((v: any) => v.path === path);
|
||||
nextTick(() => {
|
||||
router.push({ path, query: item.query });
|
||||
router.push({ path, query: item?.query });
|
||||
const element = document.querySelector('.layout-main');
|
||||
const screenfulls: any = screenfull;
|
||||
screenfulls.request(element);
|
||||
});
|
||||
};
|
||||
// 当前项右键菜单点击
|
||||
const onCurrentContextmenuClick = (data: any) => {
|
||||
// path为fullPath
|
||||
let { id, path } = data;
|
||||
let currentTag = state.tagsViewList.find((v: any) => v.fullPath === path);
|
||||
switch (id) {
|
||||
case 0:
|
||||
refreshCurrentTagsView(path);
|
||||
router.push({ path, query: currentTag.query });
|
||||
break;
|
||||
case 1:
|
||||
closeCurrentTagsView(path);
|
||||
break;
|
||||
case 2:
|
||||
router.push({ path, query: currentTag.query });
|
||||
closeOtherTagsView(path);
|
||||
break;
|
||||
case 3:
|
||||
closeAllTagsView(path);
|
||||
break;
|
||||
case 4:
|
||||
openCurrenFullscreen(path);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// 判断页面高亮
|
||||
const isActive = (route: any) => {
|
||||
return route.fullPath === state.routePath;
|
||||
const isActive = (tagView: TagsView) => {
|
||||
return tagView.path === state.routePath;
|
||||
};
|
||||
// 右键点击时:传 x,y 坐标值到子组件中(props)
|
||||
const onContextmenu = (v: any, e: any) => {
|
||||
const { clientX, clientY } = e;
|
||||
state.dropdown.x = clientX;
|
||||
state.dropdown.y = clientY;
|
||||
state.contextmenu.dropdown.x = clientX;
|
||||
state.contextmenu.dropdown.y = clientY;
|
||||
contextmenuRef.value.openContextmenu(v);
|
||||
};
|
||||
// 当前的 tagsView 项点击时
|
||||
const onTagsClick = (v: any, k: number) => {
|
||||
state.routePath = decodeURI(v.fullPath);
|
||||
state.routePath = decodeURI(v.path);
|
||||
state.tagsRefsIndex = k;
|
||||
router.push(v);
|
||||
};
|
||||
@@ -302,9 +345,9 @@ const tagsViewmoveToCurrentTag = () => {
|
||||
});
|
||||
};
|
||||
// 获取 tagsView 的下标:用于处理 tagsView 点击时的横向滚动
|
||||
const getTagsRefsIndex = (path: string) => {
|
||||
if (state.tagsViewList.length > 0) {
|
||||
state.tagsRefsIndex = state.tagsViewList.findIndex((item: any) => item.fullPath === path);
|
||||
const setTagsRefsIndex = (path: string) => {
|
||||
if (tagsViews.value.length > 0) {
|
||||
state.tagsRefsIndex = tagsViews.value.findIndex((item: any) => item.path === path);
|
||||
}
|
||||
};
|
||||
// 设置 tagsView 可以进行拖拽
|
||||
@@ -319,7 +362,7 @@ const initSortable = () => {
|
||||
onEnd: () => {
|
||||
const sortEndList: any = [];
|
||||
state.sortable.toArray().map((val: any) => {
|
||||
state.tagsViewList.map((v: any) => {
|
||||
tagsViews.value.map((v: any) => {
|
||||
if (v.name === val) sortEndList.push({ ...v });
|
||||
});
|
||||
});
|
||||
@@ -329,24 +372,8 @@ const initSortable = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 监听路由的变化,动态赋值给 tagsView
|
||||
// watch(
|
||||
// pinia.state,
|
||||
// (val) => {
|
||||
// if (val.tagsViewRoutes.tagsViewRoutes.length === state.tagsViewRoutesList.length) return false;
|
||||
// getTagsViewRoutes();
|
||||
// },
|
||||
// {
|
||||
// deep: true,
|
||||
// }
|
||||
// );
|
||||
|
||||
// 页面加载前
|
||||
onBeforeMount(() => {
|
||||
// 监听非本页面调用 0 刷新当前,1 关闭当前,2 关闭其它,3 关闭全部 4 当前页全屏
|
||||
mittBus.on('onCurrentContextmenuClick', (data: object) => {
|
||||
onCurrentContextmenuClick(data);
|
||||
});
|
||||
// 监听布局配置界面开启/关闭拖拽
|
||||
mittBus.on('openOrCloseSortable', () => {
|
||||
initSortable();
|
||||
@@ -354,8 +381,6 @@ onBeforeMount(() => {
|
||||
});
|
||||
// 页面卸载时
|
||||
onUnmounted(() => {
|
||||
// 取消非本页面调用监听
|
||||
mittBus.off('onCurrentContextmenuClick');
|
||||
// 取消监听布局配置界面开启/关闭拖拽
|
||||
mittBus.off('openOrCloseSortable');
|
||||
});
|
||||
@@ -371,9 +396,10 @@ onMounted(() => {
|
||||
});
|
||||
// 路由更新时
|
||||
onBeforeRouteUpdate((to) => {
|
||||
state.routePath = decodeURI(to.fullPath);
|
||||
addTagsView(to.fullPath, to);
|
||||
getTagsRefsIndex(to.fullPath);
|
||||
const path = decodeURI(to.fullPath);
|
||||
state.routePath = path;
|
||||
addTagsView(path, to, state.tagsRefsIndex);
|
||||
setTagsRefsIndex(path);
|
||||
tagsViewmoveToCurrentTag();
|
||||
});
|
||||
</script>
|
||||
@@ -494,8 +520,14 @@ onBeforeRouteUpdate((to) => {
|
||||
-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: 18px 30px, 20px 30px, calc(100% - 30px) calc(100% + 17px);
|
||||
-webkit-mask-position: right bottom, left bottom, center top;
|
||||
-webkit-mask-size:
|
||||
18px 30px,
|
||||
20px 30px,
|
||||
calc(100% - 30px) calc(100% + 17px);
|
||||
-webkit-mask-position:
|
||||
right bottom,
|
||||
left bottom,
|
||||
center top;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
|
||||
@@ -516,14 +548,14 @@ onBeforeRouteUpdate((to) => {
|
||||
|
||||
&:hover {
|
||||
@extend .tgs-style-three-svg;
|
||||
background: var(--el-color-primary-light-9);
|
||||
background: var(--tagsview3-active-background-color);
|
||||
color: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.is-active {
|
||||
@extend .tgs-style-three-svg;
|
||||
background: var(--el-color-primary-light-9) !important;
|
||||
background: var(--tagsview3-active-background-color) !important;
|
||||
color: var(--el-color-primary) !important;
|
||||
z-index: 1;
|
||||
}
|
||||
@@ -1,24 +1,23 @@
|
||||
<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">
|
||||
<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"/>
|
||||
<SvgIcon :name="val.meta.icon" />
|
||||
<span>{{ 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"/>
|
||||
<SvgIcon :name="val.meta.icon" />
|
||||
{{ val.meta.title }}
|
||||
</template>
|
||||
<template #title v-else>
|
||||
<a :href="val.meta.link" target="_blank">
|
||||
<SvgIcon :name="val.meta.icon"/>
|
||||
<SvgIcon :name="val.meta.icon" />
|
||||
{{ val.meta.title }}
|
||||
</a>
|
||||
</template>
|
||||
@@ -32,7 +31,7 @@
|
||||
<script lang="ts" setup name="navMenuHorizontal">
|
||||
import { reactive, computed, getCurrentInstance, onMounted, nextTick } from 'vue';
|
||||
import { useRoute, onBeforeRouteUpdate } from 'vue-router';
|
||||
import SubItem from '@/views/layout/navMenu/subItem.vue';
|
||||
import SubItem from '@/layout/navMenu/subItem.vue';
|
||||
import { useRoutesList } from '@/store/routesList';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
import mittBus from '@/common/utils/mitt';
|
||||
@@ -1,21 +1,28 @@
|
||||
<template>
|
||||
<el-menu router :default-active="state.defaultActive" background-color="transparent" :collapse="setIsCollapse"
|
||||
:unique-opened="themeConfig.isUniqueOpened" :collapse-transition="false">
|
||||
<el-menu
|
||||
router
|
||||
:default-active="state.defaultActive"
|
||||
background-color="transparent"
|
||||
:collapse="setIsCollapse"
|
||||
:unique-opened="themeConfig.isUniqueOpened"
|
||||
:collapse-transition="false"
|
||||
>
|
||||
<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"/>
|
||||
<SvgIcon :name="val.meta.icon" />
|
||||
<span>{{ val.meta.title }}</span>
|
||||
</template>
|
||||
<SubItem :chil="val.children" />
|
||||
</el-sub-menu>
|
||||
<el-menu-item :index="val.path" :key="val?.path" v-else>
|
||||
<SvgIcon :name="val.meta.icon"/>
|
||||
<SvgIcon :name="val.meta.icon" />
|
||||
<template #title v-if="!val.meta.link || (val.meta.link && val.meta.linkType == 1)">
|
||||
<span>{{ val.meta.title }}</span>
|
||||
</template>
|
||||
<template #title v-else>
|
||||
<a :href="val.meta.link" target="_blank">{{ val.meta.title }}</a></template>
|
||||
<a :href="val.meta.link" target="_blank">{{ val.meta.title }}</a></template
|
||||
>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
</el-menu>
|
||||
@@ -26,7 +33,7 @@ import { reactive, computed } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
import { useRoute, onBeforeRouteUpdate } from 'vue-router';
|
||||
import SubItem from '@/views/layout/navMenu/subItem.vue';
|
||||
import SubItem from '@/layout/navMenu/subItem.vue';
|
||||
import mittBus from '@/common/utils/mitt';
|
||||
|
||||
// 定义父组件传过来的值
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="h100">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition :name="setTransitionName" mode="out-in">
|
||||
<keep-alive :include="state.keepAliveNameList">
|
||||
<keep-alive :include="getKeepAliveNames">
|
||||
<component :is="Component" :key="state.refreshRouterViewKey" class="w100" />
|
||||
</keep-alive>
|
||||
</transition>
|
||||
@@ -11,38 +11,62 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="layoutParentView">
|
||||
import { computed, reactive, onBeforeMount, onUnmounted, nextTick } from 'vue';
|
||||
import { computed, watch, reactive, onBeforeMount, onMounted, onUnmounted, nextTick } from 'vue';
|
||||
import { useRoute } 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';
|
||||
|
||||
const route = useRoute();
|
||||
const { themeConfig } = storeToRefs(useThemeConfig());
|
||||
const { keepAliveNames } = storeToRefs(useKeepALiveNames());
|
||||
const { keepAliveNames, cachedViews } = storeToRefs(useKeepALiveNames());
|
||||
|
||||
const state: any = reactive({
|
||||
refreshRouterViewKey: null,
|
||||
keepAliveNameList: [],
|
||||
keepAliveNameNewList: [],
|
||||
});
|
||||
// 监听路由的变化,动态赋值给refreshRouterViewKey
|
||||
// onBeforeRouteUpdate((to: any) => {
|
||||
// state.refreshRouterViewKey = decodeURI(to.fullPath);
|
||||
// });
|
||||
|
||||
// 获取组件缓存列表(name值)
|
||||
const getKeepAliveNames = computed(() => {
|
||||
return themeConfig.value.isTagsview ? cachedViews.value : state.keepAliveNameList;
|
||||
});
|
||||
|
||||
// 页面加载前,处理缓存,页面刷新时路由缓存处理
|
||||
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 = route.path;
|
||||
state.refreshRouterViewKey = '';
|
||||
nextTick(() => {
|
||||
state.refreshRouterViewKey = null;
|
||||
state.refreshRouterViewKey = path;
|
||||
state.keepAliveNameList = keepAliveNames.value;
|
||||
});
|
||||
});
|
||||
});
|
||||
// 页面加载时
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
if (themeConfig.value.isCacheTagsView) {
|
||||
let tagsViewArr: any = getTagViews() || [];
|
||||
cachedViews.value = tagsViewArr.filter((item: any) => item?.isKeepAlive).map((item: any) => item.name as string);
|
||||
}
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
// 监听路由变化,防止 tagsView 多标签时,切换动画消失
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
() => {
|
||||
state.refreshRouterViewKey = decodeURI(route.fullPath);
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
// 设置主界面切换动画
|
||||
const setTransitionName = computed(() => {
|
||||
return themeConfig.value.animation;
|
||||
@@ -1,12 +1,12 @@
|
||||
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router';
|
||||
import NProgress from 'nprogress';
|
||||
import 'nprogress/nprogress.css';
|
||||
import { getSession, clearSession } from '@/common/utils/storage';
|
||||
import { clearSession, getToken } from '@/common/utils/storage';
|
||||
import { templateResolve } from '@/common/utils/string';
|
||||
import { NextLoading } from '@/common/utils/loading';
|
||||
import { dynamicRoutes, staticRoutes, pathMatch } from './route';
|
||||
import openApi from '@/common/openApi';
|
||||
import sockets from '@/common/sockets';
|
||||
import syssocket from '@/common/syssocket';
|
||||
import pinia from '@/store/index';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
import { useUserInfo } from '@/store/userInfo';
|
||||
@@ -18,7 +18,7 @@ import { useKeepALiveNames } from '@/store/keepAliveNames';
|
||||
* @method import.meta.glob
|
||||
* @link 参考:https://cn.vitejs.dev/guide/features.html#json
|
||||
*/
|
||||
const viewsModules: any = import.meta.glob(['../views/**/*.{vue,tsx}', '!../views/layout/**/*.{vue,tsx}']);
|
||||
const viewsModules: any = import.meta.glob(['../views/**/*.{vue,tsx}']);
|
||||
const dynamicViewsModules: Record<string, Function> = Object.assign({}, { ...viewsModules });
|
||||
|
||||
// 添加静态路由
|
||||
@@ -29,8 +29,7 @@ const router = createRouter({
|
||||
|
||||
// 前端控制路由:初始化方法,防止刷新时丢失
|
||||
export function initAllFun() {
|
||||
NextLoading.start(); // 界面 loading 动画开始执行
|
||||
const token = getSession('token'); // 获取浏览器缓存 token 值
|
||||
const token = getToken(); // 获取浏览器缓存 token 值
|
||||
if (!token) {
|
||||
// 无 token 停止执行下一步
|
||||
return false;
|
||||
@@ -38,18 +37,15 @@ export function initAllFun() {
|
||||
useUserInfo().setUserInfo({});
|
||||
router.addRoute(pathMatch); // 添加404界面
|
||||
resetRoute(); // 删除/重置路由
|
||||
// 添加动态路由
|
||||
setFilterRouteEnd().forEach((route: any) => {
|
||||
router.addRoute(route as unknown as RouteRecordRaw);
|
||||
});
|
||||
router.addRoute(dynamicRoutes[0]);
|
||||
|
||||
// 过滤权限菜单
|
||||
useRoutesList().setRoutesList(setFilterMenuFun(dynamicRoutes[0].children, useUserInfo().userInfo.menus));
|
||||
useRoutesList().setRoutesList(dynamicRoutes[0].children);
|
||||
}
|
||||
|
||||
// 后端控制路由:执行路由数据初始化
|
||||
export async function initBackEndControlRoutesFun() {
|
||||
NextLoading.start(); // 界面 loading 动画开始执行
|
||||
const token = getSession('token'); // 获取浏览器缓存 token 值
|
||||
const token = getToken(); // 获取浏览器缓存 token 值
|
||||
if (!token) {
|
||||
// 无 token 停止执行下一步
|
||||
return false;
|
||||
@@ -57,14 +53,25 @@ export async function initBackEndControlRoutesFun() {
|
||||
useUserInfo().setUserInfo({});
|
||||
// 获取路由
|
||||
let menuRoute = await getBackEndControlRoutes();
|
||||
dynamicRoutes[0].children = backEndRouterConverter(menuRoute); // 处理路由(component)
|
||||
|
||||
const cacheList: Array<string> = [];
|
||||
// 处理路由(component)
|
||||
dynamicRoutes[0].children = backEndRouterConverter(menuRoute, (router: any) => {
|
||||
// 可能为false时不存在isKeepAlive属性
|
||||
if (!router.meta.isKeepAlive) {
|
||||
router.meta.isKeepAlive = false;
|
||||
}
|
||||
if (router.meta.isKeepAlive) {
|
||||
cacheList.push(router.name);
|
||||
}
|
||||
});
|
||||
useKeepALiveNames().setCacheKeepAlive(cacheList);
|
||||
|
||||
// 添加404界面
|
||||
router.addRoute(pathMatch);
|
||||
resetRoute(); // 删除/重置路由
|
||||
// 添加动态路由
|
||||
formatTwoStageRoutes(formatFlatteningRoutes(dynamicRoutes)).forEach((route: any) => {
|
||||
router.addRoute(route as unknown as RouteRecordRaw);
|
||||
});
|
||||
router.addRoute(dynamicRoutes[0] as unknown as RouteRecordRaw);
|
||||
|
||||
useRoutesList().setRoutesList(dynamicRoutes[0].children);
|
||||
}
|
||||
|
||||
@@ -81,8 +88,10 @@ export async function getBackEndControlRoutes() {
|
||||
}
|
||||
}
|
||||
|
||||
type RouterConvCallbackFunc = (router: any) => void;
|
||||
|
||||
// 后端控制路由,后端返回路由 转换为vue route
|
||||
export function backEndRouterConverter(routes: any, parentPath: string = '/') {
|
||||
export function backEndRouterConverter(routes: any, callbackFunc: RouterConvCallbackFunc = null as any, parentPath: string = '/') {
|
||||
if (!routes) return;
|
||||
return routes.map((item: any) => {
|
||||
if (!item.meta) {
|
||||
@@ -117,7 +126,9 @@ export function backEndRouterConverter(routes: any, parentPath: string = '/') {
|
||||
item.redirect = item.meta.redirect;
|
||||
delete item.meta['redirect'];
|
||||
}
|
||||
item.children && backEndRouterConverter(item.children, item.path);
|
||||
// 存在回调,则执行回调
|
||||
callbackFunc && callbackFunc(item);
|
||||
item.children && backEndRouterConverter(item.children, callbackFunc, item.path);
|
||||
return item;
|
||||
});
|
||||
}
|
||||
@@ -143,86 +154,6 @@ export function dynamicImport(dynamicViewsModules: Record<string, Function>, com
|
||||
}
|
||||
}
|
||||
|
||||
// 多级嵌套数组处理成一维数组
|
||||
export function formatFlatteningRoutes(arr: any) {
|
||||
if (arr.length <= 0) return false;
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
if (arr[i].children) {
|
||||
arr = arr.slice(0, i + 1).concat(arr[i].children, arr.slice(i + 1));
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
// 多级嵌套数组处理后的一维数组,再处理成 `定义动态路由` 的格式
|
||||
// 只保留二级:也就是二级以上全部处理成只有二级,keep-alive 支持二级缓存
|
||||
// isKeepAlive 处理 `name` 值,进行缓存。顶级关闭,全部不缓存
|
||||
export function formatTwoStageRoutes(arr: any) {
|
||||
if (arr.length <= 0) return false;
|
||||
const newArr: any = [];
|
||||
const cacheList: Array<string> = [];
|
||||
arr.forEach((v: any) => {
|
||||
if (v.path === '/') {
|
||||
newArr.push({ component: v.component, name: v.name, path: v.path, redirect: v.redirect, meta: v.meta, children: [] });
|
||||
} else {
|
||||
newArr[0].children.push({ ...v });
|
||||
if (newArr[0].meta.isKeepAlive && v.meta.isKeepAlive) {
|
||||
cacheList.push(v.name);
|
||||
}
|
||||
}
|
||||
});
|
||||
useKeepALiveNames().setCacheKeepAlive(cacheList);
|
||||
return newArr;
|
||||
}
|
||||
|
||||
// 判断路由code 是否包含当前登录用户menus字段中,menus为字符串code数组
|
||||
export function hasAnth(menus: any, route: any) {
|
||||
if (route.meta && route.meta.code) {
|
||||
return menus.includes(route.meta.code);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 递归过滤有权限的路由
|
||||
export function setFilterMenuFun(routes: any, menus: any) {
|
||||
const menu: any = [];
|
||||
routes.forEach((route: any) => {
|
||||
const item = { ...route };
|
||||
if (hasAnth(menus, item)) {
|
||||
if (item.children) {
|
||||
item.children = setFilterMenuFun(item.children, menus);
|
||||
}
|
||||
menu.push(item);
|
||||
}
|
||||
});
|
||||
return menu;
|
||||
}
|
||||
|
||||
// 获取当前用户的权限去比对路由表,用于动态路由的添加
|
||||
export function setFilterRoute(chil: any) {
|
||||
let filterRoute: any = [];
|
||||
chil.forEach((route: any) => {
|
||||
// 如果路由需要拥有指定code才可访问,则校验该用户菜单是否存在该code
|
||||
if (route.meta.code) {
|
||||
useUserInfo().userInfo.menus.forEach((m: any) => {
|
||||
if (route.meta.code == m) {
|
||||
filterRoute.push({ ...route });
|
||||
}
|
||||
});
|
||||
} else {
|
||||
filterRoute.push({ ...route });
|
||||
}
|
||||
});
|
||||
return filterRoute;
|
||||
}
|
||||
|
||||
// 比对后的路由表,进行重新赋值
|
||||
export function setFilterRouteEnd() {
|
||||
let filterRouteEnd: any = formatTwoStageRoutes(formatFlatteningRoutes(dynamicRoutes));
|
||||
filterRouteEnd[0].children = setFilterRoute(filterRouteEnd[0].children);
|
||||
return filterRouteEnd;
|
||||
}
|
||||
|
||||
// 删除/重置路由
|
||||
export function resetRoute() {
|
||||
useRoutesList().routesList.forEach((route: any) => {
|
||||
@@ -232,18 +163,22 @@ export function resetRoute() {
|
||||
}
|
||||
|
||||
export async function initRouter() {
|
||||
// 初始化方法执行
|
||||
const { isRequestRoutes } = useThemeConfig(pinia).themeConfig;
|
||||
if (!isRequestRoutes) {
|
||||
// 未开启后端控制路由
|
||||
initAllFun();
|
||||
} else if (isRequestRoutes) {
|
||||
// 后端控制路由,isRequestRoutes 为 true,则开启后端控制路由
|
||||
await initBackEndControlRoutesFun();
|
||||
NextLoading.start(); // 界面 loading 动画开始执行
|
||||
try {
|
||||
// 初始化方法执行
|
||||
const { isRequestRoutes } = useThemeConfig(pinia).themeConfig;
|
||||
if (!isRequestRoutes) {
|
||||
// 未开启后端控制路由
|
||||
initAllFun();
|
||||
} else if (isRequestRoutes) {
|
||||
// 后端控制路由,isRequestRoutes 为 true,则开启后端控制路由
|
||||
await initBackEndControlRoutesFun();
|
||||
}
|
||||
} finally {
|
||||
NextLoading.done();
|
||||
}
|
||||
}
|
||||
|
||||
let SysWs: any;
|
||||
let loadRouter = false;
|
||||
|
||||
// 路由加载前
|
||||
@@ -252,11 +187,11 @@ router.beforeEach(async (to, from, next) => {
|
||||
if (to.meta.title) NProgress.start();
|
||||
|
||||
// 如果有标题参数,则再原标题后加上参数来区别
|
||||
if (to.meta.titleRename) {
|
||||
if (to.meta.titleRename && to.meta.title) {
|
||||
to.meta.title = templateResolve(to.meta.title as string, to.query);
|
||||
}
|
||||
|
||||
const token = getSession('token');
|
||||
const token = getToken();
|
||||
if ((to.path === '/login' || to.path == '/oauth2/callback') && !token) {
|
||||
next();
|
||||
NProgress.done();
|
||||
@@ -268,10 +203,7 @@ router.beforeEach(async (to, from, next) => {
|
||||
resetRoute();
|
||||
NProgress.done();
|
||||
|
||||
if (SysWs) {
|
||||
SysWs.close();
|
||||
SysWs = null;
|
||||
}
|
||||
syssocket.destory();
|
||||
return;
|
||||
}
|
||||
if (token && to.path === '/login') {
|
||||
@@ -281,9 +213,10 @@ router.beforeEach(async (to, from, next) => {
|
||||
}
|
||||
|
||||
// 终端不需要连接系统websocket消息
|
||||
if (!SysWs && to.path != '/machine/terminal') {
|
||||
SysWs = sockets.sysMsgSocket();
|
||||
if (to.path != '/machine/terminal') {
|
||||
syssocket.init();
|
||||
}
|
||||
|
||||
// 不存在路由(避免刷新页面找不到路由)并且未加载过(避免token过期,导致获取权限接口报权限不足,无限获取),则重新初始化路由
|
||||
if (useRoutesList().routesList.length == 0 && !loadRouter) {
|
||||
await initRouter();
|
||||
@@ -297,7 +230,6 @@ router.beforeEach(async (to, from, next) => {
|
||||
// 路由加载后
|
||||
router.afterEach(() => {
|
||||
NProgress.done();
|
||||
NextLoading.done();
|
||||
});
|
||||
|
||||
// 导出路由
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RouteRecordRaw } from 'vue-router';
|
||||
import Layout from '@/views/layout/index.vue';
|
||||
import Layout from '@/layout/index.vue';
|
||||
|
||||
// 定义动态路由
|
||||
export const dynamicRoutes = [
|
||||
|
||||
@@ -18,14 +18,16 @@ export const useKeepALiveNames = defineStore('keepALiveNames', {
|
||||
this.keepAliveNames = data;
|
||||
},
|
||||
async addCachedView(view: any) {
|
||||
if (view.meta.isKeepAlive) this.cachedViews?.push(view.name);
|
||||
if (view.isKeepAlive) {
|
||||
this.cachedViews?.push(view.name);
|
||||
}
|
||||
},
|
||||
async delCachedView(view: any) {
|
||||
const index = this.cachedViews.indexOf(view.name);
|
||||
index > -1 && this.cachedViews.splice(index, 1);
|
||||
},
|
||||
async delOthersCachedViews(view: any) {
|
||||
if (view.meta.isKeepAlive) this.cachedViews = [view.name];
|
||||
if (view.isKeepAlive) this.cachedViews = [view.name];
|
||||
else this.cachedViews = [];
|
||||
},
|
||||
async delAllCachedViews() {
|
||||
|
||||
25
mayfly_go_web/src/store/tagsViews.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { getNowUrl } from '@/common/utils/url';
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
/**
|
||||
* tags view
|
||||
*/
|
||||
export const useTagsViews = defineStore('tagsViews', {
|
||||
state: (): TagsViewsState => ({
|
||||
tagsViews: [],
|
||||
}),
|
||||
actions: {
|
||||
setTagsViews(data: Array<TagsView>) {
|
||||
this.tagsViews = data;
|
||||
},
|
||||
// 设置当前页面的tags view title
|
||||
setNowTitle(title: string) {
|
||||
this.tagsViews.forEach((item) => {
|
||||
// console.log(getNowUrl(), item.path);
|
||||
if (item.path == getNowUrl()) {
|
||||
item.title = title;
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,4 +1,6 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { dateFormat2 } from '@/common/utils/date';
|
||||
import { useUserInfo } from '@/store/userInfo';
|
||||
|
||||
export const useThemeConfig = defineStore('themeConfig', {
|
||||
state: (): ThemeConfigState => ({
|
||||
@@ -88,9 +90,9 @@ export const useThemeConfig = defineStore('themeConfig', {
|
||||
// 是否开启色弱模式
|
||||
isInvert: false,
|
||||
// 是否开启水印
|
||||
isWartermark: false,
|
||||
// 水印文案
|
||||
wartermarkText: 'mayfly',
|
||||
isWatermark: false,
|
||||
// 水印文案数组,0->用户信息 1->当前时间 2->额外信息
|
||||
watermarkText: ['', '', ''],
|
||||
|
||||
/* 其它设置
|
||||
------------------------------- */
|
||||
@@ -133,6 +135,10 @@ export const useThemeConfig = defineStore('themeConfig', {
|
||||
globalI18n: 'zh-cn',
|
||||
// 默认全局组件大小,可选值"<|large|default|small>",默认 ''
|
||||
globalComponentSize: '',
|
||||
|
||||
/** 全局设置 */
|
||||
// 默认列表页的分页大小
|
||||
defaultListPageSize: 15,
|
||||
},
|
||||
}),
|
||||
actions: {
|
||||
@@ -140,5 +146,40 @@ export const useThemeConfig = defineStore('themeConfig', {
|
||||
setThemeConfig(data: ThemeConfigState) {
|
||||
this.themeConfig = data.themeConfig;
|
||||
},
|
||||
// 切换暗模式
|
||||
switchDark(isDark: boolean) {
|
||||
this.themeConfig.isDark = isDark;
|
||||
const body = document.documentElement as HTMLElement;
|
||||
if (isDark) {
|
||||
body.setAttribute('class', 'dark');
|
||||
this.themeConfig.editorTheme = 'vs-dark';
|
||||
} else {
|
||||
body.setAttribute('class', '');
|
||||
this.themeConfig.editorTheme = 'vs';
|
||||
}
|
||||
},
|
||||
// 设置水印配置信息
|
||||
setWatermarkConfig(useWatermarkConfig: any) {
|
||||
this.themeConfig.watermarkText = [];
|
||||
this.themeConfig.isWatermark = useWatermarkConfig.isUse;
|
||||
if (!useWatermarkConfig.isUse) {
|
||||
return;
|
||||
}
|
||||
// 索引2为用户自定义水印信息
|
||||
this.themeConfig.watermarkText[2] = useWatermarkConfig.content;
|
||||
},
|
||||
// 设置水印用户信息
|
||||
setWatermarkUser(del: boolean = false) {
|
||||
const userinfo = useUserInfo().userInfo;
|
||||
let desc = '';
|
||||
if (!del && userinfo && userinfo.username) {
|
||||
desc = `${userinfo.username}(${userinfo.name})`;
|
||||
}
|
||||
this.themeConfig.watermarkText[0] = desc;
|
||||
},
|
||||
// 设置水印时间为当前时间
|
||||
setWatermarkNowTime() {
|
||||
this.themeConfig.watermarkText[1] = dateFormat2('yyyy-MM-dd HH:mm:ss', new Date());
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { getSession } from '@/common/utils/storage';
|
||||
import { getUser } from '@/common/utils/storage';
|
||||
|
||||
export const useUserInfo = defineStore('userInfo', {
|
||||
state: (): UserInfoState => ({
|
||||
@@ -8,7 +8,7 @@ export const useUserInfo = defineStore('userInfo', {
|
||||
actions: {
|
||||
// 设置用户信息
|
||||
async setUserInfo(data: object) {
|
||||
const ui = getSession('userInfo');
|
||||
const ui = getUser();
|
||||
if (ui) {
|
||||
this.userInfo = ui;
|
||||
} else {
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
--color-seting-main: #e9eef3;
|
||||
--color-seting-aside: #d3dce6;
|
||||
--color-seting-header: #b3c0d1;
|
||||
|
||||
--tagsview3-active-background-color: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
html,
|
||||
|
||||
@@ -83,3 +83,42 @@
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 登录页动画
|
||||
------------------------------- */
|
||||
@keyframes loginLeft {
|
||||
0% {
|
||||
left: -100%;
|
||||
}
|
||||
50%,
|
||||
100% {
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
@keyframes loginTop {
|
||||
0% {
|
||||
top: -100%;
|
||||
}
|
||||
50%,
|
||||
100% {
|
||||
top: 100%;
|
||||
}
|
||||
}
|
||||
@keyframes loginRight {
|
||||
0% {
|
||||
right: -100%;
|
||||
}
|
||||
50%,
|
||||
100% {
|
||||
right: 100%;
|
||||
}
|
||||
}
|
||||
@keyframes loginBottom {
|
||||
0% {
|
||||
bottom: -100%;
|
||||
}
|
||||
50%,
|
||||
100% {
|
||||
bottom: 100%;
|
||||
}
|
||||
}
|
||||
@@ -24,4 +24,6 @@ html.dark {
|
||||
--bg-menuBarActiveColor: var(--next-color-hover-rgba) !important;
|
||||
--bg-columnsMenuBar: var(--next-color-disabled) !important;
|
||||
--bg-columnsMenuBarColor: var(--next-color-bar) !important;
|
||||
|
||||
--tagsview3-active-background-color: var(--next-color-hover);
|
||||
}
|
||||
@@ -335,8 +335,15 @@
|
||||
/* Set padding to ensure the height is 32px */
|
||||
// padding: 6px 12px;
|
||||
background: linear-gradient(90deg, rgb(159, 229, 151), rgb(204, 229, 129));
|
||||
}
|
||||
.el-popper.is-customized .el-popper__arrow::before {
|
||||
}
|
||||
.el-popper.is-customized .el-popper__arrow::before {
|
||||
background: linear-gradient(45deg, #b2e68d, #bce689);
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.el-dialog {
|
||||
border-radius: 6px; /* 设置圆角 */
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); /* 添加轻微阴影效果 */
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
@@ -48,4 +48,4 @@
|
||||
35% {
|
||||
transform: scale3D(0, 0, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
44
mayfly_go_web/src/types/pinia.d.ts
vendored
@@ -1,5 +1,5 @@
|
||||
declare interface UserInfoState<T = any> {
|
||||
userInfo: any;
|
||||
userInfo: T;
|
||||
}
|
||||
|
||||
declare interface ThemeConfigState {
|
||||
@@ -40,8 +40,8 @@ declare interface ThemeConfigState {
|
||||
isDark: boolean;
|
||||
isGrayscale: boolean;
|
||||
isInvert: boolean;
|
||||
isWartermark: boolean;
|
||||
wartermarkText: string;
|
||||
isWatermark: boolean;
|
||||
watermarkText: Array<string>;
|
||||
tagsStyle: string;
|
||||
animation: string;
|
||||
columnsAsideStyle: string;
|
||||
@@ -57,13 +57,45 @@ declare interface ThemeConfigState {
|
||||
terminalFontSize: number;
|
||||
terminalFontWeight: string | any;
|
||||
editorTheme: string;
|
||||
|
||||
defaultListPageSize: number;
|
||||
};
|
||||
}
|
||||
|
||||
declare interface TagsView {
|
||||
/**
|
||||
* 路径
|
||||
*/
|
||||
path: string;
|
||||
|
||||
/**
|
||||
* 标题
|
||||
*/
|
||||
title: string;
|
||||
|
||||
/**
|
||||
* router name
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* router query
|
||||
*/
|
||||
query: any;
|
||||
|
||||
/**
|
||||
* 图标
|
||||
*/
|
||||
icon: string;
|
||||
|
||||
isAffix: boolean;
|
||||
isKeepAlive: boolean;
|
||||
isHide?: boolean;
|
||||
}
|
||||
|
||||
// TagsView 路由列表
|
||||
declare interface TagsViewRoutesState<T = any> {
|
||||
tagsViewRoutes: T[];
|
||||
isTagsViewCurrenFull: Boolean;
|
||||
declare interface TagsViewsState<> {
|
||||
tagsViews: TagsView[];
|
||||
}
|
||||
|
||||
// 路由列表
|
||||
|
||||
11
mayfly_go_web/src/types/shim.d.ts
vendored
@@ -1,14 +1,5 @@
|
||||
/* eslint-disable */
|
||||
import {IDisposable} from 'monaco-editor';
|
||||
declare global {
|
||||
interface Window {
|
||||
completionItemProvider?: IDisposable | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// 申明外部 npm 插件模块
|
||||
declare module 'sql-formatter';
|
||||
declare module 'jsoneditor';
|
||||
declare module 'asciinema-player';
|
||||
declare module 'monaco-editor';
|
||||
declare module 'vue-grid-layout';
|
||||
declare module 'vue-grid-layout';
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<img src="@/assets/image/401.png" />
|
||||
<img src="@/assets/image/401.svg" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { useRouter } from 'vue-router';
|
||||
import { clearSession } from '@/common/utils/storage.ts';
|
||||
import { clearSession } from '@/common/utils/storage';
|
||||
export default {
|
||||
name: '401',
|
||||
setup() {
|
||||
@@ -39,7 +39,7 @@ export default {
|
||||
<style scoped lang="scss">
|
||||
.error {
|
||||
height: 100%;
|
||||
background-color: white;
|
||||
background-color: var(--bg-main-color);
|
||||
display: flex;
|
||||
.error-flex {
|
||||
margin: auto;
|
||||
@@ -64,7 +64,7 @@ export default {
|
||||
}
|
||||
.left-item-title {
|
||||
font-size: 20px;
|
||||
color: #333333;
|
||||
// color: #333333;
|
||||
margin: 15px 0 5px 0;
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<img src="@/assets/image/404.png" />
|
||||
<img src="@/assets/image/404.svg" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -37,7 +37,7 @@ export default {
|
||||
<style scoped lang="scss">
|
||||
.error {
|
||||
height: 100%;
|
||||
background-color: white;
|
||||
background-color: var(--bg-main-color);
|
||||
display: flex;
|
||||
.error-flex {
|
||||
margin: auto;
|
||||
@@ -62,7 +62,7 @@ export default {
|
||||
}
|
||||
.left-item-title {
|
||||
font-size: 20px;
|
||||
color: #333333;
|
||||
// color: #333333;
|
||||
margin: 15px 0 5px 0;
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
<template>
|
||||
<transition name="el-zoom-in-center">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="el-dropdown__popper el-popper is-light is-pure custom-contextmenu"
|
||||
role="tooltip"
|
||||
data-popper-placement="bottom"
|
||||
:style="`top: ${dropdowns.y + 5}px;left: ${dropdowns.x}px;`"
|
||||
:key="Math.random()"
|
||||
v-show="state.isShow"
|
||||
>
|
||||
<ul class="el-dropdown-menu">
|
||||
<template v-for="(v, k) in state.dropdownList">
|
||||
<li
|
||||
class="el-dropdown-menu__item"
|
||||
aria-disabled="false"
|
||||
tabindex="-1"
|
||||
:key="k"
|
||||
v-if="!v.affix"
|
||||
@click="onCurrentContextmenuClick(v.contextMenuClickId)"
|
||||
>
|
||||
<SvgIcon :name="v.icon" />
|
||||
<span>{{ v.txt }}</span>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
<div class="el-popper__arrow" :style="{ left: `${state.arrowLeft}px` }"></div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="layoutTagsViewContextmenu">
|
||||
import { computed, reactive, onMounted, onUnmounted, watch } from 'vue';
|
||||
|
||||
// 定义父组件传过来的值
|
||||
const props = defineProps({
|
||||
dropdown: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 定义子组件向父组件传值/事件
|
||||
const emit = defineEmits(['currentContextmenuClick']);
|
||||
|
||||
// 定义变量内容
|
||||
const state = reactive({
|
||||
isShow: false,
|
||||
dropdownList: [
|
||||
{ contextMenuClickId: 0, txt: '刷新', affix: false, icon: 'RefreshRight' },
|
||||
{ contextMenuClickId: 1, txt: '关闭', affix: false, icon: 'Close' },
|
||||
{ contextMenuClickId: 2, txt: '关闭其他', affix: false, icon: 'CircleClose' },
|
||||
{ contextMenuClickId: 3, txt: '关闭所有', affix: false, icon: 'FolderDelete' },
|
||||
{
|
||||
contextMenuClickId: 4,
|
||||
txt: '当前页全屏',
|
||||
affix: false,
|
||||
icon: 'full-screen',
|
||||
},
|
||||
],
|
||||
item: {} as any,
|
||||
arrowLeft: 10,
|
||||
});
|
||||
|
||||
// 父级传过来的坐标 x,y 值
|
||||
const dropdowns = computed(() => {
|
||||
// 117 为 `Dropdown 下拉菜单` 的宽度
|
||||
if (props.dropdown.x + 117 > document.documentElement.clientWidth) {
|
||||
return {
|
||||
x: document.documentElement.clientWidth - 117 - 5,
|
||||
y: props.dropdown.y,
|
||||
};
|
||||
} else {
|
||||
return props.dropdown;
|
||||
}
|
||||
});
|
||||
// 当前项菜单点击
|
||||
const onCurrentContextmenuClick = (contextMenuClickId: number) => {
|
||||
emit('currentContextmenuClick', { id: contextMenuClickId, path: state.item.fullPath });
|
||||
};
|
||||
// 打开右键菜单:判断是否固定,固定则不显示关闭按钮
|
||||
const openContextmenu = (item: any) => {
|
||||
state.item = item;
|
||||
item.meta?.isAffix ? (state.dropdownList[1].affix = true) : (state.dropdownList[1].affix = false);
|
||||
closeContextmenu();
|
||||
setTimeout(() => {
|
||||
state.isShow = true;
|
||||
}, 10);
|
||||
};
|
||||
// 关闭右键菜单
|
||||
const closeContextmenu = () => {
|
||||
state.isShow = false;
|
||||
};
|
||||
// 监听页面监听进行右键菜单的关闭
|
||||
onMounted(() => {
|
||||
document.body.addEventListener('click', closeContextmenu);
|
||||
});
|
||||
// 页面卸载时,移除右键菜单监听事件
|
||||
onUnmounted(() => {
|
||||
document.body.removeEventListener('click', closeContextmenu);
|
||||
});
|
||||
// 监听下拉菜单位置
|
||||
watch(
|
||||
() => props.dropdown,
|
||||
({ x }) => {
|
||||
if (x + 117 > document.documentElement.clientWidth) state.arrowLeft = 117 - (document.documentElement.clientWidth - x);
|
||||
else state.arrowLeft = 10;
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
|
||||
// 暴露变量
|
||||
defineExpose({
|
||||
openContextmenu,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.custom-contextmenu {
|
||||
transform-origin: center top;
|
||||
z-index: 2190;
|
||||
position: fixed;
|
||||
.el-dropdown-menu__item {
|
||||
font-size: 12px !important;
|
||||
white-space: nowrap;
|
||||
i {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -132,16 +132,18 @@ import { nextTick, onMounted, ref, toRefs, reactive, computed } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { initRouter } from '@/router/index';
|
||||
import { getSession, setSession, setUserInfo2Session, setUseWatermark2Session } from '@/common/utils/storage';
|
||||
import { saveToken, saveUser } from '@/common/utils/storage';
|
||||
import { formatAxis } from '@/common/utils/format';
|
||||
import openApi from '@/common/openApi';
|
||||
import { RsaEncrypt } from '@/common/rsa';
|
||||
import { getAccountLoginSecurity, getLdapEnabled, useWartermark } from '@/common/sysconfig';
|
||||
import { getAccountLoginSecurity, getLdapEnabled } from '@/common/sysconfig';
|
||||
import { letterAvatar } from '@/common/utils/string';
|
||||
import { useUserInfo } from '@/store/userInfo';
|
||||
import QrcodeVue from 'qrcode.vue';
|
||||
import { personApi } from '@/views/personal/api';
|
||||
import { AccountUsernamePattern } from '@/common/pattern';
|
||||
import { getToken } from '@/common/utils/storage';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
|
||||
const rules = {
|
||||
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
||||
@@ -149,6 +151,9 @@ const rules = {
|
||||
captcha: [{ required: true, message: '请输入验证码', trigger: 'blur' }],
|
||||
};
|
||||
|
||||
// 定义变量内容
|
||||
const storesThemeConfig = useThemeConfig();
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const loginFormRef: any = ref(null);
|
||||
@@ -159,7 +164,7 @@ const baseInfoFormRef: any = ref(null);
|
||||
|
||||
const state = reactive({
|
||||
accountLoginSecurity: {
|
||||
useCaptcha: true,
|
||||
useCaptcha: false,
|
||||
useOtp: false,
|
||||
loginFailCount: 5,
|
||||
loginFailMin: 10,
|
||||
@@ -354,12 +359,12 @@ const loginResDeal = (loginRes: any) => {
|
||||
};
|
||||
|
||||
// 存储用户信息到浏览器缓存
|
||||
setUserInfo2Session(userInfos);
|
||||
saveUser(userInfos);
|
||||
// 1、请注意执行顺序(存储用户信息到vuex)
|
||||
useUserInfo().setUserInfo(userInfos);
|
||||
|
||||
const token = loginRes.token;
|
||||
// 如果不需要otp校验,则该token即为accessToken,否则为otp校验token
|
||||
// 如果不需要 otp校验,则该token即为accessToken,否则为otp校验token
|
||||
if (loginRes.otp == -1) {
|
||||
signInSuccess(token);
|
||||
return;
|
||||
@@ -376,10 +381,11 @@ const loginResDeal = (loginRes: any) => {
|
||||
// 登录成功后的跳转
|
||||
const signInSuccess = async (accessToken: string = '') => {
|
||||
if (!accessToken) {
|
||||
accessToken = getSession('token');
|
||||
accessToken = getToken();
|
||||
}
|
||||
// 存储 token 到浏览器缓存
|
||||
setSession('token', accessToken);
|
||||
saveToken(accessToken);
|
||||
|
||||
// 初始化路由
|
||||
await initRouter();
|
||||
|
||||
@@ -404,9 +410,8 @@ const toIndex = async () => {
|
||||
// 关闭 loading
|
||||
state.loading.signIn = true;
|
||||
ElMessage.success(`${currentTimeInfo},欢迎回来!`);
|
||||
if (await useWartermark()) {
|
||||
setUseWatermark2Session(true);
|
||||
}
|
||||
// 水印设置用户信息
|
||||
storesThemeConfig.setWatermarkUser();
|
||||
}, 300);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,70 +1,77 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<div class="login-logo">
|
||||
<span>{{ themeConfig.globalViceTitle }}</span>
|
||||
<div class="login-container flex">
|
||||
<div class="login-left">
|
||||
<div class="login-left-logo">
|
||||
<img :src="logoMini" />
|
||||
<div class="login-left-logo-text">
|
||||
<span>mayfly-go</span>
|
||||
<!-- <span class="login-left-logo-text-msg">mayfly-go</span> -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="login-left-img">
|
||||
<img :src="loginBgImg" />
|
||||
</div>
|
||||
<img :src="loginBgSplitImg" class="login-left-waves" />
|
||||
</div>
|
||||
<div class="login-content" :class="{ 'login-content-mobile': tabsActiveName === 'mobile' }">
|
||||
<div class="login-content-main">
|
||||
<h4 class="login-content-title">mayfly-go</h4>
|
||||
<el-tabs v-model="tabsActiveName" @tab-click="onTabsClick">
|
||||
<el-tab-pane label="账号密码登录" name="account" :disabled="tabsActiveName === 'account'">
|
||||
<transition name="el-zoom-in-center">
|
||||
<Account v-show="isTabPaneShow" ref="loginForm" />
|
||||
</transition>
|
||||
</el-tab-pane>
|
||||
<!-- <el-tab-pane label="手机号登录" name="mobile" :disabled="tabsActiveName === 'mobile'">
|
||||
<transition name="el-zoom-in-center">
|
||||
<Mobile v-show="!isTabPaneShow" />
|
||||
</transition>
|
||||
</el-tab-pane> -->
|
||||
</el-tabs>
|
||||
<div class="mt20" v-show="oauth2LoginConfig.enable">
|
||||
<el-button link size="small">第三方登录: </el-button>
|
||||
<el-tooltip :content="oauth2LoginConfig.name" placement="top-start">
|
||||
<el-button link size="small" type="primary" @click="oauth2Login">
|
||||
<el-icon :size="18">
|
||||
<Link />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<div class="login-right flex">
|
||||
<div class="login-right-warp flex-margin">
|
||||
<span class="login-right-warp-one"></span>
|
||||
<span class="login-right-warp-two"></span>
|
||||
<div class="login-right-warp-mian">
|
||||
<div class="login-right-warp-main-title">mayfly-go</div>
|
||||
<div class="login-right-warp-main-form">
|
||||
<div v-if="!state.isScan">
|
||||
<el-tabs v-model="state.tabsActiveName">
|
||||
<el-tab-pane label="账号密码登录" name="account">
|
||||
<Account ref="loginForm" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
<div class="mt20" v-show="state.oauth2LoginConfig.enable">
|
||||
<el-button link size="small">第三方登录: </el-button>
|
||||
<el-tooltip :content="state.oauth2LoginConfig.name" placement="top-start">
|
||||
<el-button link size="small" type="primary" @click="oauth2Login">
|
||||
<el-icon :size="18">
|
||||
<Link />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="login-copyright">
|
||||
<div class="mb5 login-copyright-company">mayfly</div>
|
||||
<div class="login-copyright-msg">mayfly</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, reactive, onMounted, h, ref } from 'vue';
|
||||
import Account from '@/views/login/component/AccountLogin.vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
<script setup lang="ts" name="loginIndex">
|
||||
import { ref, defineAsyncComponent, onMounted, reactive } from 'vue';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
import logoMini from '@/assets/image/logo.svg';
|
||||
import loginBgImg from '@/assets/image/login-bg-main.svg';
|
||||
import loginBgSplitImg from '@/assets/image/login-bg-split.svg';
|
||||
import openApi from '@/common/openApi';
|
||||
import config from '@/common/config';
|
||||
|
||||
const { themeConfig } = storeToRefs(useThemeConfig());
|
||||
// 引入组件
|
||||
const Account = defineAsyncComponent(() => import('./component/AccountLogin.vue'));
|
||||
|
||||
const loginForm = ref<{ loginResDeal: (data: any) => void } | null>(null);
|
||||
|
||||
// 定义变量内容
|
||||
const storesThemeConfig = useThemeConfig();
|
||||
|
||||
const state = reactive({
|
||||
tabsActiveName: 'account',
|
||||
isTabPaneShow: true,
|
||||
isScan: false,
|
||||
oauth2LoginConfig: {
|
||||
name: 'OAuth2登录',
|
||||
enable: false,
|
||||
},
|
||||
});
|
||||
|
||||
const loginForm = ref<{ loginResDeal: (data: any) => void } | null>(null);
|
||||
|
||||
const { isTabPaneShow, tabsActiveName, oauth2LoginConfig: oauth2LoginConfig } = toRefs(state);
|
||||
|
||||
// 切换密码、手机登录
|
||||
const onTabsClick = () => {
|
||||
state.isTabPaneShow = !state.isTabPaneShow;
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
storesThemeConfig.setWatermarkUser(true);
|
||||
state.oauth2LoginConfig = await openApi.oauth2LoginConfig();
|
||||
});
|
||||
|
||||
@@ -94,76 +101,178 @@ const oauth2Login = () => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.login-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url('@/assets/image/bg-login.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
|
||||
.login-logo {
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
left: 50%;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 20px;
|
||||
color: var(--el-color-primary);
|
||||
letter-spacing: 2px;
|
||||
width: 90%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.login-content {
|
||||
width: 500px;
|
||||
padding: 20px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%) translate3d(0, 0, 0);
|
||||
background-color: rgba(255, 255, 255, 0.99);
|
||||
box-shadow: 0 2px 12px 0 var(--el-color-primary-light-5);
|
||||
border-radius: 4px;
|
||||
transition: height 0.2s linear;
|
||||
height: 490px;
|
||||
overflow: hidden;
|
||||
z-index: 1;
|
||||
|
||||
.login-content-main {
|
||||
margin: 0 auto;
|
||||
width: 80%;
|
||||
|
||||
.login-content-title {
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
font-size: 22px;
|
||||
text-align: center;
|
||||
letter-spacing: 4px;
|
||||
margin: 15px 0 30px;
|
||||
white-space: nowrap;
|
||||
background: var(--bg-main-color);
|
||||
.login-left {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
background-color: rgba(211, 239, 255, 1);
|
||||
margin-right: 100px;
|
||||
.login-left-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
left: 80px;
|
||||
z-index: 1;
|
||||
animation: logoAnimation 0.3s ease;
|
||||
img {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
}
|
||||
.login-left-logo-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
span {
|
||||
margin-left: 10px;
|
||||
font-size: 28px;
|
||||
color: #26a59a;
|
||||
}
|
||||
.login-left-logo-text-msg {
|
||||
font-size: 12px;
|
||||
color: #32a99e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.login-content-mobile {
|
||||
height: 418px;
|
||||
}
|
||||
|
||||
.login-copyright {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
bottom: 30px;
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
|
||||
.login-copyright-company {
|
||||
white-space: nowrap;
|
||||
.login-left-img {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 100%;
|
||||
height: 52%;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
animation: error-num 0.6s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.login-copyright-msg {
|
||||
@extend .login-copyright-company;
|
||||
.login-left-waves {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -100px;
|
||||
}
|
||||
}
|
||||
.login-right {
|
||||
width: 700px;
|
||||
.login-right-warp {
|
||||
border: 1px solid var(--el-color-primary-light-3);
|
||||
border-radius: 3px;
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background-color: var(--bg-main-color);
|
||||
.login-right-warp-one,
|
||||
.login-right-warp-two {
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
.login-right-warp-one {
|
||||
&::before {
|
||||
filter: hue-rotate(0deg);
|
||||
top: 0px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, transparent, var(--el-color-primary));
|
||||
animation: loginLeft 3s linear infinite;
|
||||
}
|
||||
&::after {
|
||||
filter: hue-rotate(60deg);
|
||||
top: -100%;
|
||||
right: 2px;
|
||||
width: 3px;
|
||||
height: 100%;
|
||||
background: linear-gradient(180deg, transparent, var(--el-color-primary));
|
||||
animation: loginTop 3s linear infinite;
|
||||
animation-delay: 0.7s;
|
||||
}
|
||||
}
|
||||
.login-right-warp-two {
|
||||
&::before {
|
||||
filter: hue-rotate(120deg);
|
||||
bottom: 2px;
|
||||
right: -100%;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background: linear-gradient(270deg, transparent, var(--el-color-primary));
|
||||
animation: loginRight 3s linear infinite;
|
||||
animation-delay: 1.4s;
|
||||
}
|
||||
&::after {
|
||||
filter: hue-rotate(300deg);
|
||||
bottom: -100%;
|
||||
left: 0px;
|
||||
width: 3px;
|
||||
height: 100%;
|
||||
background: linear-gradient(360deg, transparent, var(--el-color-primary));
|
||||
animation: loginBottom 3s linear infinite;
|
||||
animation-delay: 2.1s;
|
||||
}
|
||||
}
|
||||
.login-right-warp-mian {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
.login-right-warp-main-title {
|
||||
height: 110px;
|
||||
line-height: 110px;
|
||||
font-size: 27px;
|
||||
text-align: center;
|
||||
letter-spacing: 3px;
|
||||
animation: logoAnimation 0.3s ease;
|
||||
animation-delay: 0.3s;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
.login-right-warp-main-form {
|
||||
flex: 1;
|
||||
padding: 0 50px 50px;
|
||||
.login-content-main-sacn {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all ease 0.3s;
|
||||
color: var(--el-color-primary);
|
||||
&-delta {
|
||||
position: absolute;
|
||||
width: 35px;
|
||||
height: 70px;
|
||||
z-index: 2;
|
||||
top: 2px;
|
||||
right: 21px;
|
||||
background: var(--el-color-white);
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
transition: all ease 0.3s;
|
||||
color: var(--el-color-primary) !important;
|
||||
}
|
||||
i {
|
||||
width: 47px;
|
||||
height: 50px;
|
||||
display: inline-block;
|
||||
font-size: 48px;
|
||||
position: absolute;
|
||||
right: 1px;
|
||||
top: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
45
mayfly_go_web/src/views/ops/component/ResourceTag.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div style="display: inline-flex; justify-content: center; align-items: center; cursor: pointer; vertical-align: middle">
|
||||
<el-popover :show-after="500" @show="getTags" placement="top-start" width="230" trigger="hover">
|
||||
<template #reference>
|
||||
<div>
|
||||
<!-- <el-button type="primary" link size="small">标签</el-button> -->
|
||||
<SvgIcon name="view" :size="16" color="var(--el-color-primary)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-tag effect="plain" v-for="tag in tags" :key="tag" class="ml5" type="success" size="small">{{ tag.tagPath }}</el-tag>
|
||||
</el-popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, toRefs } from 'vue';
|
||||
import { tagApi } from '../tag/api';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
const props = defineProps({
|
||||
resourceCode: {
|
||||
type: [String],
|
||||
required: true,
|
||||
},
|
||||
resourceType: {
|
||||
type: [Number],
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
tags: [] as any,
|
||||
});
|
||||
|
||||
const { tags } = toRefs(state);
|
||||
|
||||
const getTags = async () => {
|
||||
state.tags = await tagApi.getTagResources.request({
|
||||
resourceCode: props.resourceCode,
|
||||
resourceType: props.resourceType,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<div style="display: inline-flex; justify-content: center; align-items: center; cursor: pointer; vertical-align: middle">
|
||||
<el-popover @show="showTagInfo" placement="top-start" title="标签信息" :width="300" trigger="hover">
|
||||
<el-popover :show-after="500" @show="showTagInfo" placement="top-start" title="标签信息" :width="300" trigger="hover">
|
||||
<template #reference>
|
||||
<el-icon>
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
</template>
|
||||
<span v-for="(v, i) in tags" :key="i">
|
||||
<el-tooltip effect="customized" :content="v.remark" placement="top">
|
||||
<el-tooltip :content="v.remark" placement="top">
|
||||
<span class="color-success">{{ v.name }}</span>
|
||||
</el-tooltip>
|
||||
<span v-if="i != state.tags.length - 1" class="color-primary"> / </span>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
ref="treeRef"
|
||||
:style="{ maxHeight: state.height, height: state.height, overflow: 'auto' }"
|
||||
:highlight-current="true"
|
||||
:indent="7"
|
||||
:indent="10"
|
||||
:load="loadNode"
|
||||
:props="treeProps"
|
||||
lazy
|
||||
@@ -21,7 +21,7 @@
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<span>
|
||||
<span v-if="data.type == TagTreeNode.TagPath">
|
||||
<span v-if="data.type.value == TagTreeNode.TagPath">
|
||||
<tag-info :tag-path="data.label" />
|
||||
</span>
|
||||
|
||||
@@ -30,6 +30,8 @@
|
||||
<span class="ml3">
|
||||
<slot name="label" :data="data"> {{ data.label }}</slot>
|
||||
</span>
|
||||
|
||||
<slot :node="node" :data="data" name="suffix"></slot>
|
||||
</span>
|
||||
</template>
|
||||
</el-tree>
|
||||
@@ -40,19 +42,25 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, reactive, ref, watch, toRefs } from 'vue';
|
||||
import { TagTreeNode } from './tag';
|
||||
import { onMounted, reactive, ref, watch, toRefs, onUnmounted } from 'vue';
|
||||
import { NodeType, TagTreeNode } from './tag';
|
||||
import TagInfo from './TagInfo.vue';
|
||||
import Contextmenu from '@/components/contextmenu/index.vue';
|
||||
import { Contextmenu } from '@/components/contextmenu';
|
||||
import { useViewport } from '@/common/use';
|
||||
import { tagApi } from '../tag/api';
|
||||
|
||||
const props = defineProps({
|
||||
height: {
|
||||
type: [Number, String],
|
||||
default: 0,
|
||||
resourceType: {
|
||||
type: [Number],
|
||||
required: true,
|
||||
},
|
||||
tagPathNodeType: {
|
||||
type: [NodeType],
|
||||
required: true,
|
||||
},
|
||||
load: {
|
||||
type: Function,
|
||||
required: true,
|
||||
required: false,
|
||||
},
|
||||
loadContextmenuItems: {
|
||||
type: Function,
|
||||
@@ -70,6 +78,8 @@ const emit = defineEmits(['nodeClick', 'currentContextmenuClick']);
|
||||
const treeRef: any = ref(null);
|
||||
const contextmenuRef = ref();
|
||||
|
||||
const { vh } = useViewport();
|
||||
|
||||
const state = reactive({
|
||||
height: 600 as any,
|
||||
filterText: '',
|
||||
@@ -83,18 +93,18 @@ const state = reactive({
|
||||
const { filterText } = toRefs(state);
|
||||
|
||||
onMounted(async () => {
|
||||
if (!props.height) {
|
||||
setHeight();
|
||||
window.onresize = () => setHeight();
|
||||
} else {
|
||||
state.height = props.height;
|
||||
}
|
||||
setHeight();
|
||||
window.addEventListener('resize', setHeight);
|
||||
});
|
||||
|
||||
const setHeight = () => {
|
||||
state.height = window.innerHeight - 157 + 'px';
|
||||
state.height = vh.value - 148 + 'px';
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', setHeight);
|
||||
});
|
||||
|
||||
watch(filterText, (val) => {
|
||||
treeRef.value?.filter(val);
|
||||
});
|
||||
@@ -104,6 +114,18 @@ const filterNode = (value: string, data: any) => {
|
||||
return data.label.includes(value);
|
||||
};
|
||||
|
||||
/**
|
||||
* 加载标签树节点
|
||||
*/
|
||||
const loadTags = async () => {
|
||||
const tags = await tagApi.getResourceTagPaths.request({ resourceType: props.resourceType });
|
||||
const tagNodes = [];
|
||||
for (let tagPath of tags) {
|
||||
tagNodes.push(new TagTreeNode(tagPath, tagPath, props.tagPathNodeType));
|
||||
}
|
||||
return tagNodes;
|
||||
};
|
||||
|
||||
/**
|
||||
* 加载树节点
|
||||
* @param { Object } node
|
||||
@@ -115,7 +137,13 @@ const loadNode = async (node: any, resolve: any) => {
|
||||
}
|
||||
let nodes = [];
|
||||
try {
|
||||
nodes = await props.load(node);
|
||||
if (node.level == 0) {
|
||||
nodes = await loadTags();
|
||||
} else if (props.load) {
|
||||
nodes = await props.load(node);
|
||||
} else {
|
||||
nodes = await node.data.loadChildren();
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
}
|
||||
@@ -124,18 +152,23 @@ const loadNode = async (node: any, resolve: any) => {
|
||||
|
||||
const treeNodeClick = (data: any) => {
|
||||
emit('nodeClick', data);
|
||||
if (data.type.nodeClickFunc) {
|
||||
data.type.nodeClickFunc(data);
|
||||
}
|
||||
// 关闭可能存在的右击菜单
|
||||
contextmenuRef.value.closeContextmenu();
|
||||
};
|
||||
|
||||
// 树节点右击事件
|
||||
const nodeContextmenu = (event: any, data: any) => {
|
||||
if (!props.loadContextmenuItems) {
|
||||
return;
|
||||
}
|
||||
// 加载当前节点是否需要显示右击菜单
|
||||
const items = props.loadContextmenuItems(data);
|
||||
let items = data.type.contextMenuItems;
|
||||
if (!items || items.length == 0) {
|
||||
if (props.loadContextmenuItems) {
|
||||
items = props.loadContextmenuItems(data);
|
||||
}
|
||||
}
|
||||
if (!items) {
|
||||
return;
|
||||
}
|
||||
state.contextmenuItems = items;
|
||||
@@ -173,6 +206,7 @@ defineExpose({
|
||||
overflow: 'auto';
|
||||
position: relative;
|
||||
|
||||
border-radius: var(--el-input-border-radius, var(--el-border-radius-base));
|
||||
border: 1px solid var(--el-border-color-light, #ebeef5);
|
||||
|
||||
.el-tree {
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
<div>
|
||||
<el-tree-select
|
||||
v-bind="$attrs"
|
||||
@check="changeTag"
|
||||
v-model="selectTags"
|
||||
@change="changeTag"
|
||||
style="width: 100%"
|
||||
:data="tags"
|
||||
placeholder="请选择关联标签"
|
||||
:render-after-expand="true"
|
||||
:default-expanded-keys="[selectTags]"
|
||||
show-checkbox
|
||||
check-strictly
|
||||
node-key="id"
|
||||
:props="{
|
||||
value: 'id',
|
||||
@@ -33,35 +33,46 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useAttrs, toRefs, reactive, onMounted } from 'vue';
|
||||
import { toRefs, reactive, onMounted } from 'vue';
|
||||
import { tagApi } from '../tag/api';
|
||||
|
||||
const attrs = useAttrs();
|
||||
//定义事件
|
||||
const emit = defineEmits(['changeTag', 'update:tagPath']);
|
||||
const emit = defineEmits(['update:modelValue', 'changeTag', 'input']);
|
||||
|
||||
const props = defineProps({
|
||||
resourceCode: {
|
||||
type: [String],
|
||||
required: true,
|
||||
},
|
||||
resourceType: {
|
||||
type: [Number],
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
tags: [],
|
||||
// 单选则为id,多选为id数组
|
||||
selectTags: null as any,
|
||||
selectTags: [],
|
||||
});
|
||||
|
||||
const { tags, selectTags } = toRefs(state);
|
||||
|
||||
onMounted(async () => {
|
||||
if (attrs.modelValue) {
|
||||
state.selectTags = attrs.modelValue;
|
||||
if (props.resourceCode) {
|
||||
const resourceTags = await tagApi.getTagResources.request({
|
||||
resourceCode: props.resourceCode,
|
||||
resourceType: props.resourceType,
|
||||
});
|
||||
state.selectTags = resourceTags.map((x: any) => x.tagId);
|
||||
changeTag();
|
||||
}
|
||||
|
||||
state.tags = await tagApi.getTagTrees.request(null);
|
||||
});
|
||||
|
||||
const changeTag = (tag: any, checkInfo: any) => {
|
||||
if (checkInfo.checkedNodes.length > 0) {
|
||||
emit('update:tagPath', tag.codePath);
|
||||
emit('changeTag', tag);
|
||||
} else {
|
||||
emit('update:tagPath', null);
|
||||
}
|
||||
const changeTag = () => {
|
||||
emit('changeTag', state.selectTags);
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
@@ -1,3 +1,5 @@
|
||||
import { ContextmenuItem } from '@/components/contextmenu';
|
||||
|
||||
export class TagTreeNode {
|
||||
/**
|
||||
* 节点id
|
||||
@@ -12,18 +14,26 @@ export class TagTreeNode {
|
||||
/**
|
||||
* 树节点类型
|
||||
*/
|
||||
type: any;
|
||||
type: NodeType;
|
||||
|
||||
/**
|
||||
* 是否为叶子节点
|
||||
*/
|
||||
isLeaf: boolean = false;
|
||||
|
||||
/**
|
||||
* 额外需要传递的参数
|
||||
*/
|
||||
params: any;
|
||||
|
||||
icon: any;
|
||||
|
||||
static TagPath = -1;
|
||||
|
||||
constructor(key: any, label: string, type?: any) {
|
||||
constructor(key: any, label: string, type?: NodeType) {
|
||||
this.key = key;
|
||||
this.label = label;
|
||||
this.type = type || TagTreeNode.TagPath;
|
||||
this.type = type || new NodeType(TagTreeNode.TagPath);
|
||||
}
|
||||
|
||||
withIsLeaf(isLeaf: boolean) {
|
||||
@@ -35,4 +45,73 @@ export class TagTreeNode {
|
||||
this.params = params;
|
||||
return this;
|
||||
}
|
||||
|
||||
withIcon(icon: any) {
|
||||
this.icon = icon;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载子节点,使用节点类型的loadNodesFunc去加载子节点
|
||||
* @returns 子节点信息
|
||||
*/
|
||||
async loadChildren() {
|
||||
if (this.isLeaf) {
|
||||
return [];
|
||||
}
|
||||
if (this.type && this.type.loadNodesFunc) {
|
||||
return await this.type.loadNodesFunc(this);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点类型,用于加载子节点及点击事件等
|
||||
*/
|
||||
export class NodeType {
|
||||
/**
|
||||
* 节点类型值
|
||||
*/
|
||||
value: number;
|
||||
|
||||
contextMenuItems: ContextmenuItem[];
|
||||
|
||||
loadNodesFunc: (parentNode: TagTreeNode) => Promise<TagTreeNode[]>;
|
||||
|
||||
nodeClickFunc: (node: TagTreeNode) => void;
|
||||
|
||||
constructor(value: number) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 赋值加载子节点回调函数
|
||||
* @param func 加载子节点回调函数
|
||||
* @returns this
|
||||
*/
|
||||
withLoadNodesFunc(func: (parentNode: TagTreeNode) => Promise<TagTreeNode[]>) {
|
||||
this.loadNodesFunc = func;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 赋值节点点击事件回调函数
|
||||
* @param func 节点点击事件回调函数
|
||||
* @returns this
|
||||
*/
|
||||
withNodeClickFunc(func: (node: TagTreeNode) => void) {
|
||||
this.nodeClickFunc = func;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 赋值右击菜单按钮选项
|
||||
* @param contextMenuItems 右击菜单按钮选项
|
||||
* @returns this
|
||||
*/
|
||||
withContextMenuItems(contextMenuItems: ContextmenuItem[]) {
|
||||
this.contextMenuItems = contextMenuItems;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,16 +10,27 @@
|
||||
width="38%"
|
||||
>
|
||||
<el-form :model="form" ref="dbForm" :rules="rules" label-width="auto">
|
||||
<el-form-item prop="tagId" label="标签:" required>
|
||||
<tag-select v-model="form.tagId" v-model:tag-path="form.tagPath" style="width: 100%" />
|
||||
<el-form-item ref="tagSelectRef" prop="tagId" label="标签" required>
|
||||
<tag-tree-select
|
||||
@change-tag="
|
||||
(tagIds) => {
|
||||
form.tagId = tagIds;
|
||||
tagSelectRef.validate();
|
||||
}
|
||||
"
|
||||
multiple
|
||||
:resource-code="form.code"
|
||||
:resource-type="TagResourceTypeEnum.Db.value"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="instanceId" label="数据库实例:" required>
|
||||
<el-form-item prop="instanceId" label="数据库实例" required>
|
||||
<el-select
|
||||
:disabled="form.id !== undefined"
|
||||
remote
|
||||
:remote-method="getInstances"
|
||||
@change="getAllDatabase"
|
||||
@change="changeInstance"
|
||||
v-model="form.instanceId"
|
||||
placeholder="请输入实例名称搜索并选择实例"
|
||||
filterable
|
||||
@@ -37,11 +48,11 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="name" label="别名:" required>
|
||||
<el-form-item prop="name" label="别名" required>
|
||||
<el-input v-model.trim="form.name" placeholder="请输入数据库别名" auto-complete="off"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="database" label="数据库名:" required>
|
||||
<el-form-item prop="database" label="数据库名" required>
|
||||
<el-select
|
||||
@change="changeDatabase"
|
||||
v-model="databaseList"
|
||||
@@ -58,7 +69,7 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="remark" label="备注:">
|
||||
<el-form-item prop="remark" label="备注">
|
||||
<el-input v-model.trim="form.remark" auto-complete="off" type="textarea"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
@@ -77,7 +88,8 @@
|
||||
import { toRefs, reactive, watch, ref } from 'vue';
|
||||
import { dbApi } from './api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import TagSelect from '../component/TagSelect.vue';
|
||||
import TagTreeSelect from '../component/TagTreeSelect.vue';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
@@ -128,6 +140,7 @@ const rules = {
|
||||
};
|
||||
|
||||
const dbForm: any = ref(null);
|
||||
const tagSelectRef: any = ref(null);
|
||||
|
||||
const state = reactive({
|
||||
dialogVisible: false,
|
||||
@@ -135,9 +148,9 @@ const state = reactive({
|
||||
databaseList: [] as any,
|
||||
form: {
|
||||
id: null,
|
||||
tagId: null as any,
|
||||
tagPath: null as any,
|
||||
tagId: [],
|
||||
name: null,
|
||||
code: '',
|
||||
database: '',
|
||||
remark: '',
|
||||
instanceId: null as any,
|
||||
@@ -148,13 +161,14 @@ const state = reactive({
|
||||
|
||||
const { dialogVisible, allDatabases, databaseList, form, btnLoading } = toRefs(state);
|
||||
|
||||
watch(props, (newValue: any) => {
|
||||
watch(props, async (newValue: any) => {
|
||||
state.dialogVisible = newValue.visible;
|
||||
if (!state.dialogVisible) {
|
||||
return;
|
||||
}
|
||||
if (newValue.db) {
|
||||
state.form = { ...newValue.db };
|
||||
|
||||
// 将数据库名使用空格切割,获取所有数据库列表
|
||||
state.databaseList = newValue.db.database.split(' ');
|
||||
} else {
|
||||
@@ -163,6 +177,11 @@ watch(props, (newValue: any) => {
|
||||
}
|
||||
});
|
||||
|
||||
const changeInstance = () => {
|
||||
state.databaseList = [];
|
||||
getAllDatabase();
|
||||
};
|
||||
|
||||
/**
|
||||
* 改变表单中的数据库字段,方便表单错误提示。如全部删光,可提示请添加数据库
|
||||
*/
|
||||
|
||||
@@ -46,54 +46,43 @@
|
||||
</template>
|
||||
|
||||
<template #tagPath="{ data }">
|
||||
<tag-info :tag-path="data.tagPath" />
|
||||
<span class="ml5">
|
||||
{{ data.tagPath }}
|
||||
</span>
|
||||
<resource-tag :resource-code="data.code" :resource-type="TagResourceTypeEnum.Db.value" />
|
||||
</template>
|
||||
|
||||
<template #database="{ data }">
|
||||
<el-popover placement="right" trigger="click" :width="300">
|
||||
<template #reference>
|
||||
<el-link type="primary" :underline="false" plain @click="selectDb(data.dbs)">查看 </el-link>
|
||||
</template>
|
||||
<el-input v-model="filterDb.param" @keyup="filterSchema" class="w-50 m-2" placeholder="搜索" size="small">
|
||||
<template #prefix>
|
||||
<el-icon class="el-input__icon">
|
||||
<search-icon />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<div
|
||||
class="el-tag--plain el-tag--success"
|
||||
v-for="db in filterDb.list"
|
||||
:key="db"
|
||||
style="border: 1px var(--color-success-light-3) solid; margin-top: 3px; border-radius: 5px; padding: 2px; position: relative"
|
||||
>
|
||||
<el-link type="success" plain size="small" :underline="false">{{ db }}</el-link>
|
||||
<el-link type="primary" plain size="small" :underline="false" @click="showTableInfo(data, db)" style="position: absolute; right: 4px"
|
||||
>操作
|
||||
</el-link>
|
||||
</div>
|
||||
</el-popover>
|
||||
</template>
|
||||
|
||||
<template #more="{ data }">
|
||||
<el-button @click="showInfo(data)" link>详情</el-button>
|
||||
<el-button class="ml5" type="primary" @click="onShowSqlExec(data)" link>SQL执行记录</el-button>
|
||||
<template #host="{ data }">
|
||||
{{ `${data.host}:${data.port}` }}
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
<el-button v-if="actionBtns[perms.saveDb]" @click="editDb(data)" type="primary" link>编辑</el-button>
|
||||
<el-button v-if="data.type == 'mysql'" class="ml5" type="primary" @click="onDumpDbs(data)" link>导出</el-button>
|
||||
<span v-if="actionBtns[perms.saveDb]">
|
||||
<el-button type="primary" @click="editDb(data)" link>编辑</el-button>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
</span>
|
||||
|
||||
<el-button type="primary" @click="onShowSqlExec(data)" link>SQL记录</el-button>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-dropdown @command="handleMoreActionCommand">
|
||||
<span class="el-dropdown-link-more">
|
||||
更多
|
||||
<el-icon class="el-icon--right">
|
||||
<arrow-down />
|
||||
</el-icon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item :command="{ type: 'detail', data }"> 详情 </el-dropdown-item>
|
||||
|
||||
<!-- <el-dropdown-item :command="{ type: 'edit', data }" v-if="actionBtns[perms.saveDb]"> 编辑 </el-dropdown-item> -->
|
||||
|
||||
<el-dropdown-item :command="{ type: 'dumpDb', data }" v-if="data.type == DbType.mysql"> 导出 </el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
</page-table>
|
||||
|
||||
<el-dialog width="80%" :title="`${db} 表信息`" :before-close="closeTableInfo" v-model="tableInfoDialog.visible">
|
||||
<db-table-list :db-id="dbId" :db="db" :db-type="state.row.type" />
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog width="620" :title="`${db} 数据库导出`" v-model="exportDialog.visible">
|
||||
<el-dialog width="720px" :title="`${db} 数据库导出`" v-model="exportDialog.visible">
|
||||
<el-row justify="space-between">
|
||||
<el-col :span="9">
|
||||
<el-form-item label="导出内容: ">
|
||||
@@ -172,18 +161,20 @@ import { ref, toRefs, reactive, onMounted, defineAsyncComponent } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { dbApi } from './api';
|
||||
import config from '@/common/config';
|
||||
import { getSession } from '@/common/utils/storage';
|
||||
import { joinClientParams } from '@/common/request';
|
||||
import { isTrue } from '@/common/assert';
|
||||
import { Search as SearchIcon } from '@element-plus/icons-vue';
|
||||
import { dateFormat } from '@/common/utils/date';
|
||||
import TagInfo from '../component/TagInfo.vue';
|
||||
import ResourceTag from '../component/ResourceTag.vue';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn, TableQuery } from '@/components/pagetable';
|
||||
import { hasPerms } from '@/components/auth/auth';
|
||||
import DbSqlExecLog from './DbSqlExecLog.vue';
|
||||
import { DbType } from './dialect';
|
||||
import { tagApi } from '../tag/api';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const DbEdit = defineAsyncComponent(() => import('./DbEdit.vue'));
|
||||
const DbTableList = defineAsyncComponent(() => import('./table/DbTableList.vue'));
|
||||
|
||||
const perms = {
|
||||
base: 'db',
|
||||
@@ -194,17 +185,20 @@ const perms = {
|
||||
const queryConfig = [TableQuery.slot('tagPath', '标签', 'tagPathSelect'), TableQuery.slot('instanceId', '实例', 'instanceSelect')];
|
||||
|
||||
const columns = ref([
|
||||
TableColumn.new('tagPath', '标签路径').isSlot().setAddWidth(20),
|
||||
TableColumn.new('instanceName', '实例名'),
|
||||
TableColumn.new('type', '类型'),
|
||||
TableColumn.new('host', 'ip:port').isSlot().setAddWidth(40),
|
||||
TableColumn.new('username', 'username'),
|
||||
TableColumn.new('name', '名称'),
|
||||
TableColumn.new('database', '数据库').isSlot().setMinWidth(70),
|
||||
TableColumn.new('tagPath', '关联标签').isSlot().setAddWidth(10).alignCenter(),
|
||||
TableColumn.new('remark', '备注'),
|
||||
TableColumn.new('more', '更多').isSlot().setMinWidth(180).fixedRight(),
|
||||
]);
|
||||
|
||||
// 该用户拥有的的操作列按钮权限
|
||||
const actionBtns = hasPerms([perms.base, perms.saveDb]);
|
||||
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(150).fixedRight().alignCenter();
|
||||
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight().alignCenter();
|
||||
|
||||
const route = useRoute();
|
||||
const pageTableRef: any = ref(null);
|
||||
|
||||
const state = reactive({
|
||||
@@ -221,10 +215,10 @@ const state = reactive({
|
||||
* 查询条件
|
||||
*/
|
||||
query: {
|
||||
tagPath: null,
|
||||
tagPath: '',
|
||||
instanceId: null,
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 0,
|
||||
},
|
||||
datas: [],
|
||||
total: 0,
|
||||
@@ -236,13 +230,6 @@ const state = reactive({
|
||||
instanceId: 0,
|
||||
},
|
||||
},
|
||||
showDumpInfo: false,
|
||||
dumpInfo: {
|
||||
id: 0,
|
||||
db: '',
|
||||
type: 3,
|
||||
tables: [],
|
||||
},
|
||||
// sql执行记录弹框
|
||||
sqlExecLogDialog: {
|
||||
title: '',
|
||||
@@ -250,10 +237,6 @@ const state = reactive({
|
||||
dbs: [],
|
||||
dbId: 0,
|
||||
},
|
||||
chooseTableName: '',
|
||||
tableInfoDialog: {
|
||||
visible: false,
|
||||
},
|
||||
exportDialog: {
|
||||
visible: false,
|
||||
dbId: 0,
|
||||
@@ -275,8 +258,7 @@ const state = reactive({
|
||||
},
|
||||
});
|
||||
|
||||
const { dbId, db, tags, selectionData, query, datas, total, infoDialog, sqlExecLogDialog, tableInfoDialog, exportDialog, dbEditDialog, filterDb } =
|
||||
toRefs(state);
|
||||
const { db, tags, selectionData, query, datas, total, infoDialog, sqlExecLogDialog, exportDialog, dbEditDialog } = toRefs(state);
|
||||
|
||||
onMounted(async () => {
|
||||
if (Object.keys(actionBtns).length > 0) {
|
||||
@@ -288,6 +270,10 @@ onMounted(async () => {
|
||||
const search = async () => {
|
||||
try {
|
||||
pageTableRef.value.loading(true);
|
||||
|
||||
if (route.query.tagPath) {
|
||||
state.query.tagPath = route.query.tagPath as string;
|
||||
}
|
||||
let res: any = await dbApi.dbs.request(state.query);
|
||||
// 切割数据库
|
||||
res.list?.forEach((e: any) => {
|
||||
@@ -316,7 +302,7 @@ const onBeforeCloseInfoDialog = () => {
|
||||
};
|
||||
|
||||
const getTags = async () => {
|
||||
state.tags = await dbApi.dbTags.request(null);
|
||||
state.tags = await tagApi.getResourceTagPaths.request({ resourceType: TagResourceTypeEnum.Db.value });
|
||||
};
|
||||
|
||||
const getInstances = async (instanceName = '') => {
|
||||
@@ -330,6 +316,24 @@ const getInstances = async (instanceName = '') => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleMoreActionCommand = (commond: any) => {
|
||||
const data = commond.data;
|
||||
const type = commond.type;
|
||||
switch (type) {
|
||||
case 'detail': {
|
||||
showInfo(data);
|
||||
return;
|
||||
}
|
||||
case 'edit': {
|
||||
editDb(data);
|
||||
return;
|
||||
}
|
||||
case 'dumpDb': {
|
||||
onDumpDbs(data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const editDb = async (data: any) => {
|
||||
if (!data) {
|
||||
state.dbEditDialog.data = null;
|
||||
@@ -355,7 +359,9 @@ const deleteDb = async () => {
|
||||
await dbApi.deleteDb.request({ id: state.selectionData.map((x: any) => x.id).join(',') });
|
||||
ElMessage.success('删除成功');
|
||||
search();
|
||||
} catch (err) {}
|
||||
} catch (err) {
|
||||
//
|
||||
}
|
||||
};
|
||||
|
||||
const onShowSqlExec = async (row: any) => {
|
||||
@@ -406,40 +412,23 @@ const dumpDbs = () => {
|
||||
'href',
|
||||
`${config.baseApiUrl}/dbs/${state.exportDialog.dbId}/dump?db=${state.exportDialog.value.join(',')}&type=${type}&extName=${
|
||||
state.exportDialog.extName
|
||||
}&token=${getSession('token')}`
|
||||
}&${joinClientParams()}`
|
||||
);
|
||||
a.click();
|
||||
state.exportDialog.visible = false;
|
||||
};
|
||||
|
||||
const showTableInfo = async (row: any, db: string) => {
|
||||
state.dbId = row.id;
|
||||
state.row = row;
|
||||
state.db = db;
|
||||
state.tableInfoDialog.visible = true;
|
||||
};
|
||||
|
||||
const closeTableInfo = () => {
|
||||
state.showDumpInfo = false;
|
||||
state.tableInfoDialog.visible = false;
|
||||
};
|
||||
|
||||
// 点击查看时初始化数据
|
||||
const selectDb = (row: any) => {
|
||||
state.filterDb.param = '';
|
||||
state.filterDb.cache = row;
|
||||
state.filterDb.list = row;
|
||||
};
|
||||
|
||||
// 输入字符过滤schema
|
||||
const filterSchema = () => {
|
||||
if (state.filterDb.param) {
|
||||
state.filterDb.list = state.filterDb.cache.filter((a) => {
|
||||
return String(a).toLowerCase().indexOf(state.filterDb.param) > -1;
|
||||
});
|
||||
} else {
|
||||
state.filterDb.list = state.filterDb.cache;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
<style lang="scss">
|
||||
.db-list {
|
||||
.el-transfer-panel {
|
||||
width: 250px;
|
||||
}
|
||||
}
|
||||
.el-dropdown-link-more {
|
||||
cursor: pointer;
|
||||
color: var(--el-color-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 6px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, toRefs,watch, reactive, computed, onMounted, defineAsyncComponent } from 'vue';
|
||||
import { toRefs, watch, reactive, onMounted } from 'vue';
|
||||
import { dbApi } from './api';
|
||||
import { DbSqlExecTypeEnum } from './enums';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
@@ -103,13 +103,12 @@ onMounted(async () => {
|
||||
searchSqlExecLog();
|
||||
});
|
||||
|
||||
watch(props, async (newValue: any) => {
|
||||
watch(props, async () => {
|
||||
await searchSqlExecLog();
|
||||
});
|
||||
|
||||
|
||||
const searchSqlExecLog = async () => {
|
||||
state.query.dbId = props.dbId
|
||||
state.query.dbId = props.dbId;
|
||||
const res = await dbApi.getSqlExecs.request(state.query);
|
||||
state.data = res.list;
|
||||
state.total = res.total;
|
||||
|
||||
@@ -4,16 +4,16 @@
|
||||
<el-form :model="form" ref="dbForm" :rules="rules" label-width="auto">
|
||||
<el-tabs v-model="tabActiveName">
|
||||
<el-tab-pane label="基础信息" name="basic">
|
||||
<el-form-item prop="name" label="别名:" required>
|
||||
<el-form-item prop="name" label="别名" required>
|
||||
<el-input v-model.trim="form.name" placeholder="请输入数据库别名" auto-complete="off"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="type" label="类型:" required>
|
||||
<el-form-item prop="type" label="类型" required>
|
||||
<el-select style="width: 100%" v-model="form.type" placeholder="请选择数据库类型">
|
||||
<el-option key="item.id" label="mysql" value="mysql"> </el-option>
|
||||
<el-option key="item.id" label="postgres" value="postgres"> </el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item prop="host" label="host:" required>
|
||||
<el-form-item prop="host" label="host" required>
|
||||
<el-col :span="18">
|
||||
<el-input :disabled="form.id !== undefined" v-model.trim="form.host" placeholder="请输入主机ip" auto-complete="off"></el-input>
|
||||
</el-col>
|
||||
@@ -22,10 +22,10 @@
|
||||
<el-input type="number" v-model.number="form.port" placeholder="请输入端口"></el-input>
|
||||
</el-col>
|
||||
</el-form-item>
|
||||
<el-form-item prop="username" label="用户名:" required>
|
||||
<el-form-item prop="username" label="用户名" required>
|
||||
<el-input v-model.trim="form.username" placeholder="请输入用户名"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="password" label="密码:">
|
||||
<el-form-item prop="password" label="密码">
|
||||
<el-input type="password" show-password v-model.trim="form.password" placeholder="请输入密码" autocomplete="new-password">
|
||||
<template v-if="form.id && form.id != 0" #suffix>
|
||||
<el-popover @hide="pwd = ''" placement="right" title="原密码" :width="200" trigger="click" :content="pwd">
|
||||
@@ -39,13 +39,13 @@
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="remark" label="备注:">
|
||||
<el-input v-model.trim="form.remark" auto-complete="off" type="textarea"></el-input>
|
||||
<el-form-item prop="remark" label="备注">
|
||||
<el-input v-model="form.remark" auto-complete="off" type="textarea"></el-input>
|
||||
</el-form-item>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="其他配置" name="other">
|
||||
<el-form-item prop="params" label="连接参数:">
|
||||
<el-form-item prop="params" label="连接参数">
|
||||
<el-input v-model.trim="form.params" placeholder="其他连接参数,形如: key1=value1&key2=value2">
|
||||
<template #suffix>
|
||||
<el-link
|
||||
@@ -60,7 +60,7 @@
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="sshTunnelMachineId" label="SSH隧道:">
|
||||
<el-form-item prop="sshTunnelMachineId" label="SSH隧道">
|
||||
<ssh-tunnel-select v-model="form.sshTunnelMachineId" />
|
||||
</el-form-item>
|
||||
</el-tab-pane>
|
||||
@@ -69,6 +69,7 @@
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="testConn" :loading="state.testConnBtnLoading" type="success">测试连接</el-button>
|
||||
<el-button @click="cancel()">取 消</el-button>
|
||||
<el-button type="primary" :loading="btnLoading" @click="btnOk">确 定</el-button>
|
||||
</div>
|
||||
@@ -153,6 +154,7 @@ const state = reactive({
|
||||
// 原用户名
|
||||
oldUserName: null,
|
||||
btnLoading: false,
|
||||
testConnBtnLoading: false,
|
||||
});
|
||||
|
||||
const { dialogVisible, tabActiveName, form, pwd, btnLoading } = toRefs(state);
|
||||
@@ -176,6 +178,32 @@ const getDbPwd = async () => {
|
||||
state.pwd = await dbApi.getInstancePwd.request({ id: state.form.id });
|
||||
};
|
||||
|
||||
const getReqForm = async () => {
|
||||
const reqForm = { ...state.form };
|
||||
reqForm.password = await RsaEncrypt(reqForm.password);
|
||||
if (!state.form.sshTunnelMachineId) {
|
||||
reqForm.sshTunnelMachineId = -1;
|
||||
}
|
||||
return reqForm;
|
||||
};
|
||||
|
||||
const testConn = async () => {
|
||||
dbForm.value.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
state.testConnBtnLoading = true;
|
||||
try {
|
||||
await dbApi.testConn.request(await getReqForm());
|
||||
ElMessage.success('连接成功');
|
||||
} finally {
|
||||
state.testConnBtnLoading = false;
|
||||
}
|
||||
} else {
|
||||
ElMessage.error('请正确填写信息');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const btnOk = async () => {
|
||||
if (!state.form.id) {
|
||||
notBlank(state.form.password, '新增操作,密码不可为空');
|
||||
@@ -185,12 +213,7 @@ const btnOk = async () => {
|
||||
|
||||
dbForm.value.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
const reqForm = { ...state.form };
|
||||
reqForm.password = await RsaEncrypt(reqForm.password);
|
||||
if (!state.form.sshTunnelMachineId) {
|
||||
reqForm.sshTunnelMachineId = -1;
|
||||
}
|
||||
dbApi.saveInstance.request(reqForm).then(() => {
|
||||
dbApi.saveInstance.request(await getReqForm()).then(() => {
|
||||
ElMessage.success('保存成功');
|
||||
emit('val-change', state.form);
|
||||
state.btnLoading = true;
|
||||
|
||||
@@ -20,11 +20,8 @@
|
||||
>
|
||||
</template>
|
||||
|
||||
<template #more="{ data }">
|
||||
<el-button @click="showInfo(data)" link>详情</el-button>
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
<el-button @click="showInfo(data)" link>详情</el-button>
|
||||
<el-button v-if="actionBtns[perms.saveInstance]" @click="editInstance(data)" type="primary" link>编辑</el-button>
|
||||
</template>
|
||||
</page-table>
|
||||
@@ -81,16 +78,16 @@ const queryConfig = [TableQuery.text('name', '名称')];
|
||||
|
||||
const columns = ref([
|
||||
TableColumn.new('name', '名称'),
|
||||
TableColumn.new('host', 'host:port').setFormatFunc((data: any, _prop: string) => `${data.host}:${data.port}`),
|
||||
TableColumn.new('type', '类型'),
|
||||
TableColumn.new('host', 'host:port').setFormatFunc((data: any) => `${data.host}:${data.port}`),
|
||||
TableColumn.new('username', '用户名'),
|
||||
TableColumn.new('params', '连接参数'),
|
||||
TableColumn.new('remark', '备注'),
|
||||
TableColumn.new('more', '更多').isSlot().setMinWidth(50).fixedRight(),
|
||||
]);
|
||||
|
||||
// 该用户拥有的的操作列按钮权限
|
||||
const actionBtns = hasPerms([perms.saveInstance]);
|
||||
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(65).fixedRight().alignCenter();
|
||||
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(110).fixedRight().alignCenter();
|
||||
|
||||
const pageTableRef: any = ref(null);
|
||||
|
||||
@@ -108,7 +105,7 @@ const state = reactive({
|
||||
query: {
|
||||
name: null,
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 0,
|
||||
},
|
||||
datas: [],
|
||||
total: 0,
|
||||
@@ -173,7 +170,9 @@ const deleteInstance = async () => {
|
||||
await dbApi.deleteInstance.request({ id: state.selectionData.map((x: any) => x.id).join(',') });
|
||||
ElMessage.success('删除成功');
|
||||
search();
|
||||
} catch (err) {}
|
||||
} catch (err) {
|
||||
//
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
|
||||
@@ -1,124 +1,369 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-row class="mb5">
|
||||
<el-col :span="4">
|
||||
<el-button type="primary" icon="plus" @click="addQueryTab({ id: nowDbInst.id, dbs: nowDbInst.databases.split(' ') }, state.db)" size="small"
|
||||
>新建查询</el-button
|
||||
>
|
||||
</el-col>
|
||||
<el-col :span="20" v-if="state.db">
|
||||
<el-descriptions :column="4" size="small" border style="height: 10px" class="ml5">
|
||||
<el-descriptions-item label-align="right" label="tag">{{ nowDbInst.tagPath }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="实例" label-align="right">
|
||||
{{ nowDbInst.id }}
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
{{ nowDbInst.type }}
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
{{ nowDbInst.name }}
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="库名" label-align="right">{{ state.db }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row type="flex">
|
||||
<el-col :span="4">
|
||||
<tag-tree
|
||||
ref="tagTreeRef"
|
||||
@node-click="nodeClick"
|
||||
:load="loadNode"
|
||||
:load-contextmenu-items="getContextmenuItems"
|
||||
@current-contextmenu-click="onCurrentContextmenuClick"
|
||||
:height="state.tagTreeHeight"
|
||||
>
|
||||
<div class="db-sql-exec">
|
||||
<el-row>
|
||||
<el-col :span="5">
|
||||
<tag-tree :resource-type="TagResourceTypeEnum.Db.value" :tag-path-node-type="NodeTypeTagPath" ref="tagTreeRef">
|
||||
<template #prefix="{ data }">
|
||||
<span v-if="data.type == NodeType.DbInst">
|
||||
<el-popover placement="right-start" title="数据库实例信息" trigger="hover" :width="210">
|
||||
<span v-if="data.type.value == SqlExecNodeType.DbInst">
|
||||
<el-popover :show-after="500" placement="right-start" title="数据库实例信息" trigger="hover" :width="250">
|
||||
<template #reference>
|
||||
<SvgIcon v-if="data.params.type === 'mysql'" name="iconfont icon-op-mysql" :size="18" />
|
||||
<SvgIcon v-if="data.params.type === 'postgres'" name="iconfont icon-op-postgres" :size="18" />
|
||||
|
||||
<SvgIcon name="InfoFilled" v-else />
|
||||
<SvgIcon :name="getDbDialect(data.params.type).getIcon()" :size="18" />
|
||||
</template>
|
||||
<template #default>
|
||||
<el-form class="instances-pop-form" label-width="55px" :size="'small'">
|
||||
<el-form-item label="类型:">{{ data.params.type }}</el-form-item>
|
||||
<el-form-item label="名称:">{{ data.params.name }}</el-form-item>
|
||||
<el-form-item v-if="data.params.remark" label="备注:">{{ data.params.remark }}</el-form-item>
|
||||
</el-form>
|
||||
<el-descriptions :column="1" size="small">
|
||||
<el-descriptions-item label="名称">
|
||||
{{ data.params.name }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="host">
|
||||
{{ `${data.params.host}:${data.params.port}` }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="user">
|
||||
{{ data.params.username }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注">
|
||||
{{ data.params.remark }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
</el-popover>
|
||||
</span>
|
||||
|
||||
<SvgIcon v-if="data.type == NodeType.Db" name="Coin" color="#67c23a" />
|
||||
<SvgIcon v-if="data.icon" :name="data.icon.name" :color="data.icon.color" />
|
||||
</template>
|
||||
|
||||
<SvgIcon name="Calendar" v-if="data.type == NodeType.TableMenu" color="#409eff" />
|
||||
|
||||
<el-tooltip v-if="data.type == NodeType.Table" effect="customized" :content="data.params.tableComment" placement="top-end">
|
||||
<SvgIcon name="Calendar" color="#409eff" />
|
||||
<template #label="{ data }">
|
||||
<el-tooltip placement="left" :show-after="1000" v-if="data.type.value == SqlExecNodeType.Table" :content="data.params.tableComment">
|
||||
{{ data.label }}
|
||||
</el-tooltip>
|
||||
</template>
|
||||
|
||||
<SvgIcon name="Files" v-if="data.type == NodeType.SqlMenu || data.type == NodeType.Sql" color="#f56c6c" />
|
||||
<template #suffix="{ data }">
|
||||
<span class="db-table-size" v-if="data.type.value == SqlExecNodeType.Table && data.params.size">{{ ` ${data.params.size}` }}</span>
|
||||
<span class="db-table-size" v-if="data.type.value == SqlExecNodeType.TableMenu && data.params.dbTableSize">{{
|
||||
` ${data.params.dbTableSize}`
|
||||
}}</span>
|
||||
</template>
|
||||
</tag-tree>
|
||||
</el-col>
|
||||
<el-col :span="20">
|
||||
<el-container id="data-exec" class="mt5 ml5">
|
||||
<el-tabs @tab-remove="onRemoveTab" @tab-change="onTabChange" style="width: 100%" v-model="state.activeName">
|
||||
<el-tab-pane closable v-for="dt in state.tabs.values()" :key="dt.key" :label="dt.key" :name="dt.key">
|
||||
<table-data
|
||||
v-if="dt.type === TabType.TableData"
|
||||
@gen-insert-sql="onGenerateInsertSql"
|
||||
:data="dt"
|
||||
:table-height="state.dataTabsTableHeight"
|
||||
></table-data>
|
||||
|
||||
<query
|
||||
v-else
|
||||
<el-col :span="19">
|
||||
<el-row>
|
||||
<el-col :span="24" v-if="state.db">
|
||||
<el-descriptions :column="4" size="small" border class="ml5">
|
||||
<el-descriptions-item label-align="right" label="操作"
|
||||
><el-button
|
||||
:disabled="!state.db || !nowDbInst.id"
|
||||
type="primary"
|
||||
icon="Search"
|
||||
@click="addQueryTab({ id: nowDbInst.id, dbs: nowDbInst.databases?.split(' ') }, state.db)"
|
||||
size="small"
|
||||
>新建查询</el-button
|
||||
></el-descriptions-item
|
||||
>
|
||||
|
||||
<el-descriptions-item label-align="right" label="tag">{{ nowDbInst.tagPath }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label-align="right">
|
||||
<template #label>
|
||||
<div>
|
||||
<SvgIcon :name="getDbDialect(nowDbInst.type).getIcon()" :size="18" />
|
||||
实例
|
||||
</div>
|
||||
</template>
|
||||
{{ nowDbInst.id }}
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
{{ nowDbInst.name }}
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
{{ nowDbInst.host }}
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="库名" label-align="right">{{ state.db }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<div id="data-exec" class="mt5 ml5">
|
||||
<el-tabs
|
||||
v-if="state.tabs.size > 0"
|
||||
type="card"
|
||||
@tab-remove="onRemoveTab"
|
||||
@tab-change="onTabChange"
|
||||
style="width: 100%"
|
||||
v-model="state.activeName"
|
||||
>
|
||||
<el-tab-pane closable v-for="dt in state.tabs.values()" :label="dt.label" :name="dt.key" :key="dt.key">
|
||||
<template #label>
|
||||
<el-popover :show-after="1000" placement="bottom-start" trigger="hover" :width="250">
|
||||
<template #reference> {{ dt.label }} </template>
|
||||
<template #default>
|
||||
<el-descriptions :column="1" size="small">
|
||||
<el-descriptions-item label="tagPath">
|
||||
{{ dt.params.tagPath }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="名称">
|
||||
{{ dt.params.name }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="host">
|
||||
<SvgIcon :name="getDbDialect(dt.params.type).getIcon()" :size="18" />
|
||||
{{ dt.params.host }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="库名">
|
||||
{{ dt.params.dbName }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
</el-popover>
|
||||
</template>
|
||||
|
||||
<db-table-data-op
|
||||
v-if="dt.type === TabType.TableData"
|
||||
:db-id="dt.dbId"
|
||||
:db-name="dt.db"
|
||||
:table-name="dt.params.table"
|
||||
:table-height="state.dataTabsTableHeight"
|
||||
></db-table-data-op>
|
||||
|
||||
<db-sql-editor
|
||||
v-if="dt.type === TabType.Query"
|
||||
:db-id="dt.dbId"
|
||||
:db-name="dt.db"
|
||||
:sql-name="dt.params.sqlName"
|
||||
@save-sql-success="reloadSqls"
|
||||
@delete-sql-success="deleteSqlScript(dt)"
|
||||
:data="dt"
|
||||
:editor-height="state.editorHeight"
|
||||
>
|
||||
</query>
|
||||
</db-sql-editor>
|
||||
|
||||
<db-tables-op
|
||||
v-if="dt.type == TabType.TablesOp"
|
||||
:db-id="dt.params.id"
|
||||
:db="dt.params.db"
|
||||
:db-type="dt.params.type"
|
||||
:height="state.tablesOpHeight"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-container>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-dialog @close="state.genSqlDialog.visible = false" v-model="state.genSqlDialog.visible" title="SQL" width="1000px">
|
||||
<el-input v-model="state.genSqlDialog.sql" type="textarea" rows="20" />
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, onMounted, reactive, ref, toRefs } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { DbInst, TabInfo, TabType } from './db';
|
||||
import { TagTreeNode } from '../component/tag';
|
||||
import { defineAsyncComponent, onMounted, reactive, ref, toRefs, onBeforeUnmount } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { formatByteSize } from '@/common/utils/format';
|
||||
import { DbInst, TabInfo, TabType, registerDbCompletionItemProvider } from './db';
|
||||
import { TagTreeNode, NodeType } from '../component/tag';
|
||||
import TagTree from '../component/TagTree.vue';
|
||||
import { dbApi } from './api';
|
||||
import { dispposeCompletionItemProvider } from '@/components/monaco/completionItemProvider';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
import { ContextmenuItem } from '@/components/contextmenu';
|
||||
import { getDbDialect } from './dialect/index';
|
||||
import { sleep } from '@/common/utils/loading';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
|
||||
const DbSqlEditor = defineAsyncComponent(() => import('./component/sqleditor/DbSqlEditor.vue'));
|
||||
const DbTableDataOp = defineAsyncComponent(() => import('./component/table/DbTableDataOp.vue'));
|
||||
const DbTablesOp = defineAsyncComponent(() => import('./component/table/DbTablesOp.vue'));
|
||||
|
||||
const Query = defineAsyncComponent(() => import('./component/tab/Query.vue'));
|
||||
const TableData = defineAsyncComponent(() => import('./component/tab/TableData.vue'));
|
||||
/**
|
||||
* 树节点类型
|
||||
*/
|
||||
class NodeType {
|
||||
class SqlExecNodeType {
|
||||
static DbInst = 1;
|
||||
static Db = 2;
|
||||
static TableMenu = 3;
|
||||
static SqlMenu = 4;
|
||||
static Table = 5;
|
||||
static Sql = 6;
|
||||
static PgSchemaMenu = 7;
|
||||
static PgSchema = 8;
|
||||
}
|
||||
class ContextmenuClickId {
|
||||
static ReloadTable = 0;
|
||||
}
|
||||
|
||||
const DbIcon = {
|
||||
name: 'Coin',
|
||||
color: '#67c23a',
|
||||
};
|
||||
|
||||
// pgsql schema icon
|
||||
const SchemaIcon = {
|
||||
name: 'List',
|
||||
color: '#67c23a',
|
||||
};
|
||||
|
||||
const TableIcon = {
|
||||
name: 'Calendar',
|
||||
color: '#409eff',
|
||||
};
|
||||
|
||||
const SqlIcon = {
|
||||
name: 'Files',
|
||||
color: '#f56c6c',
|
||||
};
|
||||
|
||||
// node节点点击时,触发改变db事件
|
||||
const nodeClickChangeDb = (nodeData: TagTreeNode) => {
|
||||
const params = nodeData.params;
|
||||
if (params.db) {
|
||||
changeDb(
|
||||
{ id: params.id, host: `${params.host}`, name: params.name, type: params.type, tagPath: params.tagPath, databases: params.database },
|
||||
params.db
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// tagpath 节点类型
|
||||
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const dbInfoRes = await dbApi.dbs.request({ tagPath: parentNode.key });
|
||||
const dbInfos = dbInfoRes.list;
|
||||
if (!dbInfos) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 防止过快加载会出现一闪而过,对眼睛不好
|
||||
await sleep(100);
|
||||
return dbInfos?.map((x: any) => {
|
||||
x.tagPath = parentNode.key;
|
||||
return new TagTreeNode(`${parentNode.key}.${x.id}`, x.name, NodeTypeDbInst).withParams(x);
|
||||
});
|
||||
});
|
||||
|
||||
// 数据库实例节点类型
|
||||
const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((parentNode: TagTreeNode) => {
|
||||
const params = parentNode.params;
|
||||
const dbs = params.database.split(' ')?.sort();
|
||||
return dbs.map((x: any) => {
|
||||
return new TagTreeNode(`${parentNode.key}.${x}`, x, NodeTypeDb)
|
||||
.withParams({
|
||||
tagPath: params.tagPath,
|
||||
id: params.id,
|
||||
name: params.name,
|
||||
type: params.type,
|
||||
host: `${params.host}:${params.port}`,
|
||||
dbs: dbs,
|
||||
db: x,
|
||||
})
|
||||
.withIcon(DbIcon);
|
||||
});
|
||||
});
|
||||
|
||||
// 数据库节点
|
||||
const NodeTypeDb = new NodeType(SqlExecNodeType.Db)
|
||||
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const params = parentNode.params;
|
||||
if (params.type == 'postgres') {
|
||||
return [new TagTreeNode(`${params.id}.${params.db}.schema-menu`, 'schema', NodeTypePostgresScheamMenu).withParams(params).withIcon(SchemaIcon)];
|
||||
}
|
||||
|
||||
return [
|
||||
new TagTreeNode(`${params.id}.${params.db}.table-menu`, '表', NodeTypeTableMenu).withParams(params).withIcon(TableIcon),
|
||||
new TagTreeNode(getSqlMenuNodeKey(params.id, params.db), 'SQL', NodeTypeSqlMenu).withParams(params).withIcon(SqlIcon),
|
||||
];
|
||||
})
|
||||
.withNodeClickFunc(nodeClickChangeDb);
|
||||
|
||||
// postgres schema模式菜单
|
||||
const NodeTypePostgresScheamMenu = new NodeType(SqlExecNodeType.PgSchemaMenu)
|
||||
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const params = parentNode.params;
|
||||
const { id, db } = params;
|
||||
const schemaNames = await dbApi.pgSchemas.request({ id, db });
|
||||
return schemaNames.map((sn: any) => {
|
||||
// 将db变更为 db/schema;
|
||||
const nParams = { ...params };
|
||||
nParams.schema = sn;
|
||||
nParams.db = nParams.db + '/' + sn;
|
||||
return new TagTreeNode(`${params.id}.${params.db}.schema.${sn}`, sn, NodeTypePostgresScheam).withParams(nParams).withIcon(SchemaIcon);
|
||||
});
|
||||
})
|
||||
.withNodeClickFunc(nodeClickChangeDb);
|
||||
|
||||
// postgres schema模式
|
||||
const NodeTypePostgresScheam = new NodeType(SqlExecNodeType.PgSchema)
|
||||
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const params = parentNode.params;
|
||||
return [
|
||||
new TagTreeNode(`${params.id}.${params.db}.table-menu`, '表', NodeTypeTableMenu).withParams(params).withIcon(TableIcon),
|
||||
new TagTreeNode(getSqlMenuNodeKey(params.id, params.db), 'SQL', NodeTypeSqlMenu).withParams(params).withIcon(SqlIcon),
|
||||
];
|
||||
})
|
||||
.withNodeClickFunc(nodeClickChangeDb);
|
||||
|
||||
// 数据库表菜单节点
|
||||
const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu)
|
||||
.withContextMenuItems([
|
||||
new ContextmenuItem('reloadTables', '刷新').withIcon('RefreshRight').withOnClick((data: any) => reloadTables(data.key)),
|
||||
|
||||
new ContextmenuItem('tablesOp', '表操作').withIcon('Setting').withOnClick((data: any) => {
|
||||
const params = data.params;
|
||||
addTablesOpTab({ id: params.id, db: params.db, type: params.type, nodeKey: data.key });
|
||||
}),
|
||||
])
|
||||
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const params = parentNode.params;
|
||||
let { id, db } = params;
|
||||
// 获取当前库的所有表信息
|
||||
let tables = await DbInst.getInst(id).loadTables(db, state.reloadStatus);
|
||||
state.reloadStatus = false;
|
||||
let dbTableSize = 0;
|
||||
const tablesNode = tables.map((x: any) => {
|
||||
dbTableSize += x.dataLength + x.indexLength;
|
||||
return new TagTreeNode(`${id}.${db}.${x.tableName}`, x.tableName, NodeTypeTable)
|
||||
.withIsLeaf(true)
|
||||
.withParams({
|
||||
id,
|
||||
db,
|
||||
tableName: x.tableName,
|
||||
tableComment: x.tableComment,
|
||||
size: formatByteSize(x.dataLength + x.indexLength, 1),
|
||||
})
|
||||
.withIcon(TableIcon);
|
||||
});
|
||||
// 设置父节点参数的表大小
|
||||
parentNode.params.dbTableSize = formatByteSize(dbTableSize);
|
||||
return tablesNode;
|
||||
})
|
||||
.withNodeClickFunc(nodeClickChangeDb);
|
||||
|
||||
// 数据库sql模板菜单节点
|
||||
const NodeTypeSqlMenu = new NodeType(SqlExecNodeType.SqlMenu)
|
||||
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const params = parentNode.params;
|
||||
const id = params.id;
|
||||
const db = params.db;
|
||||
const dbs = params.dbs;
|
||||
// 加载用户保存的sql脚本
|
||||
const sqls = await dbApi.getSqlNames.request({ id: id, db: db });
|
||||
return sqls.map((x: any) => {
|
||||
return new TagTreeNode(`${id}.${db}.${x.name}`, x.name, NodeTypeSql)
|
||||
.withIsLeaf(true)
|
||||
.withParams({
|
||||
id,
|
||||
db,
|
||||
dbs,
|
||||
sqlName: x.name,
|
||||
})
|
||||
.withIcon(SqlIcon);
|
||||
});
|
||||
})
|
||||
.withNodeClickFunc(nodeClickChangeDb);
|
||||
|
||||
// 表节点类型
|
||||
const NodeTypeTable = new NodeType(SqlExecNodeType.Table).withNodeClickFunc((nodeData: TagTreeNode) => {
|
||||
const params = nodeData.params;
|
||||
loadTableData({ id: params.id, nodeKey: nodeData.key }, params.db, params.tableName);
|
||||
});
|
||||
|
||||
// sql模板节点类型
|
||||
const NodeTypeSql = new NodeType(SqlExecNodeType.Sql)
|
||||
.withNodeClickFunc((nodeData: TagTreeNode) => {
|
||||
const params = nodeData.params;
|
||||
addQueryTab({ id: params.id, nodeKey: nodeData.key, dbs: params.dbs }, params.db, params.sqlName);
|
||||
})
|
||||
.withContextMenuItems([
|
||||
new ContextmenuItem('delSql', '删除').withIcon('delete').withOnClick((data: any) => deleteSql(data.params.id, data.params.db, data.params.sqlName)),
|
||||
]);
|
||||
|
||||
const tagTreeRef: any = ref(null);
|
||||
|
||||
@@ -132,228 +377,81 @@ const state = reactive({
|
||||
activeName: '',
|
||||
reloadStatus: false,
|
||||
tabs,
|
||||
dataTabsTableHeight: '600',
|
||||
dataTabsTableHeight: 600,
|
||||
editorHeight: '600',
|
||||
tagTreeHeight: window.innerHeight - 178 + 'px',
|
||||
genSqlDialog: {
|
||||
visible: false,
|
||||
sql: '',
|
||||
},
|
||||
tablesOpHeight: '600',
|
||||
});
|
||||
|
||||
const { nowDbInst } = toRefs(state);
|
||||
|
||||
onMounted(() => {
|
||||
self.completionItemProvider?.dispose();
|
||||
setHeight();
|
||||
// 监听浏览器窗口大小变化,更新对应组件高度
|
||||
window.onresize = () => setHeight();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
dispposeCompletionItemProvider('sql');
|
||||
});
|
||||
|
||||
/**
|
||||
* 设置editor高度和数据表高度
|
||||
*/
|
||||
const setHeight = () => {
|
||||
state.editorHeight = window.innerHeight - 518 + 'px';
|
||||
state.dataTabsTableHeight = window.innerHeight - 256 + 'px';
|
||||
state.tagTreeHeight = window.innerHeight - 165 + 'px';
|
||||
state.editorHeight = window.innerHeight - 525 + 'px';
|
||||
state.dataTabsTableHeight = window.innerHeight - 255;
|
||||
state.tablesOpHeight = window.innerHeight - 220 + 'px';
|
||||
};
|
||||
|
||||
/**
|
||||
* instmap; tagPaht -> info[]
|
||||
*/
|
||||
const instMap: Map<string, any[]> = new Map();
|
||||
|
||||
const getInsts = async () => {
|
||||
const res = await dbApi.dbs.request({ pageNum: 1, pageSize: 1000 });
|
||||
if (!res.total) return;
|
||||
for (const db of res.list) {
|
||||
const tagPath = db.tagPath;
|
||||
let dbInsts = instMap.get(tagPath) || [];
|
||||
dbInsts.push(db);
|
||||
instMap.set(tagPath, dbInsts?.sort());
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 加载树节点
|
||||
* @param {Object} node
|
||||
* @param {Object} resolve
|
||||
*/
|
||||
const loadNode = async (node: any) => {
|
||||
// 一级为tagPath
|
||||
if (node.level === 0) {
|
||||
await getInsts();
|
||||
const tagPaths = instMap.keys();
|
||||
const tagNodes = [];
|
||||
for (let tagPath of tagPaths) {
|
||||
tagNodes.push(new TagTreeNode(tagPath, tagPath));
|
||||
}
|
||||
return tagNodes;
|
||||
}
|
||||
|
||||
const data = node.data;
|
||||
const nodeType = data.type;
|
||||
const params = data.params;
|
||||
|
||||
// 点击tagPath -> 加载数据库实例信息列表
|
||||
if (nodeType === TagTreeNode.TagPath) {
|
||||
const dbInfos = instMap.get(data.key);
|
||||
return dbInfos?.map((x: any) => {
|
||||
return new TagTreeNode(`${data.key}.${x.id}`, x.name, NodeType.DbInst).withParams(x);
|
||||
});
|
||||
}
|
||||
|
||||
// 点击数据库实例 -> 加载库列表
|
||||
if (nodeType === NodeType.DbInst) {
|
||||
const dbs = params.database.split(' ')?.sort();
|
||||
console.log(dbs);
|
||||
return dbs.map((x: any) => {
|
||||
return new TagTreeNode(`${data.key}.${x}`, x, NodeType.Db).withParams({
|
||||
tagPath: params.tagPath,
|
||||
id: params.id,
|
||||
name: params.name,
|
||||
type: params.type,
|
||||
dbs: dbs,
|
||||
db: x,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 点击数据库 -> 加载 表&Sql 菜单
|
||||
if (nodeType === NodeType.Db) {
|
||||
return [
|
||||
new TagTreeNode(`${params.id}.${params.db}.table-menu`, '表', NodeType.TableMenu).withParams(params),
|
||||
new TagTreeNode(getSqlMenuNodeKey(params.id, params.db), 'SQL', NodeType.SqlMenu).withParams(params),
|
||||
];
|
||||
}
|
||||
|
||||
// 点击表菜单 -> 加载表列表
|
||||
if (nodeType === NodeType.TableMenu) {
|
||||
return await getTables(params);
|
||||
}
|
||||
|
||||
if (nodeType === NodeType.SqlMenu) {
|
||||
return await loadSqls(params.id, params.db, params.dbs);
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
const nodeClick = async (data: any) => {
|
||||
const params = data.params;
|
||||
const nodeKey = data.key;
|
||||
const dataType = data.type;
|
||||
// 点击数据库,修改当前数据库信息
|
||||
if (dataType === NodeType.Db || dataType === NodeType.SqlMenu || dataType === NodeType.TableMenu || dataType === NodeType.DbInst) {
|
||||
changeSchema({ id: params.id, name: params.name, type: params.type, tagPath: params.tagPath, databases: params.database }, params.db);
|
||||
return;
|
||||
}
|
||||
|
||||
// 点击表加载表数据tab
|
||||
if (dataType === NodeType.Table) {
|
||||
await loadTableData({ id: params.id, nodeKey: nodeKey }, params.db, params.tableName);
|
||||
return;
|
||||
}
|
||||
|
||||
// 点击表加载表数据tab
|
||||
if (dataType === NodeType.Sql) {
|
||||
await addQueryTab({ id: params.id, nodeKey: nodeKey, dbs: params.dbs }, params.db, params.sqlName);
|
||||
}
|
||||
};
|
||||
|
||||
const getContextmenuItems = (data: any) => {
|
||||
const dataType = data.type;
|
||||
if (dataType === NodeType.TableMenu) {
|
||||
return [{ contextMenuClickId: ContextmenuClickId.ReloadTable, txt: '刷新', icon: 'RefreshRight' }];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
// 当前右击菜单点击事件
|
||||
const onCurrentContextmenuClick = (clickData: any) => {
|
||||
const clickId = clickData.id;
|
||||
if (clickId == ContextmenuClickId.ReloadTable) {
|
||||
reloadTables(clickData.item.key);
|
||||
}
|
||||
};
|
||||
|
||||
const getTables = async (params: any) => {
|
||||
const { id, db } = params;
|
||||
let tables = await DbInst.getInst(id).loadTables(db, state.reloadStatus);
|
||||
state.reloadStatus = false;
|
||||
return tables.map((x: any) => {
|
||||
return new TagTreeNode(`${id}.${db}.${x.tableName}`, x.tableName, NodeType.Table).withIsLeaf(true).withParams({
|
||||
id,
|
||||
db,
|
||||
tableName: x.tableName,
|
||||
tableComment: x.tableComment,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 加载用户保存的sql脚本
|
||||
*
|
||||
* @param inst
|
||||
* @param schema
|
||||
*/
|
||||
const loadSqls = async (id: any, db: string, dbs: any) => {
|
||||
const sqls = await dbApi.getSqlNames.request({ id: id, db: db });
|
||||
return sqls.map((x: any) => {
|
||||
return new TagTreeNode(`${id}.${db}.${x.name}`, x.name, NodeType.Sql).withIsLeaf(true).withParams({
|
||||
id,
|
||||
db,
|
||||
dbs,
|
||||
sqlName: x.name,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 选择数据库
|
||||
const changeSchema = (inst: any, schema: string) => {
|
||||
state.nowDbInst = DbInst.getOrNewInst(inst);
|
||||
state.db = schema;
|
||||
// 选择数据库,改变当前正在操作的数据库信息
|
||||
const changeDb = (db: any, dbName: string) => {
|
||||
state.nowDbInst = DbInst.getOrNewInst(db);
|
||||
state.db = dbName;
|
||||
};
|
||||
|
||||
// 加载选中的表数据,即新增表数据操作tab
|
||||
const loadTableData = async (inst: any, schema: string, tableName: string) => {
|
||||
changeSchema(inst, schema);
|
||||
const loadTableData = async (db: any, dbName: string, tableName: string) => {
|
||||
if (tableName == '') {
|
||||
return;
|
||||
}
|
||||
changeDb(db, dbName);
|
||||
|
||||
const label = `${inst.id}:\`${schema}\`.${tableName}`;
|
||||
let tab = state.tabs.get(label);
|
||||
state.activeName = label;
|
||||
const key = `${db.id}:\`${dbName}\`.${tableName}`;
|
||||
let tab = state.tabs.get(key);
|
||||
state.activeName = key;
|
||||
// 如果存在该表tab,则直接返回
|
||||
if (tab) {
|
||||
return;
|
||||
}
|
||||
tab = new TabInfo();
|
||||
tab.key = label;
|
||||
tab.treeNodeKey = inst.nodeKey;
|
||||
tab.dbId = inst.id;
|
||||
tab.db = schema;
|
||||
tab.label = tableName;
|
||||
tab.key = key;
|
||||
tab.treeNodeKey = db.nodeKey;
|
||||
tab.dbId = db.id;
|
||||
tab.db = dbName;
|
||||
tab.type = TabType.TableData;
|
||||
tab.params = {
|
||||
...getNowDbInfo(),
|
||||
table: tableName,
|
||||
};
|
||||
state.tabs.set(label, tab);
|
||||
state.tabs.set(key, tab);
|
||||
};
|
||||
|
||||
// 新建查询panel
|
||||
const addQueryTab = async (inst: any, db: string, sqlName: string = '') => {
|
||||
if (!db || !inst.id) {
|
||||
// 新建查询tab
|
||||
const addQueryTab = async (db: any, dbName: string, sqlName: string = '') => {
|
||||
if (!dbName || !db.id) {
|
||||
ElMessage.warning('请选择数据库实例及对应的schema');
|
||||
return;
|
||||
}
|
||||
changeDb(db, dbName);
|
||||
|
||||
const dbId = inst.id;
|
||||
const dbId = db.id;
|
||||
let label;
|
||||
let key;
|
||||
// 存在sql模板名,则该模板名只允许一个tab
|
||||
if (sqlName) {
|
||||
label = `查询:${dbId}:${db}.${sqlName}`;
|
||||
label = `查询-${sqlName}`;
|
||||
key = `查询:${dbId}:${dbName}.${sqlName}`;
|
||||
} else {
|
||||
let count = 1;
|
||||
state.tabs.forEach((v) => {
|
||||
@@ -361,24 +459,66 @@ const addQueryTab = async (inst: any, db: string, sqlName: string = '') => {
|
||||
count++;
|
||||
}
|
||||
});
|
||||
label = `新查询${count}:${dbId}:${db}`;
|
||||
label = `新查询-${count}`;
|
||||
key = `新查询${count}:${dbId}:${dbName}`;
|
||||
}
|
||||
state.activeName = label;
|
||||
let tab = state.tabs.get(label);
|
||||
state.activeName = key;
|
||||
let tab = state.tabs.get(key);
|
||||
if (tab) {
|
||||
return;
|
||||
}
|
||||
tab = new TabInfo();
|
||||
tab.key = label;
|
||||
tab.treeNodeKey = inst.nodeKey;
|
||||
tab.key = key;
|
||||
tab.label = label;
|
||||
tab.treeNodeKey = db.nodeKey;
|
||||
tab.dbId = dbId;
|
||||
tab.db = db;
|
||||
tab.db = dbName;
|
||||
tab.type = TabType.Query;
|
||||
tab.params = {
|
||||
...getNowDbInfo(),
|
||||
sqlName: sqlName,
|
||||
dbs: inst.dbs,
|
||||
dbs: db.dbs,
|
||||
};
|
||||
state.tabs.set(label, tab);
|
||||
state.tabs.set(key, tab);
|
||||
|
||||
// 注册当前sql编辑框提示词
|
||||
registerDbCompletionItemProvider('sql', tab.dbId, tab.db, tab.params.dbs);
|
||||
};
|
||||
|
||||
/**
|
||||
* 添加数据操作tab
|
||||
* @param inst
|
||||
*/
|
||||
const addTablesOpTab = async (db: any) => {
|
||||
const dbName = db.db;
|
||||
if (!db || !db.id) {
|
||||
ElMessage.warning('请选择数据库实例及对应的schema');
|
||||
return;
|
||||
}
|
||||
changeDb(db, dbName);
|
||||
|
||||
const dbId = db.id;
|
||||
let key = `表操作:${dbId}:${dbName}.tablesOp`;
|
||||
state.activeName = key;
|
||||
|
||||
let tab = state.tabs.get(key);
|
||||
if (tab) {
|
||||
return;
|
||||
}
|
||||
tab = new TabInfo();
|
||||
tab.key = key;
|
||||
tab.label = `表操作-${dbName}`;
|
||||
tab.treeNodeKey = db.nodeKey;
|
||||
tab.dbId = dbId;
|
||||
tab.db = dbName;
|
||||
tab.type = TabType.TablesOp;
|
||||
tab.params = {
|
||||
...getNowDbInfo(),
|
||||
id: db.id,
|
||||
db: dbName,
|
||||
type: db.type,
|
||||
};
|
||||
state.tabs.set(key, tab);
|
||||
};
|
||||
|
||||
const onRemoveTab = (targetName: string) => {
|
||||
@@ -397,6 +537,7 @@ const onRemoveTab = (targetName: string) => {
|
||||
}
|
||||
state.tabs.delete(targetName);
|
||||
state.activeName = activeName;
|
||||
onTabChange();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -406,23 +547,34 @@ const onTabChange = () => {
|
||||
state.db = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const nowTab = state.tabs.get(state.activeName);
|
||||
state.nowDbInst = DbInst.getInst(nowTab?.dbId);
|
||||
state.db = nowTab?.db as string;
|
||||
};
|
||||
|
||||
const onGenerateInsertSql = async (sql: string) => {
|
||||
state.genSqlDialog.sql = sql;
|
||||
state.genSqlDialog.visible = true;
|
||||
if (nowTab?.type == TabType.Query) {
|
||||
// 注册sql提示
|
||||
registerDbCompletionItemProvider('sql', nowTab.dbId, nowTab.db, nowTab.params.dbs);
|
||||
}
|
||||
};
|
||||
|
||||
const reloadSqls = (dbId: number, db: string) => {
|
||||
tagTreeRef.value.reloadNode(getSqlMenuNodeKey(dbId, db));
|
||||
};
|
||||
|
||||
const deleteSqlScript = (ti: TabInfo) => {
|
||||
reloadSqls(ti.dbId, ti.db);
|
||||
onRemoveTab(ti.key);
|
||||
const deleteSql = async (dbId: any, db: string, sqlName: string) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除【${sqlName}】该SQL内容?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
});
|
||||
await dbApi.deleteDbSql.request({ id: dbId, db: db, name: sqlName });
|
||||
ElMessage.success('删除成功');
|
||||
reloadSqls(dbId, db);
|
||||
} catch (err) {
|
||||
//
|
||||
}
|
||||
};
|
||||
|
||||
const getSqlMenuNodeKey = (dbId: number, db: string) => {
|
||||
@@ -433,50 +585,53 @@ const reloadTables = (nodeKey: string) => {
|
||||
state.reloadStatus = true;
|
||||
tagTreeRef.value.reloadNode(nodeKey);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取当前操作的数据库信息
|
||||
*/
|
||||
const getNowDbInfo = () => {
|
||||
const di = state.nowDbInst;
|
||||
return {
|
||||
tagPath: di.tagPath,
|
||||
id: di.id,
|
||||
name: di.name,
|
||||
type: di.type,
|
||||
host: di.host,
|
||||
dbName: state.db,
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.sql-file-exec {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
vertical-align: middle;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
}
|
||||
.db-sql-exec {
|
||||
.db-table-size {
|
||||
color: #c4c9c4;
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.sqlEditor {
|
||||
font-size: 8pt;
|
||||
font-weight: 600;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
#data-exec {
|
||||
min-height: calc(100vh - 155px);
|
||||
|
||||
.editor-move-resize {
|
||||
cursor: n-resize;
|
||||
height: 3px;
|
||||
text-align: center;
|
||||
}
|
||||
.el-tabs {
|
||||
--el-tabs-header-height: 30px;
|
||||
}
|
||||
|
||||
#data-exec {
|
||||
min-height: calc(100vh - 155px);
|
||||
.el-tabs__header {
|
||||
margin: 0 0 5px;
|
||||
|
||||
.el-tabs__header {
|
||||
margin: 0 0 5px;
|
||||
.el-tabs__item {
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-tabs__item {
|
||||
padding: 0 5px;
|
||||
.el-tabs__nav-next,
|
||||
.el-tabs__nav-prev {
|
||||
line-height: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.update_field_active {
|
||||
background-color: var(--el-color-success);
|
||||
}
|
||||
|
||||
.instances-pop-form {
|
||||
.el-form-item {
|
||||
margin-bottom: unset;
|
||||
.update_field_active {
|
||||
background-color: var(--el-color-success);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,8 +10,8 @@ export const dbApi = {
|
||||
tableInfos: Api.newGet('/dbs/{id}/t-infos'),
|
||||
tableIndex: Api.newGet('/dbs/{id}/t-index'),
|
||||
tableDdl: Api.newGet('/dbs/{id}/t-create-ddl'),
|
||||
tableMetadata: Api.newGet('/dbs/{id}/t-metadata'),
|
||||
columnMetadata: Api.newGet('/dbs/{id}/c-metadata'),
|
||||
pgSchemas: Api.newGet('/dbs/{id}/pg/schemas'),
|
||||
// 获取表即列提示
|
||||
hintTables: Api.newGet('/dbs/{id}/hint-tables'),
|
||||
sqlExec: Api.newPost('/dbs/{id}/exec-sql'),
|
||||
@@ -27,8 +27,9 @@ export const dbApi = {
|
||||
|
||||
// 获取权限列表
|
||||
instances: Api.newGet('/instances'),
|
||||
getInstance: Api.newGet("/instances/{instanceId}"),
|
||||
getInstance: Api.newGet('/instances/{instanceId}'),
|
||||
getAllDatabase: Api.newGet('/instances/{instanceId}/databases'),
|
||||
testConn: Api.newPost('/instances/test-conn'),
|
||||
saveInstance: Api.newPost('/instances'),
|
||||
getInstancePwd: Api.newGet('/instances/{id}/pwd'),
|
||||
deleteInstance: Api.newDelete('/instances/{id}'),
|
||||
|
||||
@@ -1,335 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-table
|
||||
@cell-dblclick="(row: any, column: any, cell: any, event: any) => cellClick(row, column, cell)"
|
||||
@sort-change="(sort: any) => onTableSortChange(sort)"
|
||||
@selection-change="onDataSelectionChange"
|
||||
:data="datas"
|
||||
size="small"
|
||||
:max-height="tableHeight"
|
||||
v-loading="loading"
|
||||
element-loading-text="查询中..."
|
||||
:empty-text="emptyText"
|
||||
highlight-current-row
|
||||
stripe
|
||||
border
|
||||
class="mt5"
|
||||
>
|
||||
<el-table-column v-if="datas.length > 0 && table" type="selection" width="35" />
|
||||
|
||||
<template v-for="(item, index) in columns">
|
||||
<el-table-column
|
||||
min-width="100"
|
||||
:width="DbInst.flexColumnWidth(item.columnName, datas)"
|
||||
align="center"
|
||||
v-if="item.show"
|
||||
:key="index"
|
||||
:prop="item.columnName"
|
||||
:label="item.columnName"
|
||||
show-overflow-tooltip
|
||||
:sortable="sortable"
|
||||
>
|
||||
<template #header v-if="showColumnTip">
|
||||
<el-tooltip raw-content placement="top" effect="customized">
|
||||
<template #content> {{ getColumnTip(item) }} </template>
|
||||
{{ item.columnName }}
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, watch, reactive, toRefs } from 'vue';
|
||||
import { DbInst, UpdateFieldsMeta, FieldsMeta } from '../db';
|
||||
|
||||
const emits = defineEmits(['sortChange', 'deleteData', 'selectionChange', 'changeUpdatedField'])
|
||||
|
||||
const props = defineProps({
|
||||
dbId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
dbType: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
db: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
table: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
data: {
|
||||
type: Array,
|
||||
},
|
||||
columns: {
|
||||
type: Array<any>,
|
||||
},
|
||||
sortable: {
|
||||
type: [String, Boolean],
|
||||
default: false,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
emptyText: {
|
||||
type: String,
|
||||
default: '暂无数据',
|
||||
},
|
||||
showColumnTip: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '600'
|
||||
}
|
||||
})
|
||||
|
||||
const state = reactive({
|
||||
dbId: 0, // 当前选中操作的数据库实例
|
||||
dbType: '',
|
||||
db: '', // 数据库名
|
||||
table: '', // 当前的表名
|
||||
datas: [],
|
||||
columns: [],
|
||||
sortable: false,
|
||||
loading: false,
|
||||
selectionDatas: [] as any,
|
||||
showColumnTip: false,
|
||||
tableHeight: '600',
|
||||
emptyText: '',
|
||||
updatedFields: [] as UpdateFieldsMeta[],// 各个tab表被修改的字段信息
|
||||
});
|
||||
|
||||
const {
|
||||
tableHeight,
|
||||
datas,
|
||||
sortable,
|
||||
loading,
|
||||
showColumnTip,
|
||||
} = toRefs(state);
|
||||
|
||||
watch(props, (newValue: any) => {
|
||||
setState(newValue);
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
console.log('in DbTable mounted');
|
||||
setState(props);
|
||||
})
|
||||
|
||||
const setState = (props: any) => {
|
||||
state.dbId = props.dbId;
|
||||
state.dbType = props.dbType;
|
||||
state.db = props.db;
|
||||
state.table = props.table;
|
||||
state.datas = props.data;
|
||||
state.tableHeight = props.height;
|
||||
state.sortable = props.sortable;
|
||||
state.loading = props.loading;
|
||||
state.columns = props.columns;
|
||||
state.showColumnTip = props.showColumnTip;
|
||||
state.emptyText = props.emptyText;
|
||||
}
|
||||
|
||||
const getColumnTip = (column: any) => {
|
||||
const comment = column.columnComment;
|
||||
return `${column.columnType} ${comment ? ' | ' + comment : ''}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 表排序字段变更
|
||||
*/
|
||||
const onTableSortChange = async (sort: any) => {
|
||||
if (!sort.prop) {
|
||||
return;
|
||||
}
|
||||
cancelUpdateFields();
|
||||
emits('sortChange', sort);
|
||||
};
|
||||
|
||||
const onDataSelectionChange = (datas: []) => {
|
||||
state.selectionDatas = datas;
|
||||
emits('selectionChange', datas);
|
||||
};
|
||||
|
||||
// 监听单元格点击事件
|
||||
const cellClick = (row: any, column: any, cell: any) => {
|
||||
const property = column.property;
|
||||
// 如果当前操作的表名不存在 或者 当前列的property不存在(如多选框),则不允许修改当前单元格内容
|
||||
if (!state.table || !property) {
|
||||
return;
|
||||
}
|
||||
let div: HTMLElement = cell.children[0];
|
||||
if (div && div.tagName === 'DIV') {
|
||||
// 转为字符串比较,可能存在数字等
|
||||
let text = (row[property] || row[property] == 0 ? row[property] : '') + '';
|
||||
let input = document.createElement('input');
|
||||
input.setAttribute('value', text);
|
||||
// 将表格width也赋值于输入框,避免输入框长度超过表格长度
|
||||
input.setAttribute('style', 'height:23px;text-align:center;border:none;' + div.getAttribute('style'));
|
||||
cell.replaceChildren(input);
|
||||
input.focus();
|
||||
input.addEventListener('blur', async () => {
|
||||
row[property] = input.value;
|
||||
cell.replaceChildren(div);
|
||||
if (input.value !== text) {
|
||||
let currentUpdatedFields = state.updatedFields
|
||||
const dbInst = getNowDbInst();
|
||||
// 主键
|
||||
const primaryKey = await dbInst.loadTableColumn(state.db, state.table);
|
||||
const primaryKeyValue = row[primaryKey.columnName];
|
||||
// 更新字段列信息
|
||||
const updateColumn = await dbInst.loadTableColumn(state.db, state.table, property);
|
||||
const newField = {
|
||||
div, row,
|
||||
fieldName: property,
|
||||
fieldType: updateColumn.columnType,
|
||||
oldValue: text,
|
||||
newValue: input.value
|
||||
} as FieldsMeta;
|
||||
|
||||
// 被修改的字段
|
||||
const primaryKeyFields = currentUpdatedFields.filter((meta) => meta.primaryKey === primaryKeyValue)
|
||||
let hasKey = false;
|
||||
if (primaryKeyFields.length <= 0) {
|
||||
primaryKeyFields[0] = {
|
||||
primaryKey: primaryKeyValue,
|
||||
primaryKeyName: primaryKey.columnName,
|
||||
primaryKeyType: primaryKey.columnType,
|
||||
fields: [newField]
|
||||
}
|
||||
} else {
|
||||
hasKey = true
|
||||
let hasField = primaryKeyFields[0].fields.some(a => {
|
||||
if (a.fieldName === newField.fieldName) {
|
||||
a.newValue = newField.newValue
|
||||
}
|
||||
return a.fieldName === newField.fieldName
|
||||
})
|
||||
if (!hasField) {
|
||||
primaryKeyFields[0].fields.push(newField)
|
||||
}
|
||||
}
|
||||
let fields = primaryKeyFields[0].fields
|
||||
|
||||
const fieldsParam = fields.filter((a) => {
|
||||
if (a.fieldName === column.property) {
|
||||
a.newValue = input.value
|
||||
}
|
||||
return a.fieldName === column.property
|
||||
})
|
||||
|
||||
const field = fieldsParam.length > 0 && fieldsParam[0] || {} as FieldsMeta
|
||||
if (field.oldValue === input.value) { // 新值=旧值
|
||||
// 删除数据
|
||||
div.classList.remove('update_field_active')
|
||||
let delIndex: number[] = [];
|
||||
currentUpdatedFields.forEach((a, i) => {
|
||||
if (a.primaryKey === primaryKeyValue) {
|
||||
a.fields = a.fields && a.fields.length > 0 ? a.fields.filter(f => f.fieldName !== column.property) : [];
|
||||
a.fields.length <= 0 && delIndex.push(i)
|
||||
}
|
||||
});
|
||||
delIndex.forEach(i => delete currentUpdatedFields[i])
|
||||
currentUpdatedFields = currentUpdatedFields.filter(a => a)
|
||||
} else {
|
||||
// 新增数据
|
||||
div.classList.add('update_field_active')
|
||||
if (hasKey) {
|
||||
currentUpdatedFields.forEach((value, index, array) => {
|
||||
if (value.primaryKey === primaryKeyValue) {
|
||||
array[index].fields = fields
|
||||
}
|
||||
})
|
||||
} else {
|
||||
currentUpdatedFields.push({
|
||||
primaryKey: primaryKeyValue,
|
||||
primaryKeyName: primaryKey.columnName,
|
||||
primaryKeyType: primaryKey.columnType,
|
||||
fields
|
||||
})
|
||||
}
|
||||
}
|
||||
state.updatedFields = currentUpdatedFields;
|
||||
changeUpdatedField();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const submitUpdateFields = () => {
|
||||
const dbInst = DbInst.getInst(state.dbId)
|
||||
let currentUpdatedFields = state.updatedFields;
|
||||
if (currentUpdatedFields.length <= 0) {
|
||||
return;
|
||||
}
|
||||
const db = state.db;
|
||||
let res = '';
|
||||
let divs: HTMLElement[] = [];
|
||||
currentUpdatedFields.forEach(a => {
|
||||
let sql = `UPDATE ${dbInst.wrapName(state.table)} SET `;
|
||||
let primaryKey = a.primaryKey;
|
||||
let primaryKeyType = a.primaryKeyType;
|
||||
let primaryKeyName = a.primaryKeyName;
|
||||
a.fields.forEach(f => {
|
||||
sql += ` ${dbInst.wrapName(f.fieldName)} = ${DbInst.wrapColumnValue(f.fieldType, f.newValue)},`
|
||||
// 如果修改的字段是主键
|
||||
if (f.fieldName === primaryKeyName) {
|
||||
primaryKey = f.oldValue
|
||||
}
|
||||
divs.push(f.div)
|
||||
})
|
||||
sql = sql.substring(0, sql.length - 1)
|
||||
sql += ` WHERE ${dbInst.wrapName(primaryKeyName)} = ${DbInst.wrapColumnValue(primaryKeyType, primaryKey)} ;`
|
||||
res += sql;
|
||||
})
|
||||
|
||||
dbInst.promptExeSql(db, res, () => { }, () => {
|
||||
currentUpdatedFields = [];
|
||||
divs.forEach(a => {
|
||||
a.classList.remove('update_field_active');
|
||||
})
|
||||
state.updatedFields = [];
|
||||
changeUpdatedField();
|
||||
});
|
||||
}
|
||||
|
||||
const cancelUpdateFields = () => {
|
||||
state.updatedFields.forEach((a: any) => {
|
||||
a.fields.forEach((b: any) => {
|
||||
b.div.classList.remove('update_field_active')
|
||||
b.row[b.fieldName] = b.oldValue
|
||||
})
|
||||
})
|
||||
state.updatedFields = [];
|
||||
changeUpdatedField();
|
||||
}
|
||||
|
||||
|
||||
const changeUpdatedField = () => {
|
||||
emits('changeUpdatedField', state.updatedFields);
|
||||
}
|
||||
|
||||
const getNowDbInst = () => {
|
||||
return DbInst.getInst(state.dbId);
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
submitUpdateFields,
|
||||
cancelUpdateFields
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.update_field_active {
|
||||
background-color: var(--el-color-success);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,712 @@
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
<div class="toolbar">
|
||||
<div class="fl">
|
||||
<el-link @click="onRunSql()" :underline="false" class="ml15" icon="VideoPlay"> </el-link>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-tooltip :show-after="1000" class="box-item" effect="dark" content="format sql" placement="top">
|
||||
<el-link @click="formatSql()" type="primary" :underline="false" icon="MagicStick"> </el-link>
|
||||
</el-tooltip>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-tooltip :show-after="1000" class="box-item" effect="dark" content="commit" placement="top">
|
||||
<el-link @click="onCommit()" type="success" :underline="false" icon="CircleCheck"> </el-link>
|
||||
</el-tooltip>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-upload
|
||||
class="sql-file-exec"
|
||||
:before-upload="beforeUpload"
|
||||
:on-success="execSqlFileSuccess"
|
||||
:headers="{ Authorization: token }"
|
||||
:action="getUploadSqlFileUrl()"
|
||||
:show-file-list="false"
|
||||
name="file"
|
||||
multiple
|
||||
:limit="100"
|
||||
>
|
||||
<el-tooltip :show-after="1000" class="box-item" effect="dark" content="SQL脚本执行" placement="top">
|
||||
<el-link type="success" :underline="false" icon="Document"></el-link>
|
||||
</el-tooltip>
|
||||
</el-upload>
|
||||
</div>
|
||||
|
||||
<div style="float: right" class="fl">
|
||||
<el-button @click="saveSql()" type="primary" icon="document-add" plain size="small">保存SQL</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MonacoEditor ref="monacoEditorRef" class="mt5" v-model="state.sql" language="sql" :height="state.editorHeight" :id="'MonacoTextarea-' + getKey()" />
|
||||
|
||||
<div class="editor-move-resize" @mousedown="onDragSetHeight">
|
||||
<el-icon>
|
||||
<Minus />
|
||||
</el-icon>
|
||||
</div>
|
||||
|
||||
<div class="mt5 sql-exec-res">
|
||||
<el-tabs v-if="state.execResTabs.length > 0" @tab-remove="onRemoveTab" style="width: 100%" v-model="state.activeTab">
|
||||
<el-tab-pane closable v-for="dt in state.execResTabs" :label="dt.id" :name="dt.id" :key="dt.id">
|
||||
<template #label>
|
||||
<el-popover :show-after="1000" placement="top-start" title="执行信息" trigger="hover" :width="300">
|
||||
<template #reference>
|
||||
<div>
|
||||
<span>
|
||||
<span v-if="dt.loading">
|
||||
<SvgIcon class="mb2 is-loading" name="Loading" color="var(--el-color-primary)" />
|
||||
</span>
|
||||
<span v-else>
|
||||
<SvgIcon class="mb2" v-if="!dt.errorMsg" name="CircleCheck" color="var(--el-color-success)" />
|
||||
<SvgIcon class="mb2" v-if="dt.errorMsg" name="CircleClose" color="var(--el-color-error)" />
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span> 结果{{ dt.id }} </span>
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
<el-descriptions v-if="dt.sql" :column="1" size="small">
|
||||
<el-descriptions-item>
|
||||
<div style="width: 280px">
|
||||
<el-text size="small" truncated :title="dt.sql"> {{ dt.sql }} </el-text>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="耗时 :"> {{ dt.execTime }}ms </el-descriptions-item>
|
||||
<el-descriptions-item label="结果集 :">
|
||||
{{ dt.data?.length }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
</el-popover>
|
||||
</template>
|
||||
|
||||
<el-row>
|
||||
<span v-if="dt.hasUpdatedFileds" class="mt5">
|
||||
<span>
|
||||
<el-link type="success" :underline="false" @click="submitUpdateFields(dt)"><span style="font-size: 12px">提交</span></el-link>
|
||||
</span>
|
||||
<span>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
<el-link type="warning" :underline="false" @click="cancelUpdateFields(dt)"><span style="font-size: 12px">取消</span></el-link>
|
||||
</span>
|
||||
</span>
|
||||
</el-row>
|
||||
<db-table-data
|
||||
v-if="!dt.errorMsg"
|
||||
:ref="(el) => (dt.dbTableRef = el)"
|
||||
:db-id="dbId"
|
||||
:db="dbName"
|
||||
:data="dt.data"
|
||||
:table="dt.table"
|
||||
:columns="dt.tableColumn"
|
||||
:loading="dt.loading"
|
||||
:height="tableDataHeight"
|
||||
empty-text="tips: select *开头的单表查询或点击表名默认查询的数据,可双击数据在线修改"
|
||||
@change-updated-field="changeUpdatedField($event, dt)"
|
||||
@data-delete="onDeleteData($event, dt)"
|
||||
></db-table-data>
|
||||
|
||||
<el-result v-else icon="error" title="执行失败" :sub-title="dt.errorMsg"> </el-result>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { h, nextTick, watch, onMounted, reactive, toRefs, ref } from 'vue';
|
||||
import { getToken } from '@/common/utils/storage';
|
||||
import { notBlank } from '@/common/assert';
|
||||
import { format as sqlFormatter } from 'sql-formatter';
|
||||
import config from '@/common/config';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
|
||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
import { editor } from 'monaco-editor';
|
||||
|
||||
import DbTableData from '@/views/ops/db/component/table/DbTableData.vue';
|
||||
import { DbInst } from '../../db';
|
||||
import { dbApi } from '../../api';
|
||||
|
||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||
import { joinClientParams } from '@/common/request';
|
||||
import { buildProgressProps } from '@/components/progress-notify/progress-notify';
|
||||
import ProgressNotify from '@/components/progress-notify/progress-notify.vue';
|
||||
import { ElNotification } from 'element-plus';
|
||||
import syssocket from '@/common/syssocket';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
import { getDbDialect } from '../../dialect';
|
||||
|
||||
const emits = defineEmits(['saveSqlSuccess']);
|
||||
|
||||
const props = defineProps({
|
||||
dbId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
dbName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
// sql脚本名,若有则去加载该sql内容
|
||||
sqlName: {
|
||||
type: String,
|
||||
},
|
||||
editorHeight: {
|
||||
type: String,
|
||||
default: '600',
|
||||
},
|
||||
});
|
||||
|
||||
class ExecResTab {
|
||||
id: number;
|
||||
|
||||
/**
|
||||
* 当前结果集对应的sql
|
||||
*/
|
||||
sql: string;
|
||||
|
||||
loading: boolean;
|
||||
|
||||
dbTableRef: any;
|
||||
|
||||
tableColumn: any[] = [];
|
||||
|
||||
data: any[] = [];
|
||||
|
||||
execTime: number;
|
||||
|
||||
/**
|
||||
* 当前单表操作sql关联的表信息
|
||||
*/
|
||||
table: string;
|
||||
|
||||
/**
|
||||
* 是否有更新字段
|
||||
*/
|
||||
hasUpdatedFileds: boolean;
|
||||
|
||||
errorMsg: string;
|
||||
|
||||
constructor(id: number) {
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
|
||||
const token = getToken();
|
||||
const monacoEditorRef: any = ref(null);
|
||||
|
||||
let monacoEditor: editor.IStandaloneCodeEditor;
|
||||
|
||||
const state = reactive({
|
||||
token,
|
||||
sql: '', // 当前编辑器的sql内容s
|
||||
sqlName: '' as any, // sql模板名称
|
||||
execResTabs: [] as ExecResTab[],
|
||||
activeTab: 1,
|
||||
editorHeight: '500',
|
||||
tableDataHeight: 255 as any,
|
||||
});
|
||||
|
||||
const { tableDataHeight } = toRefs(state);
|
||||
|
||||
watch(
|
||||
() => props.editorHeight,
|
||||
(newValue: any) => {
|
||||
state.editorHeight = newValue;
|
||||
}
|
||||
);
|
||||
|
||||
const getNowDbInst = () => {
|
||||
return DbInst.getInst(props.dbId);
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
console.log('in query mounted');
|
||||
state.editorHeight = props.editorHeight;
|
||||
|
||||
// 默认新建一个结果集tab
|
||||
state.execResTabs.push(new ExecResTab(1));
|
||||
|
||||
state.sqlName = props.sqlName;
|
||||
if (props.sqlName) {
|
||||
const res = await dbApi.getSql.request({ id: props.dbId, type: 1, db: props.dbName, name: props.sqlName });
|
||||
state.sql = res.sql;
|
||||
}
|
||||
nextTick(() => {
|
||||
setTimeout(() => initMonacoEditor(), 50);
|
||||
});
|
||||
await getNowDbInst().loadDbHints(props.dbName);
|
||||
});
|
||||
|
||||
const onRemoveTab = (targetId: number) => {
|
||||
let activeTab = state.activeTab;
|
||||
const tabs = [...state.execResTabs];
|
||||
for (let i = 0; i < tabs.length; i++) {
|
||||
const tabId = tabs[i].id;
|
||||
if (tabId !== targetId) {
|
||||
continue;
|
||||
}
|
||||
const nextTab = tabs[i + 1] || tabs[i - 1];
|
||||
if (nextTab) {
|
||||
activeTab = nextTab.id;
|
||||
} else {
|
||||
activeTab = 0;
|
||||
}
|
||||
state.execResTabs.splice(i, 1);
|
||||
state.activeTab = activeTab;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 拖拽改变sql编辑区和查询结果区高度
|
||||
*/
|
||||
const onDragSetHeight = () => {
|
||||
document.onmousemove = (e) => {
|
||||
e.preventDefault();
|
||||
//得到鼠标拖动的宽高距离:取绝对值
|
||||
state.editorHeight = `${document.getElementById('MonacoTextarea-' + getKey())!.clientHeight + e.movementY}px`;
|
||||
state.tableDataHeight -= e.movementY;
|
||||
};
|
||||
document.onmouseup = () => {
|
||||
document.onmousemove = null;
|
||||
};
|
||||
};
|
||||
|
||||
const getKey = () => {
|
||||
if (props.sqlName) {
|
||||
return `${props.dbId}:${props.dbName}.${props.sqlName}`;
|
||||
}
|
||||
return props.dbId + ':' + props.dbName;
|
||||
};
|
||||
|
||||
/**
|
||||
* 执行sql
|
||||
*/
|
||||
const onRunSql = async (newTab = false) => {
|
||||
// 没有选中的文本,则为全部文本
|
||||
let sql = getSql() as string;
|
||||
notBlank(sql && sql.trim(), '请选中需要执行的sql');
|
||||
// 去除字符串前的空格、换行等
|
||||
sql = sql.replace(/(^\s*)/g, '');
|
||||
let execRemark = '';
|
||||
let canRun = true;
|
||||
if (
|
||||
sql.startsWith('update') ||
|
||||
sql.startsWith('UPDATE') ||
|
||||
sql.startsWith('INSERT') ||
|
||||
sql.startsWith('insert') ||
|
||||
sql.startsWith('DELETE') ||
|
||||
sql.startsWith('delete')
|
||||
) {
|
||||
const res: any = await ElMessageBox.prompt('请输入备注', 'Tip', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputPattern: /^[\s\S]*.*[^\s][\s\S]*$/,
|
||||
inputErrorMessage: '请输入执行该sql的备注信息',
|
||||
});
|
||||
execRemark = res.value;
|
||||
if (!execRemark) {
|
||||
canRun = false;
|
||||
}
|
||||
}
|
||||
if (!canRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
let execRes: ExecResTab;
|
||||
let i = 0;
|
||||
let id;
|
||||
// 新tab执行,或者tabs为0,则新建tab执行sql
|
||||
if (newTab || state.execResTabs.length == 0) {
|
||||
// 取最后一个tab的id + 1
|
||||
id = state.execResTabs.length == 0 ? 1 : state.execResTabs[state.execResTabs.length - 1].id + 1;
|
||||
execRes = new ExecResTab(id);
|
||||
state.execResTabs.push(execRes);
|
||||
i = state.execResTabs.length - 1;
|
||||
} else {
|
||||
// 不是新建tab执行,则在当前激活的tab上执行sql
|
||||
i = state.execResTabs.findIndex((x) => x.id == state.activeTab);
|
||||
execRes = state.execResTabs[i];
|
||||
id = execRes.id;
|
||||
}
|
||||
|
||||
state.activeTab = id;
|
||||
const startTime = new Date().getTime();
|
||||
try {
|
||||
execRes.loading = true;
|
||||
execRes.errorMsg = '';
|
||||
execRes.sql = '';
|
||||
|
||||
const colAndData: any = await getNowDbInst().runSql(props.dbName, sql, execRemark);
|
||||
if (!colAndData.res || colAndData.res.length === 0) {
|
||||
ElMessage.warning('未查询到结果集');
|
||||
}
|
||||
// 要实时响应,故需要用索引改变数据才生效
|
||||
state.execResTabs[i].data = colAndData.res;
|
||||
// 兼容表格字段配置
|
||||
state.execResTabs[i].tableColumn = colAndData.colNames.map((x: any) => {
|
||||
return {
|
||||
columnName: x,
|
||||
show: true,
|
||||
};
|
||||
});
|
||||
cancelUpdateFields(execRes);
|
||||
} catch (e: any) {
|
||||
execRes.data = [];
|
||||
execRes.tableColumn = [];
|
||||
execRes.table = '';
|
||||
execRes.errorMsg = e.msg;
|
||||
return;
|
||||
} finally {
|
||||
state.execResTabs[i].loading = false;
|
||||
execRes.sql = sql;
|
||||
execRes.execTime = new Date().getTime() - startTime;
|
||||
}
|
||||
|
||||
// 即只有以该字符串开头的sql才可修改表数据内容
|
||||
if (sql.startsWith('SELECT *') || sql.startsWith('select *') || sql.startsWith('SELECT\n *')) {
|
||||
const tableName = sql.split(/from/i)[1];
|
||||
if (tableName) {
|
||||
const tn = tableName.trim().split(' ')[0].split('\n')[0];
|
||||
execRes.table = tn;
|
||||
execRes.table = tn;
|
||||
} else {
|
||||
execRes.table = '';
|
||||
}
|
||||
} else {
|
||||
execRes.table = '';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取sql,如果有鼠标选中,则返回选中内容,否则返回输入框内所有内容
|
||||
*/
|
||||
const getSql = () => {
|
||||
let res = '' as string | undefined;
|
||||
// 编辑器还没初始化
|
||||
if (!monacoEditor?.getModel) {
|
||||
return res;
|
||||
}
|
||||
// 选择选中的sql
|
||||
let selection = monacoEditor.getSelection();
|
||||
if (selection) {
|
||||
res = monacoEditor.getModel()?.getValueInRange(selection);
|
||||
}
|
||||
// 整个编辑器的sql
|
||||
if (!res) {
|
||||
return monacoEditor.getModel()?.getValue();
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
const saveSql = async () => {
|
||||
const sql = monacoEditor.getModel()?.getValue();
|
||||
notBlank(sql, 'sql内容不能为空');
|
||||
|
||||
let sqlName = state.sqlName;
|
||||
if (!sqlName) {
|
||||
try {
|
||||
const input = await ElMessageBox.prompt('请输入SQL脚本名', 'SQL名', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputPattern: /\w+/,
|
||||
inputErrorMessage: '请输入SQL脚本名',
|
||||
});
|
||||
sqlName = input.value;
|
||||
state.sqlName = sqlName;
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await dbApi.saveSql.request({ id: props.dbId, db: props.dbName, sql: sql, type: 1, name: sqlName });
|
||||
ElMessage.success('保存成功');
|
||||
// 保存sql脚本成功事件
|
||||
emits('saveSqlSuccess', props.dbId, props.dbName);
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化sql
|
||||
*/
|
||||
const formatSql = () => {
|
||||
let selection = monacoEditor.getSelection();
|
||||
if (!selection) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formatDialect = getDbDialect(getNowDbInst().type).getFormatDialect();
|
||||
|
||||
let sql = monacoEditor.getModel()?.getValueInRange(selection);
|
||||
// 有选中sql则格式化并替换选中sql, 否则格式化编辑器所有内容
|
||||
if (sql) {
|
||||
replaceSelection(sqlFormatter(sql, { language: formatDialect }), selection);
|
||||
return;
|
||||
}
|
||||
monacoEditor.getModel()?.setValue(sqlFormatter(monacoEditor.getValue(), { language: formatDialect }));
|
||||
};
|
||||
|
||||
/**
|
||||
* 提交事务,用于没有开启自动提交事务
|
||||
*/
|
||||
const onCommit = () => {
|
||||
getNowDbInst().runSql(props.dbName, 'COMMIT;');
|
||||
ElMessage.success('COMMIT success');
|
||||
};
|
||||
|
||||
/**
|
||||
* 替换选中的内容
|
||||
*/
|
||||
const replaceSelection = (str: string, selection: any) => {
|
||||
const model = monacoEditor.getModel();
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
if (!selection) {
|
||||
model.setValue(str);
|
||||
return;
|
||||
}
|
||||
const { startLineNumber, endLineNumber, startColumn, endColumn } = selection;
|
||||
|
||||
const textBeforeSelection = model.getValueInRange({
|
||||
startLineNumber: 1,
|
||||
startColumn: 0,
|
||||
endLineNumber: startLineNumber,
|
||||
endColumn: startColumn,
|
||||
});
|
||||
|
||||
const textAfterSelection = model.getValueInRange({
|
||||
startLineNumber: endLineNumber,
|
||||
startColumn: endColumn,
|
||||
endLineNumber: model.getLineCount(),
|
||||
endColumn: model.getLineMaxColumn(model.getLineCount()),
|
||||
});
|
||||
|
||||
monacoEditor.setValue(textBeforeSelection + str + textAfterSelection);
|
||||
monacoEditor.focus();
|
||||
monacoEditor.setPosition({
|
||||
lineNumber: startLineNumber,
|
||||
column: 0,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* sql文件执行进度通知缓存
|
||||
*/
|
||||
const sqlExecNotifyMap: Map<string, any> = new Map();
|
||||
const beforeUpload = (file: File) => {
|
||||
ElMessage.success(`'${file.name}' 正在上传执行, 请关注结果通知`);
|
||||
syssocket.registerMsgHandler('execSqlFileProgress', function (message: any) {
|
||||
const content = JSON.parse(message.msg);
|
||||
const id = content.id;
|
||||
let progress = sqlExecNotifyMap.get(id);
|
||||
if (content.terminated) {
|
||||
if (progress != undefined) {
|
||||
progress.notification?.close();
|
||||
sqlExecNotifyMap.delete(id);
|
||||
progress = undefined;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (progress == undefined) {
|
||||
progress = {
|
||||
props: reactive(buildProgressProps()),
|
||||
notification: undefined,
|
||||
};
|
||||
}
|
||||
progress.props.progress.title = content.title;
|
||||
progress.props.progress.executedStatements = content.executedStatements;
|
||||
if (!sqlExecNotifyMap.has(id)) {
|
||||
progress.notification = ElNotification({
|
||||
duration: 0,
|
||||
title: message.title,
|
||||
message: h(ProgressNotify, progress.props),
|
||||
type: syssocket.getMsgType(message.type),
|
||||
showClose: false,
|
||||
});
|
||||
sqlExecNotifyMap.set(id, progress);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 执行sql成功
|
||||
const execSqlFileSuccess = (res: any) => {
|
||||
if (res.code !== 200) {
|
||||
ElMessage.error(res.msg);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取sql文件上传执行url
|
||||
const getUploadSqlFileUrl = () => {
|
||||
return `${config.baseApiUrl}/dbs/${props.dbId}/exec-sql-file?db=${props.dbName}&${joinClientParams()}`;
|
||||
};
|
||||
|
||||
const changeUpdatedField = (updatedFields: any, dt: ExecResTab) => {
|
||||
// 如果存在要更新字段,则显示提交和取消按钮
|
||||
dt.hasUpdatedFileds = updatedFields && updatedFields.size > 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* 数据删除事件
|
||||
*/
|
||||
const onDeleteData = async (deleteDatas: any, dt: ExecResTab) => {
|
||||
const db = props.dbName;
|
||||
const dbInst = getNowDbInst();
|
||||
const primaryKey = await dbInst.loadTableColumn(db, dt.table);
|
||||
const primaryKeyColumnName = primaryKey.columnName;
|
||||
dt.data = dt.data.filter((d: any) => !(deleteDatas.findIndex((x: any) => x[primaryKeyColumnName] == d[primaryKeyColumnName]) != -1));
|
||||
};
|
||||
|
||||
const submitUpdateFields = (dt: ExecResTab) => {
|
||||
dt?.dbTableRef?.submitUpdateFields();
|
||||
};
|
||||
|
||||
const cancelUpdateFields = (dt: ExecResTab) => {
|
||||
dt?.dbTableRef?.cancelUpdateFields();
|
||||
};
|
||||
|
||||
const initMonacoEditor = () => {
|
||||
monacoEditor = monacoEditorRef.value.getEditor();
|
||||
|
||||
// 注册快捷键:ctrl + R 运行选中的sql
|
||||
monacoEditor.addAction({
|
||||
// An unique identifier of the contributed action.
|
||||
// id: 'run-sql-action' + state.ti.key,
|
||||
id: 'run-sql-action' + getKey(),
|
||||
// A label of the action that will be presented to the user.
|
||||
label: '执行SQL',
|
||||
// A precondition for this action.
|
||||
precondition: undefined,
|
||||
// A rule to evaluate on top of the precondition in order to dispatch the keybindings.
|
||||
keybindingContext: undefined,
|
||||
keybindings: [
|
||||
// chord
|
||||
monaco.KeyMod.chord(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyR, 0),
|
||||
],
|
||||
contextMenuGroupId: 'navigation',
|
||||
contextMenuOrder: 1.5,
|
||||
// Method that will be executed when the action is triggered.
|
||||
// @param editor The editor instance is passed in as a convenience
|
||||
run: async function () {
|
||||
try {
|
||||
await onRunSql();
|
||||
} catch (e: any) {
|
||||
e.message && ElMessage.error(e.message);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// 注册快捷键:ctrl + R 运行选中的sql
|
||||
monacoEditor.addAction({
|
||||
// An unique identifier of the contributed action.
|
||||
// id: 'run-sql-action' + state.ti.key,
|
||||
id: 'run-sql-action-on-newtab' + getKey(),
|
||||
// A label of the action that will be presented to the user.
|
||||
label: '新标签执行SQL',
|
||||
// A precondition for this action.
|
||||
precondition: undefined,
|
||||
// A rule to evaluate on top of the precondition in order to dispatch the keybindings.
|
||||
keybindingContext: undefined,
|
||||
keybindings: [
|
||||
// chord
|
||||
monaco.KeyMod.chord(monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyR, 0),
|
||||
],
|
||||
contextMenuGroupId: 'navigation',
|
||||
contextMenuOrder: 1.6,
|
||||
// Method that will be executed when the action is triggered.
|
||||
// @param editor The editor instance is passed in as a convenience
|
||||
run: async function () {
|
||||
try {
|
||||
await onRunSql(true);
|
||||
} catch (e: any) {
|
||||
e.message && ElMessage.error(e.message);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// 注册快捷键:ctrl + shift + f 格式化sql
|
||||
monacoEditor.addAction({
|
||||
// An unique identifier of the contributed action.
|
||||
id: 'format-sql-action' + getKey(),
|
||||
// A label of the action that will be presented to the user.
|
||||
label: '格式化SQL',
|
||||
// A precondition for this action.
|
||||
precondition: undefined,
|
||||
// A rule to evaluate on top of the precondition in order to dispatch the keybindings.
|
||||
keybindingContext: undefined,
|
||||
keybindings: [
|
||||
// chord
|
||||
monaco.KeyMod.chord(monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyF, 0),
|
||||
],
|
||||
contextMenuGroupId: 'navigation',
|
||||
contextMenuOrder: 2,
|
||||
// Method that will be executed when the action is triggered.
|
||||
// @param editor The editor instance is passed in as a convenience
|
||||
run: async function () {
|
||||
try {
|
||||
await formatSql();
|
||||
} catch (e: any) {
|
||||
e.message && ElMessage.error(e.message);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// 注册快捷键:ctrl + shift + f 格式化sql
|
||||
monacoEditor.addAction({
|
||||
// An unique identifier of the contributed action.
|
||||
id: 'save-sql-action' + getKey(),
|
||||
// A label of the action that will be presented to the user.
|
||||
label: '保存SQL',
|
||||
// A precondition for this action.
|
||||
precondition: undefined,
|
||||
// A rule to evaluate on top of the precondition in order to dispatch the keybindings.
|
||||
keybindingContext: undefined,
|
||||
keybindings: [
|
||||
// chord
|
||||
monaco.KeyMod.chord(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, 0),
|
||||
],
|
||||
contextMenuGroupId: 'navigation',
|
||||
contextMenuOrder: 3,
|
||||
// Method that will be executed when the action is triggered.
|
||||
// @param editor The editor instance is passed in as a convenience
|
||||
run: async function () {
|
||||
await saveSql();
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.sql-file-exec {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
vertical-align: middle;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.editor-move-resize {
|
||||
cursor: n-resize;
|
||||
height: 3px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sql-exec-res {
|
||||
.el-tabs__header {
|
||||
margin: 0 0 !important;
|
||||
}
|
||||
|
||||
.el-tabs__item {
|
||||
font-size: 12px;
|
||||
height: 25px;
|
||||
margin: 0px;
|
||||
padding: 0 6px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,10 +1,12 @@
|
||||
import { h, render, VNode } from 'vue';
|
||||
import SqlExecDialog from './SqlExecDialog.vue';
|
||||
import {SqlLanguage} from 'sql-formatter/lib/src/sqlFormatter'
|
||||
|
||||
export type SqlExecProps = {
|
||||
sql: string;
|
||||
dbId: number;
|
||||
db: string;
|
||||
dbType?: SqlLanguage;
|
||||
runSuccessCallback?: Function;
|
||||
cancelCallback?: Function;
|
||||
};
|
||||
@@ -2,7 +2,7 @@
|
||||
<div>
|
||||
<el-dialog :destroy-on-close="true" title="待执行SQL" v-model="dialogVisible" :show-close="false" width="600px" @close="cancel">
|
||||
<monaco-editor height="300px" class="codesql" language="sql" v-model="sqlValue" />
|
||||
<el-input ref="remarkInputRef" v-model="remark" placeholder="请输入执行备注" class="mt5" />
|
||||
<el-input @keyup.enter="runSql" ref="remarkInputRef" v-model="remark" placeholder="请输入执行备注" class="mt5" />
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, ref, nextTick, reactive } from 'vue';
|
||||
import { dbApi } from '../api';
|
||||
import { dbApi } from '@/views/ops/db/api';
|
||||
import { ElDialog, ElButton, ElInput, ElMessage, InputInstance } from 'element-plus';
|
||||
// import base style
|
||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||
@@ -113,7 +113,8 @@ const cancel = () => {
|
||||
const open = (props: SqlExecProps) => {
|
||||
runSuccessCallback = props.runSuccessCallback;
|
||||
cancelCallback = props.cancelCallback;
|
||||
state.sqlValue = sqlFormatter(props.sql);
|
||||
props.dbType = props.dbType || 'mysql';
|
||||
state.sqlValue = sqlFormatter(props.sql, { language: props.dbType });
|
||||
state.dbId = props.dbId;
|
||||
state.db = props.db;
|
||||
state.dialogVisible = true;
|
||||
@@ -1,923 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
<div class="toolbar">
|
||||
<div class="fl">
|
||||
<el-link @click="onRunSql()" :underline="false" class="ml15" icon="VideoPlay"> </el-link>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-tooltip class="box-item" effect="dark" content="format sql" placement="top">
|
||||
<el-link @click="formatSql()" type="primary" :underline="false" icon="MagicStick"> </el-link>
|
||||
</el-tooltip>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-tooltip class="box-item" effect="dark" content="commit" placement="top">
|
||||
<el-link @click="onCommit()" type="success" :underline="false" icon="CircleCheck"> </el-link>
|
||||
</el-tooltip>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-upload
|
||||
class="sql-file-exec"
|
||||
:before-upload="beforeUpload"
|
||||
:on-success="execSqlFileSuccess"
|
||||
:headers="{ Authorization: token }"
|
||||
:action="getUploadSqlFileUrl()"
|
||||
:show-file-list="false"
|
||||
name="file"
|
||||
multiple
|
||||
:limit="100"
|
||||
>
|
||||
<el-tooltip class="box-item" effect="dark" content="SQL脚本执行" placement="top">
|
||||
<el-link type="success" :underline="false" icon="Document"></el-link>
|
||||
</el-tooltip>
|
||||
</el-upload>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
<el-tooltip class="box-item" effect="dark" content="limit" placement="top">
|
||||
<el-link @click="onLimit()" type="success" :underline="false" icon="Operation"> </el-link>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<div style="float: right" class="fl">
|
||||
<el-button @click="saveSql()" type="primary" icon="document-add" plain size="small">保存SQL </el-button>
|
||||
<el-button v-if="sqlName" @click="deleteSql()" type="danger" icon="delete" plain size="small">删除SQL </el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt5 sqlEditor">
|
||||
<div :id="'MonacoTextarea-' + ti.key" :style="{ height: editorHeight }"></div>
|
||||
</div>
|
||||
|
||||
<div class="editor-move-resize" @mousedown="onDragSetHeight">
|
||||
<el-icon>
|
||||
<Minus />
|
||||
</el-icon>
|
||||
</div>
|
||||
|
||||
<div class="mt5">
|
||||
<el-row>
|
||||
<el-link v-if="table" @click="onDeleteData()" class="ml5" type="danger" icon="delete" :underline="false"></el-link>
|
||||
|
||||
<span v-if="execRes.data.length > 0">
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
<el-link type="success" :underline="false" @click="exportData"><span style="font-size: 12px">导出</span></el-link>
|
||||
</span>
|
||||
<span v-if="hasUpdatedFileds">
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
<el-link type="success" :underline="false" @click="submitUpdateFields()"><span style="font-size: 12px">提交</span></el-link>
|
||||
</span>
|
||||
<span v-if="hasUpdatedFileds">
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
<el-link type="warning" :underline="false" @click="cancelUpdateFields"><span style="font-size: 12px">取消</span></el-link>
|
||||
</span>
|
||||
</el-row>
|
||||
<db-table
|
||||
ref="dbTableRef"
|
||||
:db-id="state.ti.dbId"
|
||||
:db="state.ti.db"
|
||||
:data="execRes.data"
|
||||
:table="state.table"
|
||||
:columns="execRes.tableColumn"
|
||||
:loading="loading"
|
||||
:height="tableDataHeight"
|
||||
empty-text="tips: select *开头的单表查询或点击表名默认查询的数据,可双击数据在线修改"
|
||||
@selection-change="onDataSelectionChange"
|
||||
@change-updated-field="changeUpdatedField"
|
||||
></db-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, watch, onMounted, reactive, toRefs, ref, Ref } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
import { getSession } from '@/common/utils/storage';
|
||||
import { isTrue, notBlank } from '@/common/assert';
|
||||
import { format as sqlFormatter } from 'sql-formatter';
|
||||
import config from '@/common/config';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
|
||||
import { language as sqlLanguage } from 'monaco-editor/esm/vs/basic-languages/mysql/mysql.js';
|
||||
import { language as addSqlLanguage } from '../../lang/mysql.js';
|
||||
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker.js?worker';
|
||||
// import * as monaco from 'monaco-editor';
|
||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
import { editor, languages, Position } from 'monaco-editor';
|
||||
// 相关语言
|
||||
import 'monaco-editor/esm/vs/basic-languages/sql/sql.contribution.js';
|
||||
import 'monaco-editor/esm/vs/editor/contrib/suggest/browser/suggestController.js';
|
||||
import 'monaco-editor/esm/vs/editor/contrib/suggest/browser/suggestInlineCompletions.js';
|
||||
// 右键菜单
|
||||
import 'monaco-editor/esm/vs/editor/contrib/contextmenu/browser/contextmenu.js';
|
||||
import 'monaco-editor/esm/vs/editor/contrib/caretOperations/browser/caretOperations.js';
|
||||
import 'monaco-editor/esm/vs/editor/contrib/clipboard//browser/clipboard.js';
|
||||
import 'monaco-editor/esm/vs/editor/contrib/find/browser/findController.js';
|
||||
import 'monaco-editor/esm/vs/editor/contrib/format//browser/formatActions.js';
|
||||
|
||||
// 主题仓库 https://github.com/brijeshb42/monaco-themes
|
||||
// 主题例子 https://editor.bitwiser.in/
|
||||
import SolarizedLight from 'monaco-themes/themes/Solarized-light.json';
|
||||
import DbTable from '../DbTable.vue';
|
||||
import { DbInst, TabInfo } from '../../db';
|
||||
import { exportCsv } from '@/common/utils/export';
|
||||
import { dateStrFormat } from '@/common/utils/date';
|
||||
import { dbApi } from '../../api';
|
||||
|
||||
const sqlCompletionKeywords = [...sqlLanguage.keywords, ...addSqlLanguage.keywords];
|
||||
const sqlCompletionOperators = [...sqlLanguage.operators, ...addSqlLanguage.operators];
|
||||
const sqlCompletionBuiltinFunctions = [...sqlLanguage.builtinFunctions, ...addSqlLanguage.builtinFunctions];
|
||||
const sqlCompletionBuiltinVariables = [...sqlLanguage.builtinVariables, ...addSqlLanguage.builtinVariables];
|
||||
|
||||
const emits = defineEmits(['saveSqlSuccess', 'deleteSqlSuccess']);
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: TabInfo,
|
||||
required: true,
|
||||
},
|
||||
// sql脚本名,若有则去加载该sql内容
|
||||
sqlName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
editorHeight: {
|
||||
type: String,
|
||||
default: '600',
|
||||
},
|
||||
});
|
||||
|
||||
const { themeConfig } = storeToRefs(useThemeConfig());
|
||||
const token = getSession('token');
|
||||
let monacoEditor = {} as editor.IStandaloneCodeEditor;
|
||||
const dbTableRef = ref(null) as Ref;
|
||||
|
||||
const state = reactive({
|
||||
token,
|
||||
ti: {} as TabInfo,
|
||||
dbs: [],
|
||||
dbId: null, // 当前选中操作的数据库实例
|
||||
table: '', // 当前单表操作sql的表信息
|
||||
sqlName: '',
|
||||
sql: '', // 当前编辑器的sql内容
|
||||
loading: false, // 是否在加载数据
|
||||
execRes: {
|
||||
data: [],
|
||||
tableColumn: [],
|
||||
},
|
||||
selectionDatas: [] as any,
|
||||
editorHeight: '500',
|
||||
tableDataHeight: 250 as any,
|
||||
hasUpdatedFileds: false,
|
||||
});
|
||||
|
||||
const { tableDataHeight, editorHeight, ti, execRes, table, sqlName, loading, hasUpdatedFileds } = toRefs(state);
|
||||
|
||||
watch(
|
||||
() => props.editorHeight,
|
||||
(newValue: any) => {
|
||||
state.editorHeight = newValue;
|
||||
}
|
||||
);
|
||||
|
||||
// 监听 themeConfig editorTheme配置文件的变化
|
||||
watch(
|
||||
() => themeConfig.value.editorTheme,
|
||||
(val) => {
|
||||
console.log('monaco editor theme change: ', val);
|
||||
monaco?.editor?.setTheme(val);
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
console.log('in query mounted');
|
||||
state.ti = props.data;
|
||||
state.editorHeight = props.editorHeight;
|
||||
const params = state.ti.params;
|
||||
state.dbs = params && params.dbs;
|
||||
|
||||
if (params && params.sqlName) {
|
||||
state.sqlName = params.sqlName;
|
||||
const res = await dbApi.getSql.request({ id: state.ti.dbId, type: 1, name: state.sqlName, db: state.ti.db });
|
||||
state.sql = res.sql;
|
||||
}
|
||||
nextTick(() => {
|
||||
setTimeout(() => initMonacoEditor(), 50);
|
||||
});
|
||||
await state.ti.getNowDbInst().loadDbHints(state.ti.db);
|
||||
});
|
||||
|
||||
self.MonacoEnvironment = {
|
||||
getWorker() {
|
||||
return new EditorWorker();
|
||||
},
|
||||
};
|
||||
|
||||
const initMonacoEditor = () => {
|
||||
registerSqlCompletionItemProvider();
|
||||
|
||||
let monacoTextarea = document.getElementById('MonacoTextarea-' + state.ti.key) as HTMLElement;
|
||||
// options参数参考 https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.IStandaloneEditorConstructionOptions.html#language
|
||||
// 初始化一些主题
|
||||
monaco.editor.defineTheme('SolarizedLight', SolarizedLight);
|
||||
monacoEditor = monaco.editor.create(monacoTextarea, {
|
||||
language: 'sql',
|
||||
theme: themeConfig.value.editorTheme,
|
||||
automaticLayout: true, //自适应宽高布局
|
||||
folding: false,
|
||||
roundedSelection: false, // 禁用选择文本背景的圆角
|
||||
matchBrackets: 'near',
|
||||
linkedEditing: true,
|
||||
cursorBlinking: 'smooth', // 光标闪烁样式
|
||||
mouseWheelZoom: true, // 在按住Ctrl键的同时使用鼠标滚轮时,在编辑器中缩放字体
|
||||
overviewRulerBorder: false, // 不要滚动条的边框
|
||||
tabSize: 2, // tab 缩进长度
|
||||
// fontFamily: 'JetBrainsMono', // 字体 暂时不要设置,否则光标容易错位
|
||||
fontWeight: 'bold',
|
||||
// letterSpacing: 1, 字符间距
|
||||
// quickSuggestions:false, // 禁用代码提示
|
||||
minimap: {
|
||||
enabled: false, // 不要小地图
|
||||
},
|
||||
});
|
||||
|
||||
// 注册快捷键:ctrl + R 运行选中的sql
|
||||
monacoEditor.addAction({
|
||||
// An unique identifier of the contributed action.
|
||||
id: 'run-sql-action' + state.ti.key,
|
||||
// A label of the action that will be presented to the user.
|
||||
label: '执行SQL',
|
||||
// A precondition for this action.
|
||||
precondition: undefined,
|
||||
// A rule to evaluate on top of the precondition in order to dispatch the keybindings.
|
||||
keybindingContext: undefined,
|
||||
keybindings: [
|
||||
// chord
|
||||
monaco.KeyMod.chord(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyR, 0),
|
||||
],
|
||||
contextMenuGroupId: 'navigation',
|
||||
contextMenuOrder: 1.5,
|
||||
// Method that will be executed when the action is triggered.
|
||||
// @param editor The editor instance is passed in as a convenience
|
||||
run: async function () {
|
||||
try {
|
||||
await onRunSql();
|
||||
} catch (e: any) {
|
||||
e.message && ElMessage.error(e.message);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// 注册快捷键:ctrl + shift + f 格式化sql
|
||||
monacoEditor.addAction({
|
||||
// An unique identifier of the contributed action.
|
||||
id: 'format-sql-action' + state.ti.key,
|
||||
// A label of the action that will be presented to the user.
|
||||
label: '格式化SQL',
|
||||
// A precondition for this action.
|
||||
precondition: undefined,
|
||||
// A rule to evaluate on top of the precondition in order to dispatch the keybindings.
|
||||
keybindingContext: undefined,
|
||||
keybindings: [
|
||||
// chord
|
||||
monaco.KeyMod.chord(monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyF, 0),
|
||||
],
|
||||
contextMenuGroupId: 'navigation',
|
||||
contextMenuOrder: 2,
|
||||
// Method that will be executed when the action is triggered.
|
||||
// @param editor The editor instance is passed in as a convenience
|
||||
run: async function () {
|
||||
try {
|
||||
await formatSql();
|
||||
} catch (e: any) {
|
||||
e.message && ElMessage.error(e.message);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// 如果sql有值,则默认赋值
|
||||
if (state.sql) {
|
||||
monacoEditor.getModel()?.setValue(state.sql);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 拖拽改变sql编辑区和查询结果区高度
|
||||
*/
|
||||
const onDragSetHeight = () => {
|
||||
document.onmousemove = (e) => {
|
||||
e.preventDefault();
|
||||
//得到鼠标拖动的宽高距离:取绝对值
|
||||
state.editorHeight = `${document.getElementById('MonacoTextarea-' + state.ti.key)!.offsetHeight + e.movementY}px`;
|
||||
state.tableDataHeight -= e.movementY;
|
||||
};
|
||||
document.onmouseup = () => {
|
||||
document.onmousemove = null;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 执行sql
|
||||
*/
|
||||
const onRunSql = async () => {
|
||||
// 没有选中的文本,则为全部文本
|
||||
let sql = getSql() as string;
|
||||
notBlank(sql && sql.trim(), '请选中需要执行的sql');
|
||||
// 去除字符串前的空格、换行等
|
||||
sql = sql.replace(/(^\s*)/g, '');
|
||||
let execRemark = '';
|
||||
let canRun = true;
|
||||
if (
|
||||
sql.startsWith('update') ||
|
||||
sql.startsWith('UPDATE') ||
|
||||
sql.startsWith('INSERT') ||
|
||||
sql.startsWith('insert') ||
|
||||
sql.startsWith('DELETE') ||
|
||||
sql.startsWith('delete')
|
||||
) {
|
||||
const res: any = await ElMessageBox.prompt('请输入备注', 'Tip', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputPattern: /^[\s\S]*.*[^\s][\s\S]*$/,
|
||||
inputErrorMessage: '请输入执行该sql的备注信息',
|
||||
});
|
||||
execRemark = res.value;
|
||||
if (!execRemark) {
|
||||
canRun = false;
|
||||
}
|
||||
}
|
||||
if (!canRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
state.loading = true;
|
||||
|
||||
const colAndData: any = await state.ti.getNowDbInst().runSql(state.ti.db, sql, execRemark);
|
||||
if (!colAndData.res || colAndData.res.length === 0) {
|
||||
ElMessage.warning('未查询到结果集');
|
||||
}
|
||||
state.execRes.data = colAndData.res;
|
||||
// 兼容表格字段配置
|
||||
state.execRes.tableColumn = colAndData.colNames.map((x: any) => {
|
||||
return {
|
||||
columnName: x,
|
||||
show: true,
|
||||
};
|
||||
});
|
||||
cancelUpdateFields();
|
||||
} catch (e: any) {
|
||||
state.execRes.data = [];
|
||||
state.execRes.tableColumn = [];
|
||||
state.table = '';
|
||||
return;
|
||||
} finally {
|
||||
state.loading = false;
|
||||
}
|
||||
|
||||
// 即只有以该字符串开头的sql才可修改表数据内容
|
||||
if (sql.startsWith('SELECT *') || sql.startsWith('select *') || sql.startsWith('SELECT\n *')) {
|
||||
state.selectionDatas = [];
|
||||
const tableName = sql.split(/from/i)[1];
|
||||
if (tableName) {
|
||||
const tn = tableName.trim().split(' ')[0].split('\n')[0];
|
||||
state.table = tn;
|
||||
state.table = tn;
|
||||
} else {
|
||||
state.table = '';
|
||||
}
|
||||
} else {
|
||||
state.table = '';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取sql,如果有鼠标选中,则返回选中内容,否则返回输入框内所有内容
|
||||
*/
|
||||
const getSql = () => {
|
||||
let res = '' as string | undefined;
|
||||
// 编辑器还没初始化
|
||||
if (!monacoEditor?.getModel) {
|
||||
return res;
|
||||
}
|
||||
// 选择选中的sql
|
||||
let selection = monacoEditor.getSelection();
|
||||
if (selection) {
|
||||
res = monacoEditor.getModel()?.getValueInRange(selection);
|
||||
}
|
||||
// 整个编辑器的sql
|
||||
if (!res) {
|
||||
return monacoEditor.getModel()?.getValue();
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
const saveSql = async () => {
|
||||
const sql = monacoEditor.getModel()?.getValue();
|
||||
notBlank(sql, 'sql内容不能为空');
|
||||
|
||||
let sqlName = state.sqlName;
|
||||
const newSql = !sqlName;
|
||||
if (newSql) {
|
||||
try {
|
||||
const input = await ElMessageBox.prompt('请输入SQL脚本名', 'SQL名', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputPattern: /\w+/,
|
||||
inputErrorMessage: '请输入SQL脚本名',
|
||||
});
|
||||
sqlName = input.value;
|
||||
state.sqlName = sqlName;
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await dbApi.saveSql.request({ id: state.ti.dbId, db: state.ti.db, sql: sql, type: 1, name: sqlName });
|
||||
ElMessage.success('保存成功');
|
||||
// 保存sql脚本成功事件
|
||||
emits('saveSqlSuccess', state.ti.dbId, state.ti.db);
|
||||
};
|
||||
|
||||
const deleteSql = async () => {
|
||||
const sqlName = state.sqlName;
|
||||
notBlank(sqlName, '该sql内容未保存');
|
||||
const { dbId, db } = state.ti;
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除【${sqlName}】该SQL内容?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
});
|
||||
await dbApi.deleteDbSql.request({ id: dbId, db: db, name: sqlName });
|
||||
ElMessage.success('删除成功');
|
||||
emits('deleteSqlSuccess', dbId, db);
|
||||
} catch (err) {}
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化sql
|
||||
*/
|
||||
const formatSql = () => {
|
||||
let selection = monacoEditor.getSelection();
|
||||
if (!selection) {
|
||||
return;
|
||||
}
|
||||
let sql = monacoEditor.getModel()?.getValueInRange(selection);
|
||||
// 有选中sql则格式化并替换选中sql, 否则格式化编辑器所有内容
|
||||
if (sql) {
|
||||
replaceSelection(sqlFormatter(sql), selection);
|
||||
return;
|
||||
}
|
||||
monacoEditor.getModel()?.setValue(sqlFormatter(monacoEditor.getValue()));
|
||||
};
|
||||
|
||||
/**
|
||||
* 提交事务,用于没有开启自动提交事务
|
||||
*/
|
||||
const onCommit = () => {
|
||||
state.ti.getNowDbInst().runSql(state.ti.db, 'COMMIT;');
|
||||
ElMessage.success('COMMIT success');
|
||||
};
|
||||
|
||||
/**
|
||||
* 替换选中的内容
|
||||
*/
|
||||
const replaceSelection = (str: string, selection: any) => {
|
||||
const model = monacoEditor.getModel();
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
if (!selection) {
|
||||
model.setValue(str);
|
||||
return;
|
||||
}
|
||||
const { startLineNumber, endLineNumber, startColumn, endColumn } = selection;
|
||||
|
||||
const textBeforeSelection = model.getValueInRange({
|
||||
startLineNumber: 1,
|
||||
startColumn: 0,
|
||||
endLineNumber: startLineNumber,
|
||||
endColumn: startColumn,
|
||||
});
|
||||
|
||||
const textAfterSelection = model.getValueInRange({
|
||||
startLineNumber: endLineNumber,
|
||||
startColumn: endColumn,
|
||||
endLineNumber: model.getLineCount(),
|
||||
endColumn: model.getLineMaxColumn(model.getLineCount()),
|
||||
});
|
||||
|
||||
monacoEditor.setValue(textBeforeSelection + str + textAfterSelection);
|
||||
monacoEditor.focus();
|
||||
monacoEditor.setPosition({
|
||||
lineNumber: startLineNumber,
|
||||
column: 0,
|
||||
});
|
||||
};
|
||||
|
||||
const onLimit = () => {
|
||||
let position = monacoEditor.getPosition() as monaco.Position;
|
||||
let newText = ' limit 10';
|
||||
monacoEditor?.getModel()?.applyEdits([
|
||||
{
|
||||
range: new monaco.Range(position.lineNumber, position.column, position.lineNumber, position.column),
|
||||
text: newText,
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
/**
|
||||
* 导出当前页数据
|
||||
*/
|
||||
const exportData = () => {
|
||||
const dataList = state.execRes.data as any;
|
||||
isTrue(dataList.length > 0, '没有数据可导出');
|
||||
exportCsv(
|
||||
`数据查询导出-${dateStrFormat('yyyyMMddHHmm', new Date().toString())}`,
|
||||
state.execRes.tableColumn.map((x: any) => x.columnName),
|
||||
dataList
|
||||
);
|
||||
};
|
||||
|
||||
const beforeUpload = (file: File) => {
|
||||
ElMessage.success(`'${file.name}' 正在上传执行, 请关注结果通知`);
|
||||
};
|
||||
|
||||
// 执行sql成功
|
||||
const execSqlFileSuccess = (res: any) => {
|
||||
if (res.code !== 200) {
|
||||
ElMessage.error(res.msg);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取sql文件上传执行url
|
||||
const getUploadSqlFileUrl = () => {
|
||||
return `${config.baseApiUrl}/dbs/${state.ti.dbId}/exec-sql-file?db=${state.ti.db}`;
|
||||
};
|
||||
|
||||
const onDataSelectionChange = (datas: []) => {
|
||||
state.selectionDatas = datas;
|
||||
};
|
||||
|
||||
const changeUpdatedField = (updatedFields: []) => {
|
||||
// 如果存在要更新字段,则显示提交和取消按钮
|
||||
state.hasUpdatedFileds = updatedFields && updatedFields.length > 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* 执行删除数据事件
|
||||
*/
|
||||
const onDeleteData = async () => {
|
||||
const deleteDatas = state.selectionDatas;
|
||||
isTrue(deleteDatas && deleteDatas.length > 0, '请先选择要删除的数据');
|
||||
const { db } = state.ti;
|
||||
const dbInst = state.ti.getNowDbInst();
|
||||
const primaryKey = await dbInst.loadTableColumn(db, state.table);
|
||||
const primaryKeyColumnName = primaryKey.columnName;
|
||||
dbInst.promptExeSql(db, dbInst.genDeleteByPrimaryKeysSql(db, state.table, deleteDatas), null, () => {
|
||||
state.execRes.data = state.execRes.data.filter(
|
||||
(d: any) => !(deleteDatas.findIndex((x: any) => x[primaryKeyColumnName] == d[primaryKeyColumnName]) != -1)
|
||||
);
|
||||
state.selectionDatas = [];
|
||||
});
|
||||
};
|
||||
|
||||
const submitUpdateFields = () => {
|
||||
dbTableRef.value.submitUpdateFields();
|
||||
};
|
||||
|
||||
const cancelUpdateFields = () => {
|
||||
dbTableRef.value.cancelUpdateFields();
|
||||
};
|
||||
|
||||
const registerSqlCompletionItemProvider = () => {
|
||||
// 参考 https://microsoft.github.io/monaco-editor/playground.html#extending-language-services-completion-provider-example
|
||||
self.completionItemProvider =
|
||||
self.completionItemProvider ||
|
||||
monaco.languages.registerCompletionItemProvider('sql', {
|
||||
triggerCharacters: ['.', ' '],
|
||||
provideCompletionItems: async (model: editor.ITextModel, position: Position): Promise<languages.CompletionList | null | undefined> => {
|
||||
let word = model.getWordUntilPosition(position);
|
||||
const nowTab = props.data;
|
||||
if (!nowTab) {
|
||||
return;
|
||||
}
|
||||
const { db, dbId } = nowTab;
|
||||
const dbInst = DbInst.getInst(dbId);
|
||||
const { lineNumber, column } = position;
|
||||
const { startColumn, endColumn } = word;
|
||||
|
||||
// 当前行文本
|
||||
let lineContent = model.getLineContent(lineNumber);
|
||||
// 注释行不需要代码提示
|
||||
if (lineContent.startsWith('--')) {
|
||||
return { suggestions: [] };
|
||||
}
|
||||
|
||||
let range = {
|
||||
startLineNumber: lineNumber,
|
||||
endLineNumber: lineNumber,
|
||||
startColumn,
|
||||
endColumn,
|
||||
};
|
||||
|
||||
// 光标前文本
|
||||
const textBeforePointer = model.getValueInRange({
|
||||
startLineNumber: lineNumber,
|
||||
startColumn: 0,
|
||||
endLineNumber: lineNumber,
|
||||
endColumn: column,
|
||||
});
|
||||
const textBeforePointerMulti = model.getValueInRange({
|
||||
startLineNumber: 1,
|
||||
startColumn: 0,
|
||||
endLineNumber: lineNumber,
|
||||
endColumn: column,
|
||||
});
|
||||
// 光标后文本
|
||||
const textAfterPointerMulti = model.getValueInRange({
|
||||
startLineNumber: lineNumber,
|
||||
startColumn: column,
|
||||
endLineNumber: model.getLineCount(),
|
||||
endColumn: model.getLineMaxColumn(model.getLineCount()),
|
||||
});
|
||||
// // const nextTokens = textAfterPointer.trim().split(/\s+/)
|
||||
// // const nextToken = nextTokens[0].toLowerCase()
|
||||
const tokens = textBeforePointer.trim().split(/\s+/);
|
||||
let lastToken = tokens[tokens.length - 1].toLowerCase();
|
||||
const secondToken = (tokens.length > 2 && tokens[tokens.length - 2].toLowerCase()) || '';
|
||||
|
||||
const dbs = (nowTab.params && nowTab.params.dbs && nowTab.params.dbs) || [];
|
||||
// console.log("光标前文本:=>" + textBeforePointerMulti)
|
||||
// console.log("最后输入的:=>" + lastToken)
|
||||
|
||||
let suggestions: languages.CompletionItem[] = [];
|
||||
const tables = await dbInst.loadTables(db);
|
||||
|
||||
async function hintTableColumns(tableName: any, db: any) {
|
||||
let dbHits = await dbInst.loadDbHints(db);
|
||||
let columns = dbHits[tableName];
|
||||
let suggestions: languages.CompletionItem[] = [];
|
||||
columns?.forEach((a: string, index: any) => {
|
||||
// 字段数据格式 字段名 字段注释, 如: create_time [datetime][创建时间]
|
||||
const nameAndComment = a.split(' ');
|
||||
const fieldName = nameAndComment[0];
|
||||
suggestions.push({
|
||||
label: {
|
||||
label: a,
|
||||
description: 'column',
|
||||
},
|
||||
kind: monaco.languages.CompletionItemKind.Property,
|
||||
detail: '', // 不显示detail, 否则选中时备注等会被遮挡
|
||||
insertText: fieldName, // create_time
|
||||
range,
|
||||
sortText: 100 + index + '', // 使用表字段声明顺序排序,排序需为字符串类型
|
||||
});
|
||||
});
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
if (lastToken.indexOf('.') > -1 || secondToken.indexOf('.') > -1) {
|
||||
// 如果是.触发代码提示,则进行【 库.表名联想 】 或 【 表别名.表字段联想 】
|
||||
let str = lastToken.substring(0, lastToken.lastIndexOf('.'));
|
||||
if (lastToken.trim().startsWith('.')) {
|
||||
str = secondToken;
|
||||
}
|
||||
// 如果字符串粘连起了如:'a.creator,a.',需要重新取出别名
|
||||
let aliasArr = lastToken.split(',');
|
||||
if (aliasArr.length > 1) {
|
||||
lastToken = aliasArr[aliasArr.length - 1];
|
||||
str = lastToken.substring(0, lastToken.lastIndexOf('.'));
|
||||
if (lastToken.trim().startsWith('.')) {
|
||||
str = secondToken;
|
||||
}
|
||||
}
|
||||
// 库.表名联想
|
||||
if (dbs && dbs.filter((a: any) => a === str)?.length > 0) {
|
||||
let tables = await dbInst.loadTables(str);
|
||||
let suggestions: languages.CompletionItem[] = [];
|
||||
for (let item of tables) {
|
||||
const { tableName, tableComment } = item;
|
||||
suggestions.push({
|
||||
label: {
|
||||
label: tableName + (tableComment ? ' - ' + tableComment : ''),
|
||||
description: 'table',
|
||||
},
|
||||
kind: monaco.languages.CompletionItemKind.File,
|
||||
insertText: tableName,
|
||||
range,
|
||||
});
|
||||
}
|
||||
return { suggestions };
|
||||
}
|
||||
|
||||
let sql = textBeforePointerMulti.split(';')[textBeforePointerMulti.split(';').length - 1] + textAfterPointerMulti.split(';')[0];
|
||||
// 表别名.表字段联想
|
||||
let tableInfo = getTableByAlias(sql, db, str);
|
||||
if (tableInfo.tableName) {
|
||||
let tableName = tableInfo.tableName;
|
||||
let db = tableInfo.dbName;
|
||||
// 取出表名并提示
|
||||
let suggestions = await hintTableColumns(tableName, db);
|
||||
if (suggestions.length > 0) {
|
||||
return { suggestions };
|
||||
}
|
||||
}
|
||||
return { suggestions: [] };
|
||||
} else {
|
||||
// 如果sql里含有表名,则提示表字段
|
||||
let mat = textBeforePointerMulti.match(/[from|update]\n*\s+\n*(\w+)\n*\s+\n*/i);
|
||||
if (mat && mat.length > 1) {
|
||||
let tableName = mat[1];
|
||||
// 取出表名并提示
|
||||
let addSuggestions = await hintTableColumns(tableName, db);
|
||||
if (addSuggestions.length > 0) {
|
||||
suggestions = suggestions.concat(addSuggestions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 表名联想
|
||||
tables.forEach((tableMeta: any) => {
|
||||
const { tableName, tableComment } = tableMeta;
|
||||
suggestions.push({
|
||||
label: {
|
||||
label: tableName + ' - ' + tableComment,
|
||||
description: 'table',
|
||||
},
|
||||
kind: monaco.languages.CompletionItemKind.File,
|
||||
detail: tableComment,
|
||||
insertText: tableName + ' ',
|
||||
range,
|
||||
});
|
||||
});
|
||||
|
||||
// mysql关键字
|
||||
sqlCompletionKeywords.forEach((item: any) => {
|
||||
suggestions.push({
|
||||
label: {
|
||||
label: item,
|
||||
description: 'keyword',
|
||||
},
|
||||
kind: monaco.languages.CompletionItemKind.Keyword,
|
||||
insertText: item,
|
||||
range,
|
||||
});
|
||||
});
|
||||
|
||||
// 操作符
|
||||
sqlCompletionOperators.forEach((item: any) => {
|
||||
suggestions.push({
|
||||
label: {
|
||||
label: item,
|
||||
description: 'opt',
|
||||
},
|
||||
kind: monaco.languages.CompletionItemKind.Operator,
|
||||
insertText: item,
|
||||
range,
|
||||
});
|
||||
});
|
||||
|
||||
let replacedFunctions = [] as string[];
|
||||
|
||||
// 添加的函数
|
||||
addSqlLanguage.replaceFunctions.forEach((item: any) => {
|
||||
replacedFunctions.push(item.label);
|
||||
suggestions.push({
|
||||
label: {
|
||||
label: item.label,
|
||||
description: item.description,
|
||||
},
|
||||
kind: monaco.languages.CompletionItemKind.Function,
|
||||
insertText: item.insertText,
|
||||
range,
|
||||
});
|
||||
});
|
||||
|
||||
// 内置函数
|
||||
sqlCompletionBuiltinFunctions.forEach((item: any) => {
|
||||
replacedFunctions.indexOf(item) < 0 &&
|
||||
suggestions.push({
|
||||
label: {
|
||||
label: item,
|
||||
description: 'func',
|
||||
},
|
||||
kind: monaco.languages.CompletionItemKind.Function,
|
||||
insertText: item,
|
||||
range,
|
||||
});
|
||||
});
|
||||
// 内置变量
|
||||
sqlCompletionBuiltinVariables.forEach((item: string) => {
|
||||
suggestions.push({
|
||||
label: {
|
||||
label: item,
|
||||
description: 'var',
|
||||
},
|
||||
kind: monaco.languages.CompletionItemKind.Variable,
|
||||
insertText: item,
|
||||
range,
|
||||
});
|
||||
});
|
||||
|
||||
// 库名提示
|
||||
if (dbs && dbs.length > 0) {
|
||||
dbs.forEach((a: any) => {
|
||||
suggestions.push({
|
||||
label: {
|
||||
label: a,
|
||||
description: 'schema',
|
||||
},
|
||||
kind: monaco.languages.CompletionItemKind.Folder,
|
||||
insertText: a,
|
||||
range,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 默认提示
|
||||
return {
|
||||
suggestions: suggestions,
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据别名获取sql里的表名
|
||||
* @param sql sql
|
||||
* @param db 默认数据库
|
||||
* @param alias 别名
|
||||
*/
|
||||
const getTableByAlias = (sql: string, db: string, alias: string): { dbName: string; tableName: string } => {
|
||||
// 表别名:表名
|
||||
let result = {};
|
||||
let defName = '';
|
||||
let defResult = {};
|
||||
// 正则匹配取出表名和表别名
|
||||
// 测试sql
|
||||
/*
|
||||
|
||||
`select * from database.Outvisit l
|
||||
left join patient p on l.patid=p.patientid
|
||||
join patstatic c on l.patid=c.patid inner join patphone ph on l.patid=ph.patid
|
||||
where l.name='kevin' and exsits(select 1 from pharmacywestpas pw where p.outvisitid=l.outvisitid)
|
||||
unit all
|
||||
select * from invisit v where`.match(/(join|from)\s+(\w*-?\w*\.?\w+)\s*(as)?\s*(\w*)/gi)
|
||||
*/
|
||||
let match = sql.match(/(join|from)\n*\s+\n*(\w*-?\w*\.?\w+)\s*(as)?\s*(\w*)\n*/gi);
|
||||
if (match && match.length > 0) {
|
||||
match.forEach((a) => {
|
||||
// 去掉前缀,取出
|
||||
let t = a
|
||||
.substring(5, a.length)
|
||||
.replaceAll(/\s+/g, ' ')
|
||||
.replaceAll(/\s+as\s+/gi, ' ')
|
||||
.replaceAll(/\r\n/g, ' ')
|
||||
.trim()
|
||||
.split(/\s+/);
|
||||
let withDb = t[0].split('.');
|
||||
// 表名是 db名.表名
|
||||
let tName = withDb.length > 1 ? withDb[1] : withDb[0];
|
||||
let dbName = withDb.length > 1 ? withDb[0] : db || '';
|
||||
if (t.length == 2) {
|
||||
// 表别名:表名
|
||||
result[t[1]] = { tableName: tName, dbName };
|
||||
} else {
|
||||
// 只有表名无别名 取第一个无别名的表为默认表
|
||||
!defName && (defResult = { tableName: tName, dbName: db });
|
||||
}
|
||||
});
|
||||
}
|
||||
return result[alias] || defResult;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.sql-file-exec {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
vertical-align: middle;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.sqlEditor {
|
||||
font-size: 8pt;
|
||||
font-weight: 600;
|
||||
border: 1px solid var(--el-border-color-light, #ebeef5);
|
||||
}
|
||||
|
||||
.update_field_active {
|
||||
background-color: var(--el-color-success);
|
||||
}
|
||||
|
||||
.editor-move-resize {
|
||||
cursor: n-resize;
|
||||
height: 3px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
750
mayfly_go_web/src/views/ops/db/component/table/DbTableData.vue
Normal file
@@ -0,0 +1,750 @@
|
||||
<template>
|
||||
<div class="db-table-data mt5" :style="{ height: `${tableHeight}px` }">
|
||||
<el-auto-resizer>
|
||||
<template #default="{ height, width }">
|
||||
<el-table-v2
|
||||
ref="tableRef"
|
||||
:header-height="30"
|
||||
:row-height="30"
|
||||
:row-class="rowClass"
|
||||
:columns="state.columns"
|
||||
:data="datas"
|
||||
:width="width"
|
||||
:height="height"
|
||||
fixed
|
||||
class="table"
|
||||
:row-event-handlers="rowEventHandlers"
|
||||
>
|
||||
<template #header="{ columns }">
|
||||
<div v-for="(column, i) in columns" :key="i">
|
||||
<div
|
||||
:style="{
|
||||
width: `${column.width}px`,
|
||||
height: '100%',
|
||||
lineHeight: '30px',
|
||||
textAlign: 'center',
|
||||
borderRight: 'var(--el-table-border)',
|
||||
}"
|
||||
>
|
||||
<!-- 行号列表头 -->
|
||||
<div v-if="column.key == rowNoColumn.key || !showColumnTip">
|
||||
<el-text tag="b"> {{ column.title }} </el-text>
|
||||
</div>
|
||||
|
||||
<div v-else @contextmenu="headerContextmenuClick($event, column)">
|
||||
<div v-if="showColumnTip" @mouseover="column.showSetting = true" @mouseleave="column.showSetting = false">
|
||||
<el-tooltip :show-after="500" raw-content placement="top">
|
||||
<template #content> {{ getColumnTip(column) }} </template>
|
||||
<el-text tag="b" style="cursor: pointer"> {{ column.title }} </el-text>
|
||||
</el-tooltip>
|
||||
|
||||
<span>
|
||||
<SvgIcon
|
||||
color="var(--el-color-primary)"
|
||||
v-if="column.title == nowSortColumn?.columnName"
|
||||
:name="nowSortColumn?.order == 'asc' ? 'top' : 'bottom'"
|
||||
></SvgIcon>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<el-text tag="b" style="cursor: pointer"> {{ column.title }} </el-text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #cell="{ rowData, column, rowIndex, columnIndex }">
|
||||
<div @contextmenu="dataContextmenuClick($event, rowIndex, column, rowData)" class="table-data-cell">
|
||||
<!-- 行号列 -->
|
||||
<div v-if="column.key == 'tableDataRowNo'">
|
||||
<el-text tag="b" size="small">
|
||||
{{ rowIndex + 1 }}
|
||||
</el-text>
|
||||
</div>
|
||||
|
||||
<!-- 数据列 -->
|
||||
<div v-else @dblclick="onEnterEditMode(rowData, column, rowIndex, columnIndex)">
|
||||
<div v-if="canEdit(rowIndex, columnIndex)">
|
||||
<el-input
|
||||
:ref="(el: any) => el?.focus()"
|
||||
@blur="onExitEditMode(rowData, column, rowIndex)"
|
||||
class="w100"
|
||||
input-style="text-align: center; height: 26px;"
|
||||
size="small"
|
||||
v-model="rowData[column.dataKey!]"
|
||||
></el-input>
|
||||
</div>
|
||||
|
||||
<div v-else :class="isUpdated(rowIndex, column.dataKey) ? 'update_field_active' : ''">
|
||||
<el-text style="color: var(--el-color-info-light-5)" v-if="rowData[column.dataKey!] === null" size="small" truncated>
|
||||
NULL
|
||||
</el-text>
|
||||
|
||||
<el-text v-else :title="rowData[column.dataKey!]" size="small" truncated>
|
||||
{{ rowData[column.dataKey!] }}
|
||||
</el-text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="loading" #overlay>
|
||||
<div class="el-loading-mask" style="display: flex; align-items: center; justify-content: center">
|
||||
<SvgIcon class="is-loading" name="loading" color="var(--el-color-primary)" :size="42" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #empty>
|
||||
<div style="text-align: center">
|
||||
<el-empty :style="{ height: `${tableHeight}px` }" :description="state.emptyText" :image-size="100" />
|
||||
</div>
|
||||
</template>
|
||||
</el-table-v2>
|
||||
</template>
|
||||
</el-auto-resizer>
|
||||
|
||||
<el-dialog @close="state.genTxtDialog.visible = false" v-model="state.genTxtDialog.visible" :title="state.genTxtDialog.title" width="1000px">
|
||||
<template #header>
|
||||
<div class="mr15" style="display: flex; justify-content: flex-end">
|
||||
<el-button id="copyValue" @click="copyGenTxt(state.genTxtDialog.txt)" icon="CopyDocument" type="success" size="small">一键复制</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-input v-model="state.genTxtDialog.txt" type="textarea" rows="20" />
|
||||
</el-dialog>
|
||||
|
||||
<contextmenu :dropdown="state.contextmenu.dropdown" :items="state.contextmenu.items" ref="contextmenuRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, watch, reactive, toRefs } from 'vue';
|
||||
import { ElInput } from 'element-plus';
|
||||
import { copyToClipboard } from '@/common/utils/string';
|
||||
import { DbInst } from '@/views/ops/db/db';
|
||||
import { ContextmenuItem, Contextmenu } from '@/components/contextmenu';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
import { exportCsv, exportFile } from '@/common/utils/export';
|
||||
import { dateStrFormat } from '@/common/utils/date';
|
||||
|
||||
const emits = defineEmits(['dataDelete', 'sortChange', 'deleteData', 'selectionChange', 'changeUpdatedField']);
|
||||
|
||||
const props = defineProps({
|
||||
dbId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
dbType: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
db: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
table: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
data: {
|
||||
type: Array,
|
||||
},
|
||||
columns: {
|
||||
type: Array<any>,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
emptyText: {
|
||||
type: String,
|
||||
default: '暂无数据',
|
||||
},
|
||||
showColumnTip: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 600,
|
||||
},
|
||||
});
|
||||
|
||||
const contextmenuRef = ref();
|
||||
const tableRef = ref();
|
||||
|
||||
/** 表头 contextmenu items **/
|
||||
|
||||
const cmHeaderAsc = new ContextmenuItem('asc', '升序').withIcon('top').withOnClick((data: any) => {
|
||||
onTableSortChange({ columnName: data.dataKey, order: 'asc' });
|
||||
});
|
||||
|
||||
const cmHeaderDesc = new ContextmenuItem('desc', '降序').withIcon('bottom').withOnClick((data: any) => {
|
||||
onTableSortChange({ columnName: data.dataKey, order: 'desc' });
|
||||
});
|
||||
|
||||
const cmHeaderFixed = new ContextmenuItem('fixed', '固定')
|
||||
.withIcon('Paperclip')
|
||||
.withOnClick((data: any) => {
|
||||
data.fixed = true;
|
||||
})
|
||||
.withHideFunc((data: any) => data.fixed);
|
||||
|
||||
const cmHeaderCancenFixed = new ContextmenuItem('cancelFixed', '取消固定')
|
||||
.withIcon('Minus')
|
||||
.withOnClick((data: any) => (data.fixed = false))
|
||||
.withHideFunc((data: any) => !data.fixed);
|
||||
|
||||
/** 表数据 contextmenu items **/
|
||||
|
||||
const cmDataCopyCell = new ContextmenuItem('copyValue', '复制')
|
||||
.withIcon('CopyDocument')
|
||||
.withOnClick(async (data: any) => {
|
||||
await copyToClipboard(data.rowData[data.column.dataKey]);
|
||||
})
|
||||
.withHideFunc(() => {
|
||||
// 选中多条则隐藏该复制按钮
|
||||
return selectionRowsMap.size > 1;
|
||||
});
|
||||
|
||||
const cmDataDel = new ContextmenuItem('deleteData', '删除')
|
||||
.withIcon('delete')
|
||||
.withOnClick(() => onDeleteData())
|
||||
.withHideFunc(() => {
|
||||
return state.table == '';
|
||||
});
|
||||
|
||||
const cmDataGenInsertSql = new ContextmenuItem('genInsertSql', 'Insert SQL')
|
||||
.withIcon('tickets')
|
||||
.withOnClick(() => onGenerateInsertSql())
|
||||
.withHideFunc(() => {
|
||||
return state.table == '';
|
||||
});
|
||||
|
||||
const cmDataGenJson = new ContextmenuItem('genJson', '生成JSON').withIcon('tickets').withOnClick(() => onGenerateJson());
|
||||
|
||||
const cmDataExportCsv = new ContextmenuItem('exportCsv', '导出CSV').withIcon('document').withOnClick(() => onExportCsv());
|
||||
|
||||
const cmDataExportSql = new ContextmenuItem('exportSql', '导出SQL')
|
||||
.withIcon('document')
|
||||
.withOnClick(() => onExportSql())
|
||||
.withHideFunc(() => {
|
||||
return state.table == '';
|
||||
});
|
||||
|
||||
class NowUpdateCell {
|
||||
rowIndex: number;
|
||||
colIndex: number;
|
||||
oldValue: any;
|
||||
}
|
||||
|
||||
class UpdatedRow {
|
||||
/**
|
||||
* 主键值
|
||||
*/
|
||||
primaryValue: any;
|
||||
|
||||
/**
|
||||
* 行数据
|
||||
*/
|
||||
rowData: any;
|
||||
|
||||
/**
|
||||
* 修改到的列信息, columnName -> tablecelldata
|
||||
*/
|
||||
columnsMap: Map<string, TableCellData> = new Map();
|
||||
}
|
||||
|
||||
class TableCellData {
|
||||
/**
|
||||
* 旧值
|
||||
*/
|
||||
oldValue: any;
|
||||
}
|
||||
|
||||
let nowSortColumn = null as any;
|
||||
|
||||
// 当前正在更新的单元格
|
||||
let nowUpdateCell: NowUpdateCell = null as any;
|
||||
|
||||
// 选中的数据, key->rowIndex value->primaryKeyValue
|
||||
const selectionRowsMap: Map<number, any> = new Map();
|
||||
|
||||
// 更新单元格 key-> rowIndex value -> 更新行
|
||||
const cellUpdateMap: Map<number, UpdatedRow> = new Map();
|
||||
|
||||
const state = reactive({
|
||||
dbId: 0, // 当前选中操作的数据库实例
|
||||
dbType: '',
|
||||
db: '', // 数据库名
|
||||
table: '', // 当前的表名
|
||||
datas: [],
|
||||
columns: [] as any,
|
||||
loading: false,
|
||||
tableHeight: 600,
|
||||
emptyText: '',
|
||||
|
||||
contextmenu: {
|
||||
dropdown: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
items: [] as ContextmenuItem[],
|
||||
},
|
||||
|
||||
genTxtDialog: {
|
||||
title: 'SQL',
|
||||
visible: false,
|
||||
txt: '',
|
||||
},
|
||||
});
|
||||
|
||||
const { tableHeight, datas } = toRefs(state);
|
||||
|
||||
/**
|
||||
* 行号字段列
|
||||
*/
|
||||
const rowNoColumn = {
|
||||
title: 'No.',
|
||||
key: 'tableDataRowNo',
|
||||
dataKey: 'tableDataRowNo',
|
||||
width: 45,
|
||||
fixed: true,
|
||||
align: 'center',
|
||||
headerClass: 'table-column',
|
||||
class: 'table-column',
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
(newValue: any) => {
|
||||
setTableData(newValue);
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.columns,
|
||||
(newValue: any) => {
|
||||
// 赋值列字段值是否隐藏,state.columns多了一列索引列
|
||||
if (newValue.length + 1 == state.columns.length) {
|
||||
for (let i = 0; i < newValue.length; i++) {
|
||||
state.columns[i + 1].hidden = !newValue[i].show;
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.table,
|
||||
(newValue: any) => {
|
||||
state.table = newValue;
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.height,
|
||||
(newValue: any) => {
|
||||
state.tableHeight = newValue;
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.loading,
|
||||
(newValue: any) => {
|
||||
state.loading = newValue;
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
console.log('in DbTable mounted');
|
||||
state.tableHeight = props.height;
|
||||
state.loading = props.loading;
|
||||
state.emptyText = props.emptyText;
|
||||
|
||||
state.dbId = props.dbId;
|
||||
state.dbType = props.dbType;
|
||||
state.db = props.db;
|
||||
state.table = props.table;
|
||||
setTableData(props.data);
|
||||
});
|
||||
|
||||
const setTableData = (datas: any) => {
|
||||
tableRef.value.scrollTo({ scrollLeft: 0, scrollTop: 0 });
|
||||
selectionRowsMap.clear();
|
||||
cellUpdateMap.clear();
|
||||
state.datas = datas;
|
||||
setTableColumns(props.columns);
|
||||
};
|
||||
|
||||
const setTableColumns = (columns: any) => {
|
||||
state.columns = columns.map((x: any) => {
|
||||
const columnName = x.columnName;
|
||||
return {
|
||||
...x,
|
||||
key: columnName,
|
||||
dataKey: columnName,
|
||||
width: DbInst.flexColumnWidth(columnName, state.datas),
|
||||
title: columnName,
|
||||
align: 'center',
|
||||
headerClass: 'table-column',
|
||||
class: 'table-column',
|
||||
sortable: true,
|
||||
hidden: !x.show,
|
||||
};
|
||||
});
|
||||
if (state.columns.length > 0) {
|
||||
state.columns.unshift(rowNoColumn);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 当前单元格是否允许编辑
|
||||
* @param rowIndex ri
|
||||
* @param colIndex ci
|
||||
*/
|
||||
const canEdit = (rowIndex: number, colIndex: number) => {
|
||||
return state.table && nowUpdateCell && nowUpdateCell.rowIndex == rowIndex && nowUpdateCell.colIndex == colIndex;
|
||||
};
|
||||
|
||||
/**
|
||||
* 判断当前单元格是否被更新了
|
||||
* @param rowIndex ri
|
||||
* @param columnName cn
|
||||
*/
|
||||
const isUpdated = (rowIndex: number, columnName: string) => {
|
||||
return cellUpdateMap.get(rowIndex)?.columnsMap.get(columnName);
|
||||
};
|
||||
|
||||
/**
|
||||
* 判断当前行是否被选中
|
||||
* @param rowIndex
|
||||
*/
|
||||
const isSelection = (rowIndex: number): boolean => {
|
||||
return selectionRowsMap.get(rowIndex);
|
||||
};
|
||||
|
||||
/**
|
||||
* 选中指定行
|
||||
* @param rowIndex
|
||||
* @param rowData
|
||||
* @param isMultiple 是否允许多选
|
||||
*/
|
||||
const selectionRow = (rowIndex: number, rowData: any, isMultiple = false) => {
|
||||
if (isMultiple) {
|
||||
// 如果重复点击,则取消改选中数据
|
||||
if (selectionRowsMap.get(rowIndex)) {
|
||||
selectionRowsMap.delete(rowIndex);
|
||||
triggerRefresh();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
selectionRowsMap.clear();
|
||||
}
|
||||
selectionRowsMap.set(rowIndex, rowData);
|
||||
triggerRefresh();
|
||||
};
|
||||
|
||||
/**
|
||||
* 行事件处理
|
||||
*/
|
||||
const rowEventHandlers = {
|
||||
onClick: (e: any) => {
|
||||
const event = e.event;
|
||||
const rowIndex = e.rowIndex;
|
||||
const rowData = e.rowData;
|
||||
// 按住ctrl点击,则新建标签页打开, metaKey对应mac command键
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
selectionRow(rowIndex, rowData, true);
|
||||
return;
|
||||
}
|
||||
selectionRow(rowIndex, rowData);
|
||||
},
|
||||
};
|
||||
|
||||
const headerContextmenuClick = (event: any, data: any) => {
|
||||
event.preventDefault(); // 阻止默认的右击菜单行为
|
||||
|
||||
const { clientX, clientY } = event;
|
||||
state.contextmenu.dropdown.x = clientX;
|
||||
state.contextmenu.dropdown.y = clientY;
|
||||
state.contextmenu.items = [cmHeaderAsc, cmHeaderDesc, cmHeaderFixed, cmHeaderCancenFixed];
|
||||
contextmenuRef.value.openContextmenu(data);
|
||||
};
|
||||
|
||||
const dataContextmenuClick = (event: any, rowIndex: number, column: any, data: any) => {
|
||||
event.preventDefault(); // 阻止默认的右击菜单行为
|
||||
|
||||
// 当前行未选中,则单行选中该行
|
||||
if (!isSelection(rowIndex)) {
|
||||
selectionRow(rowIndex, data);
|
||||
}
|
||||
const { clientX, clientY } = event;
|
||||
state.contextmenu.dropdown.x = clientX;
|
||||
state.contextmenu.dropdown.y = clientY;
|
||||
state.contextmenu.items = [cmDataCopyCell, cmDataDel, cmDataGenInsertSql, cmDataGenJson, cmDataExportCsv, cmDataExportSql];
|
||||
contextmenuRef.value.openContextmenu({ column, rowData: data });
|
||||
};
|
||||
|
||||
/**
|
||||
* 表排序字段变更
|
||||
*/
|
||||
const onTableSortChange = async (sort: any) => {
|
||||
nowSortColumn = sort;
|
||||
cancelUpdateFields();
|
||||
emits('sortChange', sort);
|
||||
};
|
||||
|
||||
/**
|
||||
* 执行删除数据事件
|
||||
*/
|
||||
const onDeleteData = async () => {
|
||||
const deleteDatas = Array.from(selectionRowsMap.values());
|
||||
const db = state.db;
|
||||
const dbInst = getNowDbInst();
|
||||
dbInst.promptExeSql(db, await dbInst.genDeleteByPrimaryKeysSql(db, state.table, deleteDatas as any), null, () => {
|
||||
emits('dataDelete', deleteDatas);
|
||||
});
|
||||
};
|
||||
|
||||
const onGenerateInsertSql = async () => {
|
||||
const selectionDatas = Array.from(selectionRowsMap.values());
|
||||
state.genTxtDialog.txt = await getNowDbInst().genInsertSql(state.db, state.table, selectionDatas);
|
||||
state.genTxtDialog.title = 'SQL';
|
||||
state.genTxtDialog.visible = true;
|
||||
};
|
||||
|
||||
const onGenerateJson = async () => {
|
||||
const selectionDatas = Array.from(selectionRowsMap.values());
|
||||
// 按列字段重新排序对象key
|
||||
const jsonObj = [];
|
||||
for (let selectionData of selectionDatas) {
|
||||
let obj = {};
|
||||
for (let column of state.columns) {
|
||||
if (column.show) {
|
||||
obj[column.title] = selectionData[column.dataKey];
|
||||
}
|
||||
}
|
||||
jsonObj.push(obj);
|
||||
}
|
||||
state.genTxtDialog.txt = JSON.stringify(jsonObj, null, 4);
|
||||
state.genTxtDialog.title = 'JSON';
|
||||
state.genTxtDialog.visible = true;
|
||||
};
|
||||
|
||||
const copyGenTxt = async (txt: string) => {
|
||||
await copyToClipboard(txt);
|
||||
state.genTxtDialog.visible = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* 导出当前页数据
|
||||
*/
|
||||
const onExportCsv = () => {
|
||||
const dataList = state.datas as any;
|
||||
let columnNames = [];
|
||||
for (let column of state.columns) {
|
||||
if (column.show) {
|
||||
columnNames.push(column.columnName);
|
||||
}
|
||||
}
|
||||
exportCsv(`数据导出-${state.table}-${dateStrFormat('yyyyMMddHHmm', new Date().toString())}`, columnNames, dataList);
|
||||
};
|
||||
|
||||
const onExportSql = async () => {
|
||||
const selectionDatas = state.datas;
|
||||
exportFile(
|
||||
`数据导出-${state.table}-${dateStrFormat('yyyyMMddHHmm', new Date().toString())}.sql`,
|
||||
await getNowDbInst().genInsertSql(state.db, state.table, selectionDatas)
|
||||
);
|
||||
};
|
||||
|
||||
const onEnterEditMode = (rowData: any, column: any, rowIndex = 0, columnIndex = 0) => {
|
||||
if (!state.table) {
|
||||
return;
|
||||
}
|
||||
|
||||
triggerRefresh();
|
||||
nowUpdateCell = {
|
||||
rowIndex: rowIndex,
|
||||
colIndex: columnIndex,
|
||||
oldValue: rowData[column.dataKey],
|
||||
};
|
||||
};
|
||||
|
||||
const onExitEditMode = (rowData: any, column: any, rowIndex = 0) => {
|
||||
const oldValue = nowUpdateCell.oldValue;
|
||||
const newValue = rowData[column.dataKey];
|
||||
|
||||
// 未改变单元格值
|
||||
if (oldValue == newValue) {
|
||||
nowUpdateCell = null as any;
|
||||
triggerRefresh();
|
||||
return;
|
||||
}
|
||||
|
||||
let updatedRow = cellUpdateMap.get(rowIndex);
|
||||
if (!updatedRow) {
|
||||
updatedRow = new UpdatedRow();
|
||||
updatedRow.rowData = rowData;
|
||||
cellUpdateMap.set(rowIndex, updatedRow);
|
||||
}
|
||||
|
||||
const columnName = column.dataKey;
|
||||
let cellData = updatedRow.columnsMap.get(columnName);
|
||||
if (cellData) {
|
||||
// 多次修改情况,可能又修改回原值,则移除该修改单元格
|
||||
if (cellData.oldValue == newValue) {
|
||||
cellUpdateMap.delete(rowIndex);
|
||||
}
|
||||
} else {
|
||||
cellData = new TableCellData();
|
||||
cellData.oldValue = oldValue;
|
||||
updatedRow.columnsMap.set(columnName, cellData);
|
||||
}
|
||||
|
||||
nowUpdateCell = null as any;
|
||||
triggerRefresh();
|
||||
changeUpdatedField();
|
||||
};
|
||||
|
||||
const submitUpdateFields = async () => {
|
||||
const dbInst = getNowDbInst();
|
||||
if (cellUpdateMap.size == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const db = state.db;
|
||||
let res = '';
|
||||
|
||||
for (let updateRow of cellUpdateMap.values()) {
|
||||
let sql = `UPDATE ${dbInst.wrapName(state.table)} SET `;
|
||||
const rowData = updateRow.rowData;
|
||||
// 主键列信息
|
||||
const primaryKey = await dbInst.loadTableColumn(db, state.table);
|
||||
let primaryKeyType = primaryKey.columnType;
|
||||
let primaryKeyName = primaryKey.columnName;
|
||||
let primaryKeyValue = rowData[primaryKeyName];
|
||||
|
||||
for (let k of updateRow.columnsMap.keys()) {
|
||||
const v = updateRow.columnsMap.get(k);
|
||||
if (!v) {
|
||||
continue;
|
||||
}
|
||||
// 更新字段列信息
|
||||
const updateColumn = await dbInst.loadTableColumn(db, state.table, k);
|
||||
sql += ` ${dbInst.wrapName(k)} = ${DbInst.wrapColumnValue(updateColumn.columnType, rowData[k])},`;
|
||||
|
||||
// 如果修改的字段是主键
|
||||
if (k === primaryKeyName) {
|
||||
primaryKeyValue = v.oldValue;
|
||||
}
|
||||
}
|
||||
|
||||
sql = sql.substring(0, sql.length - 1);
|
||||
sql += ` WHERE ${dbInst.wrapName(primaryKeyName)} = ${DbInst.wrapColumnValue(primaryKeyType, primaryKeyValue)} ;`;
|
||||
res += sql;
|
||||
}
|
||||
|
||||
dbInst.promptExeSql(
|
||||
db,
|
||||
res,
|
||||
() => {},
|
||||
() => {
|
||||
triggerRefresh();
|
||||
cellUpdateMap.clear();
|
||||
changeUpdatedField();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const cancelUpdateFields = () => {
|
||||
const updateRows = cellUpdateMap.values();
|
||||
// 恢复原值
|
||||
for (let updateRow of updateRows) {
|
||||
const rowData = updateRow.rowData;
|
||||
updateRow.columnsMap.forEach((v: TableCellData, k: string) => {
|
||||
rowData[k] = v.oldValue;
|
||||
});
|
||||
}
|
||||
cellUpdateMap.clear();
|
||||
changeUpdatedField();
|
||||
};
|
||||
|
||||
const changeUpdatedField = () => {
|
||||
emits('changeUpdatedField', cellUpdateMap);
|
||||
};
|
||||
|
||||
const rowClass = (row: any) => {
|
||||
if (isSelection(row.rowIndex)) {
|
||||
return 'data-selection';
|
||||
}
|
||||
if (row.rowIndex % 2 != 0) {
|
||||
return 'data-spacing';
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const getColumnTip = (column: any) => {
|
||||
const comment = column.columnComment;
|
||||
return `${column.columnType} ${comment ? ' | ' + comment : ''}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 触发响应式实时刷新,否则需要滑动或移动才能使样式实时生效
|
||||
*/
|
||||
const triggerRefresh = () => {
|
||||
// 改变columns等属性值,才能触发slot中的if条件等, 暂不知为啥
|
||||
if (state.columns[0].opTimes) {
|
||||
state.columns[0].opTimes = state.columns[0].opTimes + 1;
|
||||
} else {
|
||||
state.columns[0].opTimes = 1;
|
||||
}
|
||||
};
|
||||
|
||||
const getNowDbInst = () => {
|
||||
return DbInst.getInst(state.dbId);
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
submitUpdateFields,
|
||||
cancelUpdateFields,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.db-table-data {
|
||||
.table {
|
||||
border-left: var(--el-table-border);
|
||||
border-top: var(--el-table-border);
|
||||
}
|
||||
|
||||
.table-column {
|
||||
padding: 0 2px;
|
||||
font-size: 12px;
|
||||
border-right: var(--el-table-border);
|
||||
}
|
||||
|
||||
.table-data-cell {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
line-height: 30px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.data-selection {
|
||||
background-color: var(--el-table-current-row-bg-color);
|
||||
}
|
||||
|
||||
.data-spacing {
|
||||
background-color: var(--el-fill-color-lighter);
|
||||
}
|
||||
|
||||
.update_field_active {
|
||||
background-color: var(--el-color-success);
|
||||
}
|
||||
}
|
||||
</style>
|
||||