mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-02 23:40:24 +08:00
Compare commits
65 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2b524dadb | ||
|
|
8998a21626 | ||
|
|
abc015aec0 | ||
|
|
4ef8d27b1e | ||
|
|
40b6e603fc | ||
|
|
21498584b1 | ||
|
|
408bac09a1 | ||
|
|
582d879a77 | ||
|
|
38ff5152e0 | ||
|
|
d1d372e1bf | ||
|
|
5e4793433b | ||
|
|
54ad19f97e | ||
|
|
fc166650b3 | ||
|
|
2acc295259 | ||
|
|
4b3ed1310d | ||
|
|
b2cfd1517c | ||
|
|
b13d27ccd6 | ||
|
|
68e0088016 | ||
|
|
bd1e83989d | ||
|
|
263dfa6be7 | ||
|
|
eb55f93864 | ||
|
|
8589105e44 | ||
|
|
986b187f0a | ||
|
|
008d34c453 | ||
|
|
49d3f988c9 | ||
|
|
76475e807e | ||
|
|
f93231da61 | ||
|
|
bf75483a3c | ||
|
|
b56b0187cf | ||
|
|
7e7f02b502 | ||
|
|
878985f7c5 | ||
|
|
2133d9b737 | ||
|
|
d711a36749 | ||
|
|
9dbf104ef1 | ||
|
|
20eb06fb28 | ||
|
|
9c20bdef39 | ||
|
|
3fdd98a390 | ||
|
|
d4f456c0cf | ||
|
|
f2b6e15cf4 | ||
|
|
6be0ea6aed | ||
|
|
eee08be2cc | ||
|
|
252fc553f2 | ||
|
|
ac2ceed3f9 | ||
|
|
3f828cc5b0 | ||
|
|
fc1b9ef35d | ||
|
|
d0b71a1c40 | ||
|
|
a743a6a05a | ||
|
|
0e6b9713ce | ||
|
|
b9afbc764d | ||
|
|
923e183a67 | ||
|
|
7e9a381641 | ||
|
|
bed95254d0 | ||
|
|
e4d13f3377 | ||
|
|
d530365ef9 | ||
|
|
070d4ea104 | ||
|
|
3fc86f0fae | ||
|
|
3b77ab2727 | ||
|
|
76cb991282 | ||
|
|
9efd20f1b9 | ||
|
|
de5b9e46d3 | ||
|
|
f27d3d200f | ||
|
|
f4a64b96a9 | ||
|
|
9a59749763 | ||
|
|
b017b902f8 | ||
|
|
7c53353c60 |
@@ -5,12 +5,12 @@ WORKDIR /mayfly
|
|||||||
|
|
||||||
COPY mayfly_go_web .
|
COPY mayfly_go_web .
|
||||||
|
|
||||||
RUN yarn config set registry 'https://registry.npm.taobao.org' && \
|
RUN yarn config set registry 'https://registry.npmmirror.com' && \
|
||||||
yarn install && \
|
yarn install && \
|
||||||
yarn build
|
yarn build
|
||||||
|
|
||||||
# 构建后端资源
|
# 构建后端资源
|
||||||
FROM golang:1.21.5 as be-builder
|
FROM golang:1.22 as be-builder
|
||||||
|
|
||||||
ENV GOPROXY https://goproxy.cn
|
ENV GOPROXY https://goproxy.cn
|
||||||
WORKDIR /mayfly
|
WORKDIR /mayfly
|
||||||
@@ -24,7 +24,7 @@ COPY --from=fe-builder /mayfly/dist /mayfly/static/static
|
|||||||
|
|
||||||
# Build
|
# Build
|
||||||
RUN GO111MODULE=on CGO_ENABLED=0 GOOS=linux \
|
RUN GO111MODULE=on CGO_ENABLED=0 GOOS=linux \
|
||||||
go build -a \
|
go build -a -ldflags=-w \
|
||||||
-o mayfly-go main.go
|
-o mayfly-go main.go
|
||||||
|
|
||||||
FROM debian:bookworm-slim
|
FROM debian:bookworm-slim
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<img src="https://img.shields.io/docker/pulls/mayflygo/mayfly-go.svg?label=docker%20pulls&color=fac858" alt="docker pulls"/>
|
<img src="https://img.shields.io/docker/pulls/mayflygo/mayfly-go.svg?label=docker%20pulls&color=fac858" alt="docker pulls"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/golang/go" target="_blank">
|
<a href="https://github.com/golang/go" target="_blank">
|
||||||
<img src="https://img.shields.io/badge/Golang-1.21%2B-yellow.svg" alt="golang"/>
|
<img src="https://img.shields.io/badge/Golang-1.22%2B-yellow.svg" alt="golang"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://cn.vuejs.org" target="_blank">
|
<a href="https://cn.vuejs.org" target="_blank">
|
||||||
<img src="https://img.shields.io/badge/Vue-3.x-green.svg" alt="vue">
|
<img src="https://img.shields.io/badge/Vue-3.x-green.svg" alt="vue">
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
### 介绍
|
### 介绍
|
||||||
|
|
||||||
web 版 **linux(终端[终端回放] 文件 脚本 进程 计划任务)、数据库(mysql postgres oracle 达梦 高斯)、redis(单机 哨兵 集群)、mongo 统一管理操作平台**
|
web 版 **linux(终端[终端回放] 文件 脚本 进程 计划任务)、数据库(mysql postgres oracle sqlserver 达梦 高斯 sqlite)、redis(单机 哨兵 集群)、mongo 等集工单流程审批于一体的统一管理操作平台**
|
||||||
|
|
||||||
### 开发语言与主要框架
|
### 开发语言与主要框架
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2021 lyt-Top
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -10,31 +10,32 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "^2.3.1",
|
"@element-plus/icons-vue": "^2.3.1",
|
||||||
"@vueuse/core": "^10.7.2",
|
"@vueuse/core": "^10.9.0",
|
||||||
"asciinema-player": "^3.6.3",
|
"asciinema-player": "^3.7.0",
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
"clipboard": "^2.0.11",
|
"clipboard": "^2.0.11",
|
||||||
"countup.js": "^2.7.0",
|
"countup.js": "^2.8.0",
|
||||||
"cropperjs": "^1.5.11",
|
"cropperjs": "^1.6.1",
|
||||||
"echarts": "^5.4.3",
|
"echarts": "^5.5.0",
|
||||||
"element-plus": "^2.5.1",
|
"element-plus": "^2.6.2",
|
||||||
"js-base64": "^3.7.5",
|
"js-base64": "^3.7.7",
|
||||||
"jsencrypt": "^3.3.2",
|
"jsencrypt": "^3.3.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
"monaco-editor": "^0.45.0",
|
"monaco-editor": "^0.47.0",
|
||||||
"monaco-sql-languages": "^0.11.0",
|
"monaco-sql-languages": "^0.11.0",
|
||||||
"monaco-themes": "^0.4.4",
|
"monaco-themes": "^0.4.4",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"qrcode.vue": "^3.4.1",
|
"qrcode.vue": "^3.4.1",
|
||||||
"screenfull": "^6.0.2",
|
"screenfull": "^6.0.2",
|
||||||
"sortablejs": "^1.15.0",
|
"sortablejs": "^1.15.2",
|
||||||
"splitpanes": "^3.1.5",
|
"splitpanes": "^3.1.5",
|
||||||
"sql-formatter": "^15.0.2",
|
"sql-formatter": "^15.0.2",
|
||||||
|
"trzsz": "^1.1.5",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"vue": "^3.4.14",
|
"vue": "^3.4.21",
|
||||||
"vue-router": "^4.2.5",
|
"vue-router": "^4.3.0",
|
||||||
"xterm": "^5.3.0",
|
"xterm": "^5.3.0",
|
||||||
"xterm-addon-fit": "^0.8.0",
|
"xterm-addon-fit": "^0.8.0",
|
||||||
"xterm-addon-search": "^0.13.0",
|
"xterm-addon-search": "^0.13.0",
|
||||||
@@ -44,19 +45,20 @@
|
|||||||
"@types/lodash": "^4.14.178",
|
"@types/lodash": "^4.14.178",
|
||||||
"@types/node": "^18.14.0",
|
"@types/node": "^18.14.0",
|
||||||
"@types/nprogress": "^0.2.0",
|
"@types/nprogress": "^0.2.0",
|
||||||
"@types/sortablejs": "^1.15.3",
|
"@types/sortablejs": "^1.15.8",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
||||||
"@typescript-eslint/parser": "^6.7.4",
|
"@typescript-eslint/parser": "^6.7.4",
|
||||||
"@vitejs/plugin-vue": "^5.0.3",
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
"@vue/compiler-sfc": "^3.4.14",
|
"@vue/compiler-sfc": "^3.4.21",
|
||||||
|
"code-inspector-plugin": "^0.4.5",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"eslint": "^8.35.0",
|
"eslint": "^8.35.0",
|
||||||
"eslint-plugin-vue": "^9.19.2",
|
"eslint-plugin-vue": "^9.21.1",
|
||||||
"prettier": "^3.1.0",
|
"prettier": "^3.2.5",
|
||||||
"sass": "^1.69.0",
|
"sass": "^1.69.0",
|
||||||
"typescript": "^5.3.2",
|
"typescript": "^5.3.2",
|
||||||
"vite": "^5.0.11",
|
"vite": "^5.2.8",
|
||||||
"vue-eslint-parser": "^9.4.0"
|
"vue-eslint-parser": "^9.4.2"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"> 1%",
|
"> 1%",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
:zIndex="10000000"
|
:zIndex="10000000"
|
||||||
:width="210"
|
:width="210"
|
||||||
v-if="themeConfig.isWatermark"
|
v-if="themeConfig.isWatermark"
|
||||||
:font="{ color: 'rgba(180, 180, 180, 0.5)' }"
|
:font="{ color: 'rgba(180, 180, 180, 0.3)' }"
|
||||||
:content="themeConfig.watermarkText"
|
:content="themeConfig.watermarkText"
|
||||||
class="h100"
|
class="h100"
|
||||||
>
|
>
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -55,11 +55,11 @@
|
|||||||
"unicode_decimal": 58905
|
"unicode_decimal": 58905
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"icon_id": "11617944",
|
"icon_id": "25271976",
|
||||||
"name": "oracle",
|
"name": "oracle",
|
||||||
"font_class": "oracle",
|
"font_class": "oracle",
|
||||||
"unicode": "e6ea",
|
"unicode": "e507",
|
||||||
"unicode_decimal": 59114
|
"unicode_decimal": 58631
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"icon_id": "8105644",
|
"icon_id": "8105644",
|
||||||
@@ -67,6 +67,41 @@
|
|||||||
"font_class": "mariadb",
|
"font_class": "mariadb",
|
||||||
"unicode": "e513",
|
"unicode": "e513",
|
||||||
"unicode_decimal": 58643
|
"unicode_decimal": 58643
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "13601813",
|
||||||
|
"name": "sqlite",
|
||||||
|
"font_class": "sqlite",
|
||||||
|
"unicode": "e546",
|
||||||
|
"unicode_decimal": 58694
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "29340317",
|
||||||
|
"name": "temp-mssql",
|
||||||
|
"font_class": "MSSQLNATIVE",
|
||||||
|
"unicode": "e600",
|
||||||
|
"unicode_decimal": 58880
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "7699332",
|
||||||
|
"name": "gaussdb",
|
||||||
|
"font_class": "gauss",
|
||||||
|
"unicode": "e683",
|
||||||
|
"unicode_decimal": 59011
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "34836637",
|
||||||
|
"name": "kingbase",
|
||||||
|
"font_class": "kingbase",
|
||||||
|
"unicode": "e882",
|
||||||
|
"unicode_decimal": 59522
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "33047500",
|
||||||
|
"name": "vastbase",
|
||||||
|
"font_class": "vastbase",
|
||||||
|
"unicode": "e62b",
|
||||||
|
"unicode_decimal": 58923
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ export class EnumValue {
|
|||||||
*/
|
*/
|
||||||
tag: EnumValueTag;
|
tag: EnumValueTag;
|
||||||
|
|
||||||
|
extra: any;
|
||||||
|
|
||||||
constructor(value: any, label: string) {
|
constructor(value: any, label: string) {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
this.label = label;
|
this.label = label;
|
||||||
@@ -53,6 +55,11 @@ export class EnumValue {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setExtra(extra: any): EnumValue {
|
||||||
|
this.extra = extra;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public static of(value: any, label: string): EnumValue {
|
public static of(value: any, label: string): EnumValue {
|
||||||
return new EnumValue(value, label);
|
return new EnumValue(value, label);
|
||||||
}
|
}
|
||||||
@@ -60,11 +67,12 @@ export class EnumValue {
|
|||||||
/**
|
/**
|
||||||
* 根据枚举值获取指定枚举值对象
|
* 根据枚举值获取指定枚举值对象
|
||||||
*
|
*
|
||||||
* @param enumValues 所有枚举值
|
* @param enums 枚举对象
|
||||||
* @param value 需要匹配的枚举值
|
* @param value 需要匹配的枚举值
|
||||||
* @returns 枚举值对象
|
* @returns 枚举值对象
|
||||||
*/
|
*/
|
||||||
static getEnumByValue(enumValues: EnumValue[], value: any): EnumValue | null {
|
static getEnumByValue(enums: any, value: any): EnumValue | null {
|
||||||
|
const enumValues = Object.values(enums) as any;
|
||||||
for (let enumValue of enumValues) {
|
for (let enumValue of enumValues) {
|
||||||
if (enumValue.value == value) {
|
if (enumValue.value == value) {
|
||||||
return enumValue;
|
return enumValue;
|
||||||
|
|||||||
@@ -2,8 +2,13 @@ import EnumValue from './Enum';
|
|||||||
|
|
||||||
// 标签关联的资源类型
|
// 标签关联的资源类型
|
||||||
export const TagResourceTypeEnum = {
|
export const TagResourceTypeEnum = {
|
||||||
Machine: EnumValue.of(1, '机器'),
|
AuthCert: EnumValue.of(-2, '公共凭证').setExtra({ icon: 'Ticket' }),
|
||||||
Db: EnumValue.of(2, '数据库'),
|
Tag: EnumValue.of(-1, '标签').setExtra({ icon: 'CollectionTag' }),
|
||||||
Redis: EnumValue.of(3, 'redis'),
|
|
||||||
Mongo: EnumValue.of(4, 'mongo'),
|
Machine: EnumValue.of(1, '机器').setExtra({ icon: 'Monitor' }).tagTypeSuccess(),
|
||||||
|
Db: EnumValue.of(2, '数据库').setExtra({ icon: 'Coin' }).tagTypeWarning(),
|
||||||
|
Redis: EnumValue.of(3, 'redis').setExtra({ icon: 'iconfont icon-redis' }).tagTypeInfo(),
|
||||||
|
Mongo: EnumValue.of(4, 'mongo').setExtra({ icon: 'iconfont icon-mongo' }).tagTypeDanger(),
|
||||||
|
|
||||||
|
MachineAuthCert: EnumValue.of(11, '机器-授权凭证').setExtra({ icon: 'Ticket' }),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const config = {
|
|||||||
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
|
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
|
||||||
|
|
||||||
// 系统版本
|
// 系统版本
|
||||||
version: 'v1.7.0',
|
version: 'v1.8.0',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
@@ -2,3 +2,8 @@ export const AccountUsernamePattern = {
|
|||||||
pattern: /^[a-zA-Z0-9_]{5,20}$/g,
|
pattern: /^[a-zA-Z0-9_]{5,20}$/g,
|
||||||
message: '只允许输入5-20位大小写字母、数字、下划线',
|
message: '只允许输入5-20位大小写字母、数字、下划线',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ResourceCodePattern = {
|
||||||
|
pattern: /^[a-zA-Z0-9_.:]{1,32}$/g,
|
||||||
|
message: '只允许输入1-32位大小写字母、数字、_.:',
|
||||||
|
};
|
||||||
|
|||||||
@@ -7,24 +7,22 @@ export function exportCsv(filename: string, columns: string[], datas: []) {
|
|||||||
for (let column of columns) {
|
for (let column of columns) {
|
||||||
let val: any = data[column];
|
let val: any = data[column];
|
||||||
if (val == null || val == undefined) {
|
if (val == null || val == undefined) {
|
||||||
dataValueArr.push('');
|
val = '';
|
||||||
continue;
|
} else if (val && typeof val == 'string') {
|
||||||
}
|
// 替换换行符
|
||||||
|
val = val.replace(/[\r\n]/g, '\\n');
|
||||||
|
|
||||||
if (typeof val == 'string' && val) {
|
|
||||||
// csv格式如果有逗号,整体用双引号括起来;如果里面还有双引号就替换成两个双引号,这样导出来的格式就不会有问题了
|
// csv格式如果有逗号,整体用双引号括起来;如果里面还有双引号就替换成两个双引号,这样导出来的格式就不会有问题了
|
||||||
if (val.indexOf(',') != -1) {
|
if (val.indexOf(',') != -1) {
|
||||||
// 如果还有双引号,先将双引号转义,避免两边加了双引号后转义错误
|
// 如果还有双引号,先将双引号转义,避免两边加了双引号后转义错误
|
||||||
if (val.indexOf('"') != -1) {
|
if (val.indexOf('"') != -1) {
|
||||||
val = val.replace(/\"/g, '""');
|
val = val.replace(/"/g, '""');
|
||||||
}
|
}
|
||||||
// 再将逗号转义
|
// 再将逗号转义
|
||||||
val = `"${val}"`;
|
val = `"${val}"`;
|
||||||
}
|
}
|
||||||
dataValueArr.push(val + '\t');
|
|
||||||
} else {
|
|
||||||
dataValueArr.push(val + '\t');
|
|
||||||
}
|
}
|
||||||
|
dataValueArr.push(String(val));
|
||||||
}
|
}
|
||||||
cvsData.push(dataValueArr);
|
cvsData.push(dataValueArr);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,60 +46,6 @@ export function convertToBytes(sizeStr: string) {
|
|||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 格式化json字符串
|
|
||||||
* @param txt json字符串
|
|
||||||
* @param compress 是否压缩
|
|
||||||
* @returns 格式化后的字符串
|
|
||||||
*/
|
|
||||||
export function formatJsonString(txt: string, compress: boolean) {
|
|
||||||
var indentChar = ' ';
|
|
||||||
if (/^\s*$/.test(txt)) {
|
|
||||||
console.log('数据为空,无法格式化! ');
|
|
||||||
return txt;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
var data = JSON.parse(txt);
|
|
||||||
} catch (e: any) {
|
|
||||||
console.log('数据源语法错误,格式化失败! 错误信息: ' + e.description, 'err');
|
|
||||||
return txt;
|
|
||||||
}
|
|
||||||
var draw: any = [],
|
|
||||||
line = compress ? '' : '\n',
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
nodeCount: number = 0,
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
maxDepth: number = 0;
|
|
||||||
|
|
||||||
var notify = function (name: any, value: any, isLast: any, indent: any, formObj: any) {
|
|
||||||
nodeCount++; /*节点计数*/
|
|
||||||
for (var i = 0, tab = ''; i < indent; i++) tab += indentChar; /* 缩进HTML */
|
|
||||||
tab = compress ? '' : tab; /*压缩模式忽略缩进*/
|
|
||||||
maxDepth = ++indent; /*缩进递增并记录*/
|
|
||||||
if (value && value.constructor == Array) {
|
|
||||||
/*处理数组*/
|
|
||||||
draw.push(tab + (formObj ? '"' + name + '": ' : '') + '[' + line); /*缩进'[' 然后换行*/
|
|
||||||
for (var i = 0; i < value.length; i++) notify(i, value[i], i == value.length - 1, indent, false);
|
|
||||||
draw.push(tab + ']' + (isLast ? line : ',' + line)); /*缩进']'换行,若非尾元素则添加逗号*/
|
|
||||||
} else if (value && typeof value == 'object') {
|
|
||||||
/*处理对象*/
|
|
||||||
draw.push(tab + (formObj ? '"' + name + '": ' : '') + '{' + line); /*缩进'{' 然后换行*/
|
|
||||||
var len = 0,
|
|
||||||
i = 0;
|
|
||||||
for (var key in value) len++;
|
|
||||||
for (var key in value) notify(key, value[key], ++i == len, indent, true);
|
|
||||||
draw.push(tab + '}' + (isLast ? line : ',' + line)); /*缩进'}'换行,若非尾元素则添加逗号*/
|
|
||||||
} else {
|
|
||||||
if (typeof value == 'string') value = '"' + value + '"';
|
|
||||||
draw.push(tab + (formObj ? '"' + name + '": ' : '') + value + (isLast ? '' : ',') + line);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
var isLast = true,
|
|
||||||
indent = 0;
|
|
||||||
notify('', data, isLast, indent, false);
|
|
||||||
return draw.join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* 年(Y) 可用1-4个占位符
|
* 年(Y) 可用1-4个占位符
|
||||||
* 月(m)、日(d)、小时(H)、分(M)、秒(S) 可用1-2个占位符
|
* 月(m)、日(d)、小时(H)、分(M)、秒(S) 可用1-2个占位符
|
||||||
@@ -204,6 +150,45 @@ export function formatPast(param: any, format: string = 'YYYY-mm-dd') {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化指定时间数为人性化可阅读的内容(默认time为秒单位)
|
||||||
|
*
|
||||||
|
* @param time 时间数
|
||||||
|
* @param unit time对应的单位
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function formatTime(time: number, unit: string = 's') {
|
||||||
|
const units = {
|
||||||
|
y: 31536000,
|
||||||
|
M: 2592000,
|
||||||
|
d: 86400,
|
||||||
|
h: 3600,
|
||||||
|
m: 60,
|
||||||
|
s: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!units[unit]) {
|
||||||
|
return 'Invalid unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
let seconds = time * units[unit];
|
||||||
|
let result = '';
|
||||||
|
|
||||||
|
const timeUnits = Object.entries(units).map(([unit, duration]) => {
|
||||||
|
const value = Math.floor(seconds / duration);
|
||||||
|
seconds %= duration;
|
||||||
|
return { value, unit };
|
||||||
|
});
|
||||||
|
|
||||||
|
timeUnits.forEach(({ value, unit }) => {
|
||||||
|
if (value > 0) {
|
||||||
|
result += `${value}${unit} `;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* formatAxis(new Date()) // 上午好
|
* formatAxis(new Date()) // 上午好
|
||||||
*/
|
*/
|
||||||
|
|||||||
33
mayfly_go_web/src/common/utils/object.ts
Normal file
33
mayfly_go_web/src/common/utils/object.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* 根据对象访问路径,获取对应的值
|
||||||
|
*
|
||||||
|
* @param obj 对象,如 {user: {name: 'xxx'}, orderNo: 1212211, products: [{id: 12}]}
|
||||||
|
* @param path 访问路径,如 orderNo 或者 user.name 或者product[0].id
|
||||||
|
* @returns 路径对应的值
|
||||||
|
*/
|
||||||
|
export function getValueByPath(obj: any, path: string) {
|
||||||
|
const keys = path.split('.');
|
||||||
|
let result = obj;
|
||||||
|
for (let key of keys) {
|
||||||
|
if (!result || typeof result !== 'object') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.includes('[') && key.includes(']')) {
|
||||||
|
// 处理包含数组索引的情况
|
||||||
|
const arrayKey = key.substring(0, key.indexOf('['));
|
||||||
|
const matchIndex = key.match(/\[(.*?)\]/);
|
||||||
|
|
||||||
|
if (!matchIndex) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = parseInt(matchIndex[1]);
|
||||||
|
result = Array.isArray(result[arrayKey]) ? result[arrayKey][index] : undefined;
|
||||||
|
} else {
|
||||||
|
result = result[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-input v-model="cron" placeholder="可点击左边按钮进行可视化配置">
|
<el-input v-model="cron" placeholder="可点击左边按钮配置">
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<el-button @click="showCron = true" icon="Pointer"></el-button>
|
<el-button @click="showCron = true" icon="Pointer"></el-button>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
33
mayfly_go_web/src/components/drawer-header/DrawerHeader.vue
Normal file
33
mayfly_go_web/src/components/drawer-header/DrawerHeader.vue
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<template>
|
||||||
|
<el-page-header @back="props.back">
|
||||||
|
<template #content>
|
||||||
|
<span>{{ header }}</span>
|
||||||
|
<span v-if="resource && !hideResource">
|
||||||
|
-
|
||||||
|
<el-tooltip v-if="resource.length > 25" :content="resource" placement="bottom">
|
||||||
|
<el-tag effect="dark" type="success">{{ resource.substring(0, 23) + '...' }}</el-tag>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tag v-else effect="dark" type="success">{{ resource }}</el-tag>
|
||||||
|
</span>
|
||||||
|
<el-divider v-if="slots.buttons" direction="vertical" />
|
||||||
|
<slot v-if="slots.buttons" name="buttons"></slot>
|
||||||
|
</template>
|
||||||
|
<template #extra>
|
||||||
|
<slot v-if="slots.extra" name="extra"></slot>
|
||||||
|
</template>
|
||||||
|
</el-page-header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useSlots } from 'vue';
|
||||||
|
const slots = useSlots();
|
||||||
|
|
||||||
|
defineOptions({ name: 'DrawerHeader' });
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
header: String,
|
||||||
|
back: Function,
|
||||||
|
resource: String,
|
||||||
|
hideResource: Boolean,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -40,7 +40,7 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const convert = (value: any) => {
|
const convert = (value: any) => {
|
||||||
const enumValue = EnumValue.getEnumByValue(Object.values(props.enums as any) as any, value) as any;
|
const enumValue = EnumValue.getEnumByValue(props.enums, value) as any;
|
||||||
if (!enumValue) {
|
if (!enumValue) {
|
||||||
state.enumLabel = '-';
|
state.enumLabel = '-';
|
||||||
state.type = 'danger';
|
state.type = 'danger';
|
||||||
|
|||||||
@@ -119,8 +119,8 @@ const open = (optionProps: MonacoEditorDialogProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
editorRef.value?.format();
|
|
||||||
editorRef.value?.focus();
|
editorRef.value?.focus();
|
||||||
|
editorRef.value?.format();
|
||||||
}, 300);
|
}, 300);
|
||||||
|
|
||||||
state.dialogVisible = true;
|
state.dialogVisible = true;
|
||||||
|
|||||||
@@ -74,7 +74,7 @@
|
|||||||
trigger="click"
|
trigger="click"
|
||||||
>
|
>
|
||||||
<div v-for="(item, index) in tableColumns" :key="index">
|
<div v-for="(item, index) in tableColumns" :key="index">
|
||||||
<el-checkbox v-model="item.show" :label="item.label" :true-label="true" :false-label="false" />
|
<el-checkbox v-model="item.show" :label="item.label" :true-value="true" :false-value="false" />
|
||||||
</div>
|
</div>
|
||||||
<template #reference>
|
<template #reference>
|
||||||
<el-button icon="Operation" circle :size="props.size"></el-button>
|
<el-button icon="Operation" circle :size="props.size"></el-button>
|
||||||
@@ -115,18 +115,18 @@
|
|||||||
>
|
>
|
||||||
<!-- 插槽:预留功能 -->
|
<!-- 插槽:预留功能 -->
|
||||||
<template #default="scope" v-if="item.slot">
|
<template #default="scope" v-if="item.slot">
|
||||||
<slot :name="item.prop" :data="scope.row"></slot>
|
<slot :name="item.slotName ? item.slotName : item.prop" :data="scope.row"></slot>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 枚举类型使用tab展示 -->
|
<!-- 枚举类型使用tab展示 -->
|
||||||
<template #default="scope" v-else-if="item.type == 'tag'">
|
<template #default="scope" v-else-if="item.type == 'tag'">
|
||||||
<enum-tag :size="props.size" :enums="item.typeParam" :value="scope.row[item.prop]"></enum-tag>
|
<enum-tag :size="props.size" :enums="item.typeParam" :value="item.getValueByData(scope.row)"></enum-tag>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #default="scope" v-else>
|
<template #default="scope" v-else>
|
||||||
<!-- 配置了美化文本按钮以及文本内容大于指定长度,则显示美化按钮 -->
|
<!-- 配置了美化文本按钮以及文本内容大于指定长度,则显示美化按钮 -->
|
||||||
<el-popover
|
<el-popover
|
||||||
v-if="item.isBeautify && scope.row[item.prop]?.length > 35"
|
v-if="item.isBeautify && item.getValueByData(scope.row)?.length > 35"
|
||||||
effect="light"
|
effect="light"
|
||||||
trigger="click"
|
trigger="click"
|
||||||
placement="top"
|
placement="top"
|
||||||
@@ -137,7 +137,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<template #reference>
|
<template #reference>
|
||||||
<el-link
|
<el-link
|
||||||
@click="formatText(scope.row[item.prop])"
|
@click="formatText(item.getValueByData(scope.row))"
|
||||||
:underline="false"
|
:underline="false"
|
||||||
type="success"
|
type="success"
|
||||||
icon="MagicStick"
|
icon="MagicStick"
|
||||||
@@ -185,11 +185,11 @@ import SvgIcon from '@/components/svgIcon/index.vue';
|
|||||||
import { usePageTable } from '@/hooks/usePageTable';
|
import { usePageTable } from '@/hooks/usePageTable';
|
||||||
import { ElTable } from 'element-plus';
|
import { ElTable } from 'element-plus';
|
||||||
|
|
||||||
const emit = defineEmits(['update:queryForm', 'update:selectionData', 'pageChange']);
|
const emit = defineEmits(['update:selectionData', 'pageChange']);
|
||||||
|
|
||||||
export interface PageTableProps {
|
export interface PageTableProps {
|
||||||
size?: string;
|
size?: string;
|
||||||
pageApi: Api; // 请求表格数据的 api
|
pageApi?: Api; // 请求表格数据的 api
|
||||||
columns: TableColumn[]; // 列配置项 ==> 必传
|
columns: TableColumn[]; // 列配置项 ==> 必传
|
||||||
showSelection?: boolean;
|
showSelection?: boolean;
|
||||||
selectable?: (row: any) => boolean; // 是否可选
|
selectable?: (row: any) => boolean; // 是否可选
|
||||||
@@ -257,7 +257,7 @@ const changeSimpleFormItem = (searchItem: SearchItem) => {
|
|||||||
nowSearchItem.value = searchItem;
|
nowSearchItem.value = searchItem;
|
||||||
};
|
};
|
||||||
|
|
||||||
const { tableData, total, loading, search, reset, getTableData, handlePageNumChange, handlePageSizeChange } = usePageTable(
|
let { tableData, total, loading, search, reset, getTableData, handlePageNumChange, handlePageSizeChange } = usePageTable(
|
||||||
props.pageable,
|
props.pageable,
|
||||||
props.pageApi,
|
props.pageApi,
|
||||||
queryForm,
|
queryForm,
|
||||||
@@ -288,6 +288,13 @@ watch(isShowSearch, () => {
|
|||||||
calcuTableHeight();
|
calcuTableHeight();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.data,
|
||||||
|
(newValue: any) => {
|
||||||
|
tableData = newValue;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
calcuTableHeight();
|
calcuTableHeight();
|
||||||
useEventListener(window, 'resize', calcuTableHeight);
|
useEventListener(window, 'resize', calcuTableHeight);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import EnumValue from '@/common/Enum';
|
import EnumValue from '@/common/Enum';
|
||||||
import { dateFormat } from '@/common/utils/date';
|
import { dateFormat } from '@/common/utils/date';
|
||||||
|
import { getValueByPath } from '@/common/utils/object';
|
||||||
import { getTextWidth } from '@/common/utils/string';
|
import { getTextWidth } from '@/common/utils/string';
|
||||||
|
|
||||||
export class TableColumn {
|
export class TableColumn {
|
||||||
@@ -29,10 +30,15 @@ export class TableColumn {
|
|||||||
minWidth: number | string;
|
minWidth: number | string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否插槽,是的话插槽名则为prop属性名
|
* 是否为插槽,若slotName为空则插槽名为prop属性名
|
||||||
*/
|
*/
|
||||||
slot: boolean = false;
|
slot: boolean = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插槽名,
|
||||||
|
*/
|
||||||
|
slotName: string = '';
|
||||||
|
|
||||||
showOverflowTooltip: boolean = true;
|
showOverflowTooltip: boolean = true;
|
||||||
|
|
||||||
sortable: boolean = false;
|
sortable: boolean = false;
|
||||||
@@ -87,7 +93,7 @@ export class TableColumn {
|
|||||||
if (this.formatFunc) {
|
if (this.formatFunc) {
|
||||||
return this.formatFunc(rowData, this.prop);
|
return this.formatFunc(rowData, this.prop);
|
||||||
}
|
}
|
||||||
return rowData[this.prop];
|
return getValueByPath(rowData, this.prop);
|
||||||
}
|
}
|
||||||
|
|
||||||
static new(prop: string, label: string): TableColumn {
|
static new(prop: string, label: string): TableColumn {
|
||||||
@@ -144,8 +150,9 @@ export class TableColumn {
|
|||||||
* 标识该列为插槽
|
* 标识该列为插槽
|
||||||
* @returns this
|
* @returns this
|
||||||
*/
|
*/
|
||||||
isSlot(): TableColumn {
|
isSlot(slotName: string = ''): TableColumn {
|
||||||
this.slot = true;
|
this.slot = true;
|
||||||
|
this.slotName = slotName;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,7 +172,7 @@ export class TableColumn {
|
|||||||
*/
|
*/
|
||||||
isTime(): TableColumn {
|
isTime(): TableColumn {
|
||||||
this.setFormatFunc((data: any, prop: string) => {
|
this.setFormatFunc((data: any, prop: string) => {
|
||||||
return dateFormat(data[prop]);
|
return dateFormat(getValueByPath(data, prop));
|
||||||
});
|
});
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@@ -176,7 +183,7 @@ export class TableColumn {
|
|||||||
*/
|
*/
|
||||||
isEnum(enums: any): TableColumn {
|
isEnum(enums: any): TableColumn {
|
||||||
this.setFormatFunc((data: any, prop: string) => {
|
this.setFormatFunc((data: any, prop: string) => {
|
||||||
return EnumValue.getLabelByValue(enums, data[prop]);
|
return EnumValue.getLabelByValue(enums, getValueByPath(data, prop));
|
||||||
});
|
});
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@@ -218,7 +225,7 @@ export class TableColumn {
|
|||||||
// 获取该列中最长的数据(内容)
|
// 获取该列中最长的数据(内容)
|
||||||
for (let i = 0; i < tableData.length; i++) {
|
for (let i = 0; i < tableData.length; i++) {
|
||||||
let nowData = tableData[i];
|
let nowData = tableData[i];
|
||||||
let nowValue = nowData[prop];
|
let nowValue = getValueByPath(nowData, prop);
|
||||||
if (!nowValue) {
|
if (!nowValue) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
478
mayfly_go_web/src/components/terminal-rdp/MachineRdp.vue
Normal file
478
mayfly_go_web/src/components/terminal-rdp/MachineRdp.vue
Normal file
@@ -0,0 +1,478 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="viewportRef" class="viewport" :style="{ width: state.size.width + 'px', height: state.size.height + 'px' }">
|
||||||
|
<div ref="displayRef" class="display" tabindex="0" />
|
||||||
|
<div class="btn-box">
|
||||||
|
<SvgIcon name="DocumentCopy" @click="openPaste" :size="20" class="pointer-icon mr10" title="剪贴板" />
|
||||||
|
<SvgIcon name="FolderOpened" @click="openFilesystem" :size="20" class="pointer-icon mr10" title="文件管理" />
|
||||||
|
<SvgIcon name="FullScreen" @click="state.fullscreen ? closeFullScreen() : openFullScreen()" :size="20" class="pointer-icon mr10" title="全屏" />
|
||||||
|
|
||||||
|
<el-dropdown>
|
||||||
|
<SvgIcon name="Monitor" :size="20" class="pointer-icon mr10" title="发送快捷键" style="color: #fff" />
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item @click="openSendKeyboard(['65507', '65513', '65535'])"> Ctrl + Alt + Delete </el-dropdown-item>
|
||||||
|
<el-dropdown-item @click="openSendKeyboard(['65507', '65513', '65288'])"> Ctrl + Alt + Backspace </el-dropdown-item>
|
||||||
|
<el-dropdown-item @click="openSendKeyboard(['65515', '100'])"> Windows + D </el-dropdown-item>
|
||||||
|
<el-dropdown-item @click="openSendKeyboard(['65515', '101'])"> Windows + E </el-dropdown-item>
|
||||||
|
<el-dropdown-item @click="openSendKeyboard(['65515', '114'])"> Windows + R </el-dropdown-item>
|
||||||
|
<el-dropdown-item @click="openSendKeyboard(['65515'])"> Windows </el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
|
||||||
|
<SvgIcon name="Refresh" @click="connect(0, 0)" :size="20" class="pointer-icon mr10" title="重新连接" />
|
||||||
|
</div>
|
||||||
|
<clipboard-dialog ref="clipboardRef" v-model:visible="state.clipboardDialog.visible" @close="closePaste" @submit="onsubmitClipboard" />
|
||||||
|
|
||||||
|
<el-dialog destroy-on-close :title="state.filesystemDialog.title" v-model="state.filesystemDialog.visible" :close-on-click-modal="false" width="70%">
|
||||||
|
<machine-file
|
||||||
|
:machine-id="state.filesystemDialog.machineId"
|
||||||
|
:auth-cert-name="state.filesystemDialog.authCertName"
|
||||||
|
:protocol="state.filesystemDialog.protocol"
|
||||||
|
:file-id="state.filesystemDialog.fileId"
|
||||||
|
:path="state.filesystemDialog.path"
|
||||||
|
/>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import Guacamole from './guac/guacamole-common';
|
||||||
|
import { getMachineRdpSocketUrl } from '@/views/ops/machine/api';
|
||||||
|
import clipboard from './guac/clipboard';
|
||||||
|
import { reactive, ref } from 'vue';
|
||||||
|
import { TerminalStatus } from '@/components/terminal/common';
|
||||||
|
import ClipboardDialog from '@/components/terminal-rdp/guac/ClipboardDialog.vue';
|
||||||
|
import { TerminalExpose } from '@/components/terminal-rdp/index';
|
||||||
|
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||||
|
import MachineFile from '@/views/ops/machine/file/MachineFile.vue';
|
||||||
|
import { exitFullscreen, launchIntoFullscreen, unWatchFullscreenChange, watchFullscreenChange } from '@/components/terminal-rdp/guac/screen';
|
||||||
|
import { useEventListener } from '@vueuse/core';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
import { ClientState, TunnelState } from '@/components/terminal-rdp/guac/states';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
|
||||||
|
const viewportRef = ref({} as any);
|
||||||
|
const displayRef = ref({} as any);
|
||||||
|
const clipboardRef = ref({} as any);
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
machineId: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
authCert: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
clipboardList: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['statusChange']);
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
client: null as any,
|
||||||
|
display: null as any,
|
||||||
|
displayElm: {} as any,
|
||||||
|
clipboard: {} as any,
|
||||||
|
keyboard: {} as any,
|
||||||
|
mouse: null as any,
|
||||||
|
touchpad: null as any,
|
||||||
|
errorMessage: '',
|
||||||
|
arguments: {},
|
||||||
|
status: TerminalStatus.NoConnected,
|
||||||
|
size: {
|
||||||
|
height: 710,
|
||||||
|
width: 1024,
|
||||||
|
force: false,
|
||||||
|
},
|
||||||
|
enableClipboard: true,
|
||||||
|
clipboardDialog: {
|
||||||
|
visible: false,
|
||||||
|
},
|
||||||
|
filesystemDialog: {
|
||||||
|
visible: false,
|
||||||
|
authCertName: '',
|
||||||
|
machineId: 0,
|
||||||
|
protocol: 1,
|
||||||
|
title: '',
|
||||||
|
fileId: 0,
|
||||||
|
path: '',
|
||||||
|
},
|
||||||
|
fullscreen: false,
|
||||||
|
beforeFullSize: {
|
||||||
|
height: 710,
|
||||||
|
width: 1024,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const installKeyboard = () => {
|
||||||
|
state.keyboard = new Guacamole.Keyboard(state.displayElm);
|
||||||
|
uninstallKeyboard();
|
||||||
|
state.keyboard.onkeydown = (keysym: any) => {
|
||||||
|
state.client.sendKeyEvent(1, keysym);
|
||||||
|
};
|
||||||
|
state.keyboard.onkeyup = (keysym: any) => {
|
||||||
|
state.client.sendKeyEvent(0, keysym);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const uninstallKeyboard = () => {
|
||||||
|
state.keyboard!.onkeydown = state.keyboard!.onkeyup = () => {};
|
||||||
|
};
|
||||||
|
|
||||||
|
const installMouse = () => {
|
||||||
|
state.mouse = new Guacamole.Mouse(state.displayElm);
|
||||||
|
// Hide software cursor when mouse leaves display
|
||||||
|
state.mouse.onmouseout = () => {
|
||||||
|
if (!state.display) return;
|
||||||
|
state.display.showCursor(false);
|
||||||
|
};
|
||||||
|
state.mouse.onmousedown = state.mouse.onmouseup = state.mouse.onmousemove = handleMouseState;
|
||||||
|
};
|
||||||
|
|
||||||
|
const installTouchpad = () => {
|
||||||
|
state.touchpad = new Guacamole.Mouse.Touchpad(state.displayElm);
|
||||||
|
|
||||||
|
state.touchpad.onmousedown =
|
||||||
|
state.touchpad.onmouseup =
|
||||||
|
state.touchpad.onmousemove =
|
||||||
|
(st: any) => {
|
||||||
|
// 记录按下时,光标所在位置
|
||||||
|
console.log(st);
|
||||||
|
handleMouseState(st, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 记录单指按压时候手在屏幕的位置
|
||||||
|
state.displayElm.ontouchend = (event: TouchEvent) => {
|
||||||
|
console.log('end', event);
|
||||||
|
state.displayElm.ontouchend = () => {};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const setClipboard = (data: string) => {
|
||||||
|
clipboardRef.value.setValue(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const installClipboard = () => {
|
||||||
|
state.enableClipboard = clipboard.install(state.client) as any;
|
||||||
|
clipboard.installWatcher(props.clipboardList, setClipboard);
|
||||||
|
state.client.onclipboard = clipboard.onClipboard;
|
||||||
|
};
|
||||||
|
|
||||||
|
const installResize = () => {
|
||||||
|
// 在resize事件结束后300毫秒执行
|
||||||
|
useEventListener('resize', debounce(resize, 300));
|
||||||
|
};
|
||||||
|
|
||||||
|
const installDisplay = () => {
|
||||||
|
let { width, height, force } = state.size;
|
||||||
|
state.display = state.client.getDisplay();
|
||||||
|
const displayElm = displayRef.value;
|
||||||
|
displayElm.appendChild(state.display.getElement());
|
||||||
|
displayElm.addEventListener('contextmenu', (e: any) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (e.preventDefault) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
e.returnValue = false;
|
||||||
|
});
|
||||||
|
state.client.connect('width=' + width + '&height=' + height + '&force=' + force);
|
||||||
|
window.onunload = () => state.client.disconnect();
|
||||||
|
|
||||||
|
// allows focusing on the display div so that keyboard doesn't always go to session
|
||||||
|
displayElm.onclick = () => {
|
||||||
|
displayElm.focus();
|
||||||
|
};
|
||||||
|
displayElm.onfocus = () => {
|
||||||
|
displayElm.className = 'focus';
|
||||||
|
};
|
||||||
|
displayElm.onblur = () => {
|
||||||
|
displayElm.className = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
state.displayElm = displayElm;
|
||||||
|
};
|
||||||
|
|
||||||
|
const installClient = () => {
|
||||||
|
let tunnel = new Guacamole.WebSocketTunnel(getMachineRdpSocketUrl(props.authCert)) as any;
|
||||||
|
if (state.client) {
|
||||||
|
state.display?.scale(0);
|
||||||
|
uninstallKeyboard();
|
||||||
|
state.client.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
state.client = new Guacamole.Client(tunnel);
|
||||||
|
|
||||||
|
tunnel.onerror = (status: any) => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(`Tunnel failed ${JSON.stringify(status)}`);
|
||||||
|
// state.connectionState = states.TUNNEL_ERROR;
|
||||||
|
};
|
||||||
|
|
||||||
|
tunnel.onstatechange = (st: any) => {
|
||||||
|
console.log('statechange', st);
|
||||||
|
state.status = st;
|
||||||
|
switch (st) {
|
||||||
|
case TunnelState.CONNECTING: // 'CONNECTING'
|
||||||
|
break;
|
||||||
|
case TunnelState.OPEN: // 'OPEN'
|
||||||
|
state.status = TerminalStatus.Connected;
|
||||||
|
emit('statusChange', TerminalStatus.Connected);
|
||||||
|
break;
|
||||||
|
case TunnelState.CLOSED: // 'CLOSED'
|
||||||
|
state.status = TerminalStatus.Disconnected;
|
||||||
|
emit('statusChange', TerminalStatus.Disconnected);
|
||||||
|
break;
|
||||||
|
case TunnelState.UNSTABLE: // 'UNSTABLE'
|
||||||
|
state.status = TerminalStatus.Error;
|
||||||
|
emit('statusChange', TerminalStatus.Error);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
state.client.onstatechange = (clientState: any) => {
|
||||||
|
console.log('clientState', clientState);
|
||||||
|
switch (clientState) {
|
||||||
|
case ClientState.IDLE:
|
||||||
|
console.log('连接空闲');
|
||||||
|
break;
|
||||||
|
case ClientState.CONNECTING:
|
||||||
|
console.log('连接中...');
|
||||||
|
break;
|
||||||
|
case ClientState.WAITING:
|
||||||
|
console.log('等待服务器响应...');
|
||||||
|
break;
|
||||||
|
case ClientState.CONNECTED:
|
||||||
|
console.log('连接成功...');
|
||||||
|
break;
|
||||||
|
// eslint-disable-next-line no-fallthrough
|
||||||
|
case ClientState.DISCONNECTING:
|
||||||
|
console.log('断开连接中...');
|
||||||
|
break;
|
||||||
|
case ClientState.DISCONNECTED:
|
||||||
|
console.log('已断开连接...');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
state.client.onerror = (error: any) => {
|
||||||
|
state.client.disconnect();
|
||||||
|
console.error(`Client error ${JSON.stringify(error)}`);
|
||||||
|
state.errorMessage = error.message;
|
||||||
|
// state.connectionState = states.CLIENT_ERROR;
|
||||||
|
};
|
||||||
|
|
||||||
|
state.client.onsync = () => {};
|
||||||
|
|
||||||
|
state.client.onargv = (stream: any, mimetype: any, name: any) => {
|
||||||
|
if (mimetype !== 'text/plain') return;
|
||||||
|
|
||||||
|
const reader = new Guacamole.StringReader(stream);
|
||||||
|
|
||||||
|
// Assemble received data into a single string
|
||||||
|
let value = '';
|
||||||
|
reader.ontext = (text: any) => {
|
||||||
|
value += text;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test mutability once stream is finished, storing the current value for the argument only if it is mutable
|
||||||
|
reader.onend = () => {
|
||||||
|
const stream = state.client.createArgumentValueStream('text/plain', name);
|
||||||
|
stream.onack = (status: any) => {
|
||||||
|
if (status.isError()) {
|
||||||
|
// ignore reject
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.arguments[name] = value;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const resize = () => {
|
||||||
|
const elm = viewportRef.value;
|
||||||
|
if (!elm || !elm.offsetWidth) {
|
||||||
|
// resize is being called on the hidden window
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let box = elm.parentElement;
|
||||||
|
|
||||||
|
state.size.width = box.clientWidth;
|
||||||
|
state.size.height = box.clientHeight;
|
||||||
|
|
||||||
|
const width = parseInt(String(box.clientWidth));
|
||||||
|
const height = parseInt(String(box.clientHeight));
|
||||||
|
|
||||||
|
if (state.display.getWidth() !== width || state.display.getHeight() !== height) {
|
||||||
|
if (state.status !== TerminalStatus.Connected) {
|
||||||
|
connect(width, height);
|
||||||
|
} else {
|
||||||
|
state.client.sendSize(width, height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// setting timeout so display has time to get the correct size
|
||||||
|
// setTimeout(() => {
|
||||||
|
// const scale = Math.min(box.clientWidth / Math.max(state.display.getWidth(), 1), box.clientHeight / Math.max(state.display.getHeight(), 1));
|
||||||
|
// state.display.scale(scale);
|
||||||
|
// console.log(state.size, scale);
|
||||||
|
// }, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseState = (mouseState: any, showCursor = false) => {
|
||||||
|
state.client.getDisplay().showCursor(showCursor);
|
||||||
|
|
||||||
|
const scaledMouseState = Object.assign({}, mouseState, {
|
||||||
|
x: mouseState.x / state.display.getScale(),
|
||||||
|
y: mouseState.y / state.display.getScale(),
|
||||||
|
});
|
||||||
|
state.client.sendMouseState(scaledMouseState);
|
||||||
|
};
|
||||||
|
|
||||||
|
const connect = (width: number, height: number, force = false) => {
|
||||||
|
if (!width && !height) {
|
||||||
|
if (state.size && state.size.width && state.size.height) {
|
||||||
|
width = state.size.width;
|
||||||
|
height = state.size.height;
|
||||||
|
} else {
|
||||||
|
// 获取当前viewportRef宽高
|
||||||
|
width = viewportRef.value.clientWidth;
|
||||||
|
height = viewportRef.value.clientHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.size = { width, height, force };
|
||||||
|
|
||||||
|
installClient();
|
||||||
|
installDisplay();
|
||||||
|
installKeyboard();
|
||||||
|
installMouse();
|
||||||
|
installTouchpad();
|
||||||
|
installClipboard();
|
||||||
|
installResize();
|
||||||
|
};
|
||||||
|
|
||||||
|
const disconnect = () => {
|
||||||
|
uninstallKeyboard();
|
||||||
|
state.client?.disconnect();
|
||||||
|
};
|
||||||
|
|
||||||
|
const blur = () => {
|
||||||
|
uninstallKeyboard();
|
||||||
|
};
|
||||||
|
|
||||||
|
const focus = () => {};
|
||||||
|
|
||||||
|
const openPaste = async () => {
|
||||||
|
state.clipboardDialog.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closePaste = async () => {
|
||||||
|
installKeyboard();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onsubmitClipboard = (val: string) => {
|
||||||
|
state.clipboardDialog.visible = false;
|
||||||
|
installKeyboard();
|
||||||
|
clipboard.sendRemoteClipboard(state.client, val);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openFilesystem = async () => {
|
||||||
|
state.filesystemDialog.protocol = 2;
|
||||||
|
state.filesystemDialog.machineId = props.machineId;
|
||||||
|
state.filesystemDialog.authCertName = props.authCert;
|
||||||
|
state.filesystemDialog.fileId = props.machineId;
|
||||||
|
state.filesystemDialog.path = '/';
|
||||||
|
state.filesystemDialog.title = `远程桌面文件管理`;
|
||||||
|
state.filesystemDialog.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openFullScreen = function () {
|
||||||
|
launchIntoFullscreen(viewportRef.value);
|
||||||
|
state.fullscreen = true;
|
||||||
|
|
||||||
|
// 记录原始尺寸
|
||||||
|
state.beforeFullSize = {
|
||||||
|
width: state.size.width,
|
||||||
|
height: state.size.height,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 使用新的宽高重新连接
|
||||||
|
setTimeout(() => {
|
||||||
|
connect(viewportRef.value.clientWidth, viewportRef.value.clientHeight, false);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
watchFullscreenChange(watchFullscreen);
|
||||||
|
};
|
||||||
|
|
||||||
|
function watchFullscreen(event: Event, isFull: boolean) {
|
||||||
|
if (!isFull) {
|
||||||
|
closeFullScreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeFullScreen = function () {
|
||||||
|
exitFullscreen();
|
||||||
|
|
||||||
|
state.fullscreen = false;
|
||||||
|
|
||||||
|
// 使用新的宽高重新连接
|
||||||
|
setTimeout(() => {
|
||||||
|
connect(state.beforeFullSize.width, state.beforeFullSize.height, false);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
// 取消注册esc事件,退出全屏
|
||||||
|
unWatchFullscreenChange(watchFullscreen);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openSendKeyboard = (keys: string[]) => {
|
||||||
|
if (!state.client) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
state.client.sendKeyEvent(1, keys[i]);
|
||||||
|
}
|
||||||
|
for (let j = 0; j < keys.length; j++) {
|
||||||
|
state.client.sendKeyEvent(0, keys[j]);
|
||||||
|
}
|
||||||
|
ElMessage.success('发送组合键成功');
|
||||||
|
};
|
||||||
|
|
||||||
|
const exposes = {
|
||||||
|
connect,
|
||||||
|
disconnect,
|
||||||
|
init: connect,
|
||||||
|
close: disconnect,
|
||||||
|
fitTerminal: resize,
|
||||||
|
focus,
|
||||||
|
blur,
|
||||||
|
setRemoteClipboard: onsubmitClipboard,
|
||||||
|
} as TerminalExpose;
|
||||||
|
|
||||||
|
defineExpose(exposes);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.viewport {
|
||||||
|
position: relative;
|
||||||
|
width: 1024px;
|
||||||
|
min-height: 710px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.display {
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.btn-box {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
right: 30px;
|
||||||
|
padding: 5px 0 5px 10px;
|
||||||
|
background: #dddddd4a;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
130
mayfly_go_web/src/components/terminal-rdp/MachineRdpDialog.vue
Normal file
130
mayfly_go_web/src/components/terminal-rdp/MachineRdpDialog.vue
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
<template>
|
||||||
|
<div class="rdpDialog" ref="dialogRef">
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:before-close="handleClose"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
:destroy-on-close="true"
|
||||||
|
:close-on-press-escape="false"
|
||||||
|
:show-close="false"
|
||||||
|
width="1024"
|
||||||
|
@open="connect()"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="terminal-title-wrapper">
|
||||||
|
<!-- 左侧 -->
|
||||||
|
<div class="title-left-fixed">
|
||||||
|
<!-- title信息 -->
|
||||||
|
<div>
|
||||||
|
{{ title }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧 -->
|
||||||
|
<div class="title-right-fixed">
|
||||||
|
<el-popconfirm @confirm="connect(true)" title="确认重新连接?">
|
||||||
|
<template #reference>
|
||||||
|
<div class="mr10 pointer">
|
||||||
|
<el-tag v-if="state.status == TerminalStatus.Connected" type="success" effect="light" round> 已连接 </el-tag>
|
||||||
|
<el-tag v-else type="danger" effect="light" round> 未连接,点击重连 </el-tag>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-popconfirm>
|
||||||
|
|
||||||
|
<el-popconfirm @confirm="handleClose" title="确认关闭?">
|
||||||
|
<template #reference>
|
||||||
|
<SvgIcon name="Close" class="pointer-icon" title="关闭" :size="20" />
|
||||||
|
</template>
|
||||||
|
</el-popconfirm>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<machine-rdp ref="rdpRef" :machine-id="machineId" @status-change="handleStatusChange" />
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { reactive, ref, toRefs, watch } from 'vue';
|
||||||
|
import MachineRdp from '@/components/terminal-rdp/MachineRdp.vue';
|
||||||
|
import { TerminalStatus } from '@/components/terminal/common';
|
||||||
|
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||||
|
|
||||||
|
const rdpRef = ref({} as any);
|
||||||
|
const dialogRef = ref({} as any);
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visible: { type: Boolean },
|
||||||
|
machineId: { type: Number },
|
||||||
|
title: { type: String },
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:visible', 'cancel', 'update:machineId']);
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
dialogVisible: false,
|
||||||
|
title: '',
|
||||||
|
status: TerminalStatus.NoConnected,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { dialogVisible } = toRefs(state);
|
||||||
|
|
||||||
|
watch(props, async (newValue: any) => {
|
||||||
|
const visible = newValue.visible;
|
||||||
|
state.dialogVisible = visible;
|
||||||
|
if (visible) {
|
||||||
|
state.title = newValue.title;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const connect = (force = false) => {
|
||||||
|
rdpRef.value?.disconnect();
|
||||||
|
|
||||||
|
let width = 1024;
|
||||||
|
let height = 710;
|
||||||
|
rdpRef.value?.connect(width, height, force);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStatusChange = (status: TerminalStatus) => {
|
||||||
|
state.status = status;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭取消按钮触发的事件
|
||||||
|
*/
|
||||||
|
const handleClose = () => {
|
||||||
|
emit('update:visible', false);
|
||||||
|
emit('update:machineId', null);
|
||||||
|
emit('cancel');
|
||||||
|
rdpRef.value?.disconnect();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss">
|
||||||
|
.rdpDialog {
|
||||||
|
.el-dialog {
|
||||||
|
padding: 0;
|
||||||
|
.el-dialog__header {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-overlay .el-overlay-dialog .el-dialog .el-dialog__body {
|
||||||
|
max-height: 100% !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-title-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 16px;
|
||||||
|
|
||||||
|
.title-right-fixed {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 20px;
|
||||||
|
text-align: end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<template>
|
||||||
|
<div class="clipboard-dialog">
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
title="请输入需要粘贴的文本"
|
||||||
|
:before-close="onclose"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
:close-on-press-escape="false"
|
||||||
|
width="600"
|
||||||
|
>
|
||||||
|
<el-input v-model="state.modelValue" type="textarea" :rows="20" />
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button type="primary" @click="onsubmit">确 定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, toRefs, watch } from 'vue';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visible: { type: Boolean },
|
||||||
|
});
|
||||||
|
|
||||||
|
const emits = defineEmits(['submit', 'close', 'update:visible']);
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
dialogVisible: false,
|
||||||
|
modelValue: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { dialogVisible } = toRefs(state);
|
||||||
|
|
||||||
|
watch(props, async (newValue: any) => {
|
||||||
|
state.dialogVisible = newValue.visible;
|
||||||
|
});
|
||||||
|
|
||||||
|
const onclose = () => {
|
||||||
|
emits('update:visible', false);
|
||||||
|
emits('close');
|
||||||
|
};
|
||||||
|
|
||||||
|
const onsubmit = () => {
|
||||||
|
state.dialogVisible = false;
|
||||||
|
if (state.modelValue) {
|
||||||
|
ElMessage.success('发送剪贴板数据成功');
|
||||||
|
emits('submit', state.modelValue);
|
||||||
|
} else {
|
||||||
|
ElMessage.warning('请输入需要粘贴的文本');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setValue = (val: string) => {
|
||||||
|
state.modelValue = val;
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({ setValue });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.clipboard-dialog {
|
||||||
|
}
|
||||||
|
</style>
|
||||||
147
mayfly_go_web/src/components/terminal-rdp/guac/clipboard.js
Normal file
147
mayfly_go_web/src/components/terminal-rdp/guac/clipboard.js
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import Guacamole from './guacamole-common';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
|
||||||
|
const clipboard = {};
|
||||||
|
|
||||||
|
clipboard.install = (client) => {
|
||||||
|
if (!navigator.clipboard) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
clipboard.getLocalClipboard().then((data) => (clipboard.cache = data));
|
||||||
|
|
||||||
|
window.addEventListener('load', clipboard.update(client), true);
|
||||||
|
window.addEventListener('copy', clipboard.update(client));
|
||||||
|
window.addEventListener('cut', clipboard.update(client));
|
||||||
|
window.addEventListener(
|
||||||
|
'focus',
|
||||||
|
(e) => {
|
||||||
|
if (e.target === window) {
|
||||||
|
clipboard.update(client)();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
clipboard.update = (client) => {
|
||||||
|
return () => {
|
||||||
|
clipboard.getLocalClipboard().then((data) => {
|
||||||
|
clipboard.cache = data;
|
||||||
|
clipboard.setRemoteClipboard(client);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
clipboard.sendRemoteClipboard = (client, text) => {
|
||||||
|
clipboard.cache = {
|
||||||
|
type: 'text/plain',
|
||||||
|
data: text,
|
||||||
|
};
|
||||||
|
|
||||||
|
clipboard.setRemoteClipboard(client);
|
||||||
|
};
|
||||||
|
|
||||||
|
clipboard.setRemoteClipboard = (client) => {
|
||||||
|
if (!clipboard.cache) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let writer;
|
||||||
|
|
||||||
|
const stream = client.createClipboardStream(clipboard.cache.type);
|
||||||
|
|
||||||
|
if (typeof clipboard.cache.data === 'string') {
|
||||||
|
writer = new Guacamole.StringWriter(stream);
|
||||||
|
writer.sendText(clipboard.cache.data);
|
||||||
|
writer.sendEnd();
|
||||||
|
|
||||||
|
clipboard.appendClipboardList('up', clipboard.cache.data);
|
||||||
|
} else {
|
||||||
|
writer = new Guacamole.BlobWriter(stream);
|
||||||
|
writer.oncomplete = function clipboardSent() {
|
||||||
|
writer.sendEnd();
|
||||||
|
};
|
||||||
|
writer.sendBlob(clipboard.cache.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
clipboard.getLocalClipboard = async () => {
|
||||||
|
// 获取本地剪贴板数据
|
||||||
|
if (navigator.clipboard && navigator.clipboard.readText) {
|
||||||
|
const text = await navigator.clipboard.readText();
|
||||||
|
return {
|
||||||
|
type: 'text/plain',
|
||||||
|
data: text,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
ElMessage.warning('只有https才可以访问剪贴板');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
clipboard.setLocalClipboard = async (data) => {
|
||||||
|
if (data.type === 'text/plain') {
|
||||||
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
|
await navigator.clipboard.writeText(data.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取到远程服务器剪贴板变动
|
||||||
|
clipboard.onClipboard = (stream, mimetype) => {
|
||||||
|
let reader;
|
||||||
|
|
||||||
|
if (/^text\//.exec(mimetype)) {
|
||||||
|
reader = new Guacamole.StringReader(stream);
|
||||||
|
|
||||||
|
// Assemble received data into a single string
|
||||||
|
let data = '';
|
||||||
|
reader.ontext = (text) => {
|
||||||
|
data += text;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set clipboard contents once stream is finished
|
||||||
|
reader.onend = () => {
|
||||||
|
clipboard.setLocalClipboard({
|
||||||
|
type: mimetype,
|
||||||
|
data: data,
|
||||||
|
});
|
||||||
|
|
||||||
|
clipboard.setClipboardFn && typeof clipboard.setClipboardFn === 'function' && clipboard.setClipboardFn(data);
|
||||||
|
|
||||||
|
clipboard.appendClipboardList('down', data);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
reader = new Guacamole.BlobReader(stream, mimetype);
|
||||||
|
reader.onend = () => {
|
||||||
|
clipboard.setLocalClipboard({
|
||||||
|
type: mimetype,
|
||||||
|
data: reader.getBlob(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/***
|
||||||
|
* 注册剪贴板监听器,如果有本地或远程剪贴板变动,则会更新剪贴板列表
|
||||||
|
*/
|
||||||
|
clipboard.installWatcher = (clipboardList, setClipboardFn) => {
|
||||||
|
clipboard.clipboardList = clipboardList;
|
||||||
|
clipboard.setClipboardFn = setClipboardFn;
|
||||||
|
};
|
||||||
|
|
||||||
|
clipboard.appendClipboardList = (src, data) => {
|
||||||
|
clipboard.clipboardList = clipboard.clipboardList || [];
|
||||||
|
// 循环判断是否重复
|
||||||
|
for (let i = 0; i < clipboard.clipboardList.length; i++) {
|
||||||
|
if (clipboard.clipboardList[i].data === data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clipboard.clipboardList.push({ type: 'text/plain', data, src });
|
||||||
|
};
|
||||||
|
|
||||||
|
export default clipboard;
|
||||||
14441
mayfly_go_web/src/components/terminal-rdp/guac/guacamole-common.js
Normal file
14441
mayfly_go_web/src/components/terminal-rdp/guac/guacamole-common.js
Normal file
File diff suppressed because it is too large
Load Diff
38
mayfly_go_web/src/components/terminal-rdp/guac/screen.js
Normal file
38
mayfly_go_web/src/components/terminal-rdp/guac/screen.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
export function launchIntoFullscreen(element) {
|
||||||
|
if (element.requestFullscreen) {
|
||||||
|
element.requestFullscreen();
|
||||||
|
} else if (element.mozRequestFullScreen) {
|
||||||
|
element.mozRequestFullScreen();
|
||||||
|
} else if (element.webkitRequestFullscreen) {
|
||||||
|
element.webkitRequestFullscreen();
|
||||||
|
} else if (element.msRequestFullscreen) {
|
||||||
|
element.msRequestFullscreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export function exitFullscreen() {
|
||||||
|
if (document.exitFullscreen) {
|
||||||
|
document.exitFullscreen();
|
||||||
|
} else if (document.mozCancelFullScreen) {
|
||||||
|
document.mozCancelFullScreen();
|
||||||
|
} else if (document.webkitExitFullscreen) {
|
||||||
|
document.webkitExitFullscreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function watchFullscreenChange(callback) {
|
||||||
|
function onFullscreenChange(e) {
|
||||||
|
let isFull = (document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.msFullscreenElement) != null;
|
||||||
|
callback(e, isFull);
|
||||||
|
}
|
||||||
|
document.addEventListener('fullscreenchange', onFullscreenChange);
|
||||||
|
document.addEventListener('mozfullscreenchange', onFullscreenChange);
|
||||||
|
document.addEventListener('webkitfullscreenchange', onFullscreenChange);
|
||||||
|
document.addEventListener('msfullscreenchange', onFullscreenChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unWatchFullscreenChange(callback) {
|
||||||
|
document.removeEventListener('fullscreenchange', callback);
|
||||||
|
document.removeEventListener('mozfullscreenchange', callback);
|
||||||
|
document.removeEventListener('webkitfullscreenchange', callback);
|
||||||
|
document.removeEventListener('msfullscreenchange', callback);
|
||||||
|
}
|
||||||
78
mayfly_go_web/src/components/terminal-rdp/guac/states.js
Normal file
78
mayfly_go_web/src/components/terminal-rdp/guac/states.js
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
export const ClientState = {
|
||||||
|
/**
|
||||||
|
* The client is idle, with no active connection.
|
||||||
|
*
|
||||||
|
* @type number
|
||||||
|
*/
|
||||||
|
IDLE: 0,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The client is in the process of establishing a connection.
|
||||||
|
*
|
||||||
|
* @type {!number}
|
||||||
|
*/
|
||||||
|
CONNECTING: 1,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The client is waiting on further information or a remote server to
|
||||||
|
* establish the connection.
|
||||||
|
*
|
||||||
|
* @type {!number}
|
||||||
|
*/
|
||||||
|
WAITING: 2,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The client is actively connected to a remote server.
|
||||||
|
*
|
||||||
|
* @type {!number}
|
||||||
|
*/
|
||||||
|
CONNECTED: 3,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The client is in the process of disconnecting from the remote server.
|
||||||
|
*
|
||||||
|
* @type {!number}
|
||||||
|
*/
|
||||||
|
DISCONNECTING: 4,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The client has completed the connection and is no longer connected.
|
||||||
|
*
|
||||||
|
* @type {!number}
|
||||||
|
*/
|
||||||
|
DISCONNECTED: 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TunnelState = {
|
||||||
|
/**
|
||||||
|
* A connection is in pending. It is not yet known whether connection was
|
||||||
|
* successful.
|
||||||
|
*
|
||||||
|
* @type {!number}
|
||||||
|
*/
|
||||||
|
CONNECTING: 0,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connection was successful, and data is being received.
|
||||||
|
*
|
||||||
|
* @type {!number}
|
||||||
|
*/
|
||||||
|
OPEN: 1,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The connection is closed. Connection may not have been successful, the
|
||||||
|
* tunnel may have been explicitly closed by either side, or an error may
|
||||||
|
* have occurred.
|
||||||
|
*
|
||||||
|
* @type {!number}
|
||||||
|
*/
|
||||||
|
CLOSED: 2,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The connection is open, but communication through the tunnel appears to
|
||||||
|
* be disrupted, and the connection may close as a result.
|
||||||
|
*
|
||||||
|
* @type {!number}
|
||||||
|
*/
|
||||||
|
UNSTABLE: 3,
|
||||||
|
};
|
||||||
11
mayfly_go_web/src/components/terminal-rdp/index.ts
Normal file
11
mayfly_go_web/src/components/terminal-rdp/index.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export interface TerminalExpose {
|
||||||
|
/** 连接 */
|
||||||
|
init(width: number, height: number, force: boolean): void;
|
||||||
|
|
||||||
|
/** 短开连接 */
|
||||||
|
close(): void;
|
||||||
|
|
||||||
|
blur(): void;
|
||||||
|
|
||||||
|
focus(): void;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="terminal-body" :style="{ height, background: themeConfig.terminalBackground }">
|
<div id="terminal-body" :style="{ height }">
|
||||||
<div ref="terminalRef" class="terminal" />
|
<div ref="terminalRef" class="terminal" />
|
||||||
|
|
||||||
<TerminalSearch ref="terminalSearchRef" :search-addon="state.addon.search" @close="focus" />
|
<TerminalSearch ref="terminalSearchRef" :search-addon="state.addon.search" @close="focus" />
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import 'xterm/css/xterm.css';
|
import 'xterm/css/xterm.css';
|
||||||
import { Terminal } from 'xterm';
|
import { Terminal, ITheme } from 'xterm';
|
||||||
import { FitAddon } from 'xterm-addon-fit';
|
import { FitAddon } from 'xterm-addon-fit';
|
||||||
import { SearchAddon } from 'xterm-addon-search';
|
import { SearchAddon } from 'xterm-addon-search';
|
||||||
import { WebLinksAddon } from 'xterm-addon-web-links';
|
import { WebLinksAddon } from 'xterm-addon-web-links';
|
||||||
@@ -20,8 +20,15 @@ import TerminalSearch from './TerminalSearch.vue';
|
|||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import { TerminalStatus } from './common';
|
import { TerminalStatus } from './common';
|
||||||
import { useEventListener } from '@vueuse/core';
|
import { useEventListener } from '@vueuse/core';
|
||||||
|
import themes from './themes';
|
||||||
|
import { TrzszFilter } from 'trzsz';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
// mounted时,是否执行init方法
|
||||||
|
mountInit: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* 初始化执行命令
|
* 初始化执行命令
|
||||||
*/
|
*/
|
||||||
@@ -64,9 +71,9 @@ const state = reactive({
|
|||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
nextTick(() => {
|
if (props.mountInit) {
|
||||||
init();
|
init();
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -76,6 +83,14 @@ watch(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 监听 themeConfig terminalTheme配置的变化
|
||||||
|
watch(
|
||||||
|
() => themeConfig.value.terminalTheme,
|
||||||
|
() => {
|
||||||
|
term.options.theme = getTerminalTheme();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
close();
|
close();
|
||||||
});
|
});
|
||||||
@@ -85,6 +100,12 @@ function init() {
|
|||||||
console.log('重新连接...');
|
console.log('重新连接...');
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
|
nextTick(() => {
|
||||||
|
initTerm();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initTerm() {
|
||||||
term = new Terminal({
|
term = new Terminal({
|
||||||
fontSize: themeConfig.value.terminalFontSize || 15,
|
fontSize: themeConfig.value.terminalFontSize || 15,
|
||||||
fontWeight: themeConfig.value.terminalFontWeight || 'normal',
|
fontWeight: themeConfig.value.terminalFontWeight || 'normal',
|
||||||
@@ -92,13 +113,10 @@ function init() {
|
|||||||
cursorBlink: true,
|
cursorBlink: true,
|
||||||
disableStdin: false,
|
disableStdin: false,
|
||||||
allowProposedApi: true,
|
allowProposedApi: true,
|
||||||
theme: {
|
fastScrollModifier: 'ctrl',
|
||||||
foreground: themeConfig.value.terminalForeground || '#7e9192', //字体
|
theme: getTerminalTheme(),
|
||||||
background: themeConfig.value.terminalBackground || '#002833', //背景色
|
|
||||||
cursor: themeConfig.value.terminalCursor || '#268F81', //设置光标
|
|
||||||
// cursorAccent: "red", // 光标停止颜色
|
|
||||||
} as any,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
term.open(terminalRef.value);
|
term.open(terminalRef.value);
|
||||||
|
|
||||||
// 注册自适应组件
|
// 注册自适应组件
|
||||||
@@ -106,31 +124,12 @@ function init() {
|
|||||||
state.addon.fit = fitAddon;
|
state.addon.fit = fitAddon;
|
||||||
term.loadAddon(fitAddon);
|
term.loadAddon(fitAddon);
|
||||||
fitTerminal();
|
fitTerminal();
|
||||||
|
// 注册窗口大小监听器
|
||||||
|
useEventListener('resize', debounce(fitTerminal, 400));
|
||||||
|
|
||||||
// 注册搜索组件
|
|
||||||
const searchAddon = new SearchAddon();
|
|
||||||
state.addon.search = searchAddon;
|
|
||||||
term.loadAddon(searchAddon);
|
|
||||||
|
|
||||||
// 注册 url link组件
|
|
||||||
const weblinks = new WebLinksAddon();
|
|
||||||
state.addon.weblinks = weblinks;
|
|
||||||
term.loadAddon(weblinks);
|
|
||||||
|
|
||||||
// 初始化websocket
|
|
||||||
initSocket();
|
initSocket();
|
||||||
}
|
// 注册其他插件
|
||||||
|
loadAddon();
|
||||||
/**
|
|
||||||
* 连接成功
|
|
||||||
*/
|
|
||||||
const onConnected = () => {
|
|
||||||
// 注册心跳
|
|
||||||
pingInterval = setInterval(sendPing, 15000);
|
|
||||||
|
|
||||||
// 注册 terminal 事件
|
|
||||||
term.onResize((event) => sendResize(event.cols, event.rows));
|
|
||||||
term.onData((event) => sendCmd(event));
|
|
||||||
|
|
||||||
// 注册自定义快捷键
|
// 注册自定义快捷键
|
||||||
term.attachCustomKeyEventHandler((event: KeyboardEvent) => {
|
term.attachCustomKeyEventHandler((event: KeyboardEvent) => {
|
||||||
@@ -142,50 +141,25 @@ const onConnected = () => {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
state.status = TerminalStatus.Connected;
|
|
||||||
|
|
||||||
// 注册窗口大小监听器
|
|
||||||
useEventListener('resize', debounce(fitTerminal, 400));
|
|
||||||
|
|
||||||
focus();
|
|
||||||
|
|
||||||
// 如果有初始要执行的命令,则发送执行命令
|
|
||||||
if (props.cmd) {
|
|
||||||
sendCmd(props.cmd + ' \r');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 自适应终端
|
|
||||||
const fitTerminal = () => {
|
|
||||||
const dimensions = state.addon.fit && state.addon.fit.proposeDimensions();
|
|
||||||
if (!dimensions) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (dimensions?.cols && dimensions?.rows) {
|
|
||||||
term.resize(dimensions.cols, dimensions.rows);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const focus = () => {
|
|
||||||
setTimeout(() => term.focus(), 400);
|
|
||||||
};
|
|
||||||
|
|
||||||
const clear = () => {
|
|
||||||
term.clear();
|
|
||||||
term.clearSelection();
|
|
||||||
term.focus();
|
|
||||||
};
|
|
||||||
|
|
||||||
function initSocket() {
|
function initSocket() {
|
||||||
if (props.socketUrl) {
|
if (!props.socketUrl) {
|
||||||
let socketUrl = `${props.socketUrl}&rows=${term?.rows}&cols=${term?.cols}`;
|
return;
|
||||||
socket = new WebSocket(socketUrl);
|
|
||||||
}
|
}
|
||||||
|
socket = new WebSocket(`${props.socketUrl}&rows=${term?.rows}&cols=${term?.cols}`);
|
||||||
// 监听socket连接
|
// 监听socket连接
|
||||||
socket.onopen = () => {
|
socket.onopen = () => {
|
||||||
onConnected();
|
// 注册心跳
|
||||||
|
pingInterval = setInterval(sendPing, 15000);
|
||||||
|
state.status = TerminalStatus.Connected;
|
||||||
|
|
||||||
|
focus();
|
||||||
|
|
||||||
|
// 如果有初始要执行的命令,则发送执行命令
|
||||||
|
if (props.cmd) {
|
||||||
|
sendCmd(props.cmd + ' \r');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 监听socket错误信息
|
// 监听socket错误信息
|
||||||
@@ -197,20 +171,96 @@ function initSocket() {
|
|||||||
|
|
||||||
socket.onclose = (e: CloseEvent) => {
|
socket.onclose = (e: CloseEvent) => {
|
||||||
console.log('terminal socket close...', e.reason);
|
console.log('terminal socket close...', e.reason);
|
||||||
// 清除 ping
|
|
||||||
pingInterval && clearInterval(pingInterval);
|
|
||||||
state.status = TerminalStatus.Disconnected;
|
state.status = TerminalStatus.Disconnected;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 监听socket消息
|
|
||||||
socket.onmessage = getMessage;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMessage(msg: any) {
|
function loadAddon() {
|
||||||
// msg.data是真正后端返回的数据
|
// 注册搜索组件
|
||||||
term.write(msg.data);
|
const searchAddon = new SearchAddon();
|
||||||
|
state.addon.search = searchAddon;
|
||||||
|
term.loadAddon(searchAddon);
|
||||||
|
|
||||||
|
// 注册 url link组件
|
||||||
|
const weblinks = new WebLinksAddon();
|
||||||
|
state.addon.weblinks = weblinks;
|
||||||
|
term.loadAddon(weblinks);
|
||||||
|
|
||||||
|
// 注册 trzsz
|
||||||
|
// initialize trzsz filter
|
||||||
|
const trzsz = new TrzszFilter({
|
||||||
|
// write the server output to the terminal
|
||||||
|
writeToTerminal: (data: any) => term.write(typeof data === 'string' ? data : new Uint8Array(data)),
|
||||||
|
// send the user input to the server
|
||||||
|
sendToServer: sendCmd,
|
||||||
|
// the terminal columns
|
||||||
|
terminalColumns: term.cols,
|
||||||
|
// there is a windows shell
|
||||||
|
isWindowsShell: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// let trzsz process the server output
|
||||||
|
socket?.addEventListener('message', (e) => trzsz.processServerOutput(e.data));
|
||||||
|
// let trzsz process the user input
|
||||||
|
term.onData((data) => trzsz.processTerminalInput(data));
|
||||||
|
term.onBinary((data) => trzsz.processBinaryInput(data));
|
||||||
|
term.onResize((size) => {
|
||||||
|
sendResize(size.cols, size.rows);
|
||||||
|
// tell trzsz the terminal columns has been changed
|
||||||
|
trzsz.setTerminalColumns(size.cols);
|
||||||
|
});
|
||||||
|
window.addEventListener('resize', () => state.addon.fit.fit());
|
||||||
|
// enable drag files or directories to upload
|
||||||
|
terminalRef.value.addEventListener('dragover', (event: Event) => event.preventDefault());
|
||||||
|
terminalRef.value.addEventListener('drop', (event: any) => {
|
||||||
|
event.preventDefault();
|
||||||
|
trzsz
|
||||||
|
.uploadFiles(event.dataTransfer.items)
|
||||||
|
.then(() => console.log('upload success'))
|
||||||
|
.catch((err: any) => console.log(err));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 写入内容至终端
|
||||||
|
const write2Term = (data: any) => {
|
||||||
|
term.write(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const writeln2Term = (data: any) => {
|
||||||
|
term.writeln(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTerminalTheme = () => {
|
||||||
|
const terminalTheme = themeConfig.value.terminalTheme;
|
||||||
|
// 如果不是自定义主题,则返回内置主题
|
||||||
|
if (terminalTheme != 'custom') {
|
||||||
|
return themes[terminalTheme];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自定义主题
|
||||||
|
return {
|
||||||
|
foreground: themeConfig.value.terminalForeground || '#7e9192', //字体
|
||||||
|
background: themeConfig.value.terminalBackground || '#002833', //背景色
|
||||||
|
cursor: themeConfig.value.terminalCursor || '#268F81', //设置光标
|
||||||
|
// cursorAccent: "red", // 光标停止颜色
|
||||||
|
} as ITheme;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 自适应终端
|
||||||
|
const fitTerminal = () => {
|
||||||
|
state.addon.fit.fit();
|
||||||
|
};
|
||||||
|
|
||||||
|
const focus = () => {
|
||||||
|
setTimeout(() => term.focus(), 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clear = () => {
|
||||||
|
term.clear();
|
||||||
|
term.clearSelection();
|
||||||
|
term.focus();
|
||||||
|
};
|
||||||
|
|
||||||
enum MsgType {
|
enum MsgType {
|
||||||
Resize = 1,
|
Resize = 1,
|
||||||
Data = 2,
|
Data = 2,
|
||||||
@@ -218,29 +268,19 @@ enum MsgType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const send = (msg: any) => {
|
const send = (msg: any) => {
|
||||||
state.status == TerminalStatus.Connected && socket.send(JSON.stringify(msg));
|
state.status == TerminalStatus.Connected && socket?.send(msg);
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendResize = (cols: number, rows: number) => {
|
const sendResize = (cols: number, rows: number) => {
|
||||||
send({
|
send(`${MsgType.Resize}|${rows}|${cols}`);
|
||||||
type: MsgType.Resize,
|
|
||||||
Cols: cols,
|
|
||||||
Rows: rows,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendPing = () => {
|
const sendPing = () => {
|
||||||
send({
|
send(`${MsgType.Ping}|ping`);
|
||||||
type: MsgType.Ping,
|
|
||||||
msg: 'ping',
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function sendCmd(key: any) {
|
function sendCmd(key: any) {
|
||||||
send({
|
send(`${MsgType.Data}|${key}`);
|
||||||
type: MsgType.Data,
|
|
||||||
msg: key,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeSocket() {
|
function closeSocket() {
|
||||||
@@ -265,20 +305,19 @@ const getStatus = (): TerminalStatus => {
|
|||||||
return state.status;
|
return state.status;
|
||||||
};
|
};
|
||||||
|
|
||||||
defineExpose({ init, fitTerminal, focus, clear, close, getStatus });
|
defineExpose({ init, fitTerminal, focus, clear, close, getStatus, sendResize, write2Term, writeln2Term });
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
#terminal-body {
|
#terminal-body {
|
||||||
background: #212529;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
.terminal {
|
.terminal {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
.xterm .xterm-viewport {
|
// .xterm .xterm-viewport {
|
||||||
overflow-y: hidden;
|
// overflow-y: hidden;
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="terminal-dialog-container" v-for="openTerminal of terminals" :key="openTerminal.terminalId">
|
<div class="terminal-dialog-container" v-for="openTerminal of terminals" :key="openTerminal.terminalId">
|
||||||
<el-dialog
|
<el-dialog
|
||||||
title="终端"
|
title="SSH终端"
|
||||||
v-model="openTerminal.visible"
|
v-model="openTerminal.visible"
|
||||||
top="32px"
|
top="32px"
|
||||||
class="terminal-dialog"
|
class="terminal-dialog"
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="terminal-wrapper" :style="{ height: `calc(100vh - ${openTerminal.fullscreen ? '49px' : '200px'})` }">
|
<div :style="{ height: `calc(100vh - ${openTerminal.fullscreen ? '49px' : '200px'})` }">
|
||||||
<TerminalBody
|
<TerminalBody
|
||||||
@status-change="terminalStatusChange(openTerminal.terminalId, $event)"
|
@status-change="terminalStatusChange(openTerminal.terminalId, $event)"
|
||||||
:ref="(el) => setTerminalRef(el, openTerminal.terminalId)"
|
:ref="(el) => setTerminalRef(el, openTerminal.terminalId)"
|
||||||
@@ -92,7 +92,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { toRefs, reactive } from 'vue';
|
import { reactive, toRefs } from 'vue';
|
||||||
import TerminalBody from '@/components/terminal/TerminalBody.vue';
|
import TerminalBody from '@/components/terminal/TerminalBody.vue';
|
||||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||||
import { TerminalStatus } from './common';
|
import { TerminalStatus } from './common';
|
||||||
@@ -259,6 +259,10 @@ defineExpose({
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.el-dialog {
|
||||||
|
padding: 1px 1px;
|
||||||
|
}
|
||||||
|
|
||||||
// 取消body最大高度,否则全屏有问题
|
// 取消body最大高度,否则全屏有问题
|
||||||
.el-dialog__body {
|
.el-dialog__body {
|
||||||
max-height: 100% !important;
|
max-height: 100% !important;
|
||||||
|
|||||||
113
mayfly_go_web/src/components/terminal/TerminalLog.vue
Normal file
113
mayfly_go_web/src/components/terminal/TerminalLog.vue
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-drawer v-model="visible" :before-close="cancel" size="50%">
|
||||||
|
<template #header>
|
||||||
|
<DrawerHeader :header="props.title" :back="cancel">
|
||||||
|
<template #extra>
|
||||||
|
<EnumTag :enums="LogTypeEnum" :value="log?.type" class="mr20" />
|
||||||
|
</template>
|
||||||
|
</DrawerHeader>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-descriptions class="mb10" :column="1" border v-if="extra">
|
||||||
|
<el-descriptions-item v-for="(value, key) in extra" :key="key" :span="1" :label="key">{{ value }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
|
||||||
|
<TerminalBody class="mb10" ref="terminalRef" height="calc(100vh - 220px)" />
|
||||||
|
</el-drawer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||||
|
import TerminalBody from './TerminalBody.vue';
|
||||||
|
import { logApi } from '../../views/system/api';
|
||||||
|
import { LogTypeEnum } from '@/views/system/enums';
|
||||||
|
import { useIntervalFn } from '@vueuse/core';
|
||||||
|
import EnumTag from '@/components/enumtag/EnumTag.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: '日志',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const visible = defineModel<boolean>('visible', { default: false });
|
||||||
|
const logId = defineModel<number>('logId', { default: 0 });
|
||||||
|
|
||||||
|
const terminalRef: any = ref(null);
|
||||||
|
const nowLine = ref(0);
|
||||||
|
const log = ref({}) as any;
|
||||||
|
|
||||||
|
const extra = computed(() => {
|
||||||
|
if (log.value?.extra) {
|
||||||
|
return JSON.parse(log.value.extra);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 定时获取最新日志
|
||||||
|
const { pause, resume } = useIntervalFn(() => {
|
||||||
|
writeLog();
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => logId.value,
|
||||||
|
(logId: number) => {
|
||||||
|
terminalRef.value?.clear();
|
||||||
|
if (!logId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
writeLog();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
visible.value = false;
|
||||||
|
logId.value = 0;
|
||||||
|
nowLine.value = 0;
|
||||||
|
pause();
|
||||||
|
};
|
||||||
|
|
||||||
|
const writeLog = async () => {
|
||||||
|
const log = await getLog();
|
||||||
|
if (!log) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
writeLog2Term(log);
|
||||||
|
|
||||||
|
// 如果不是还在执行中的日志,则暂停轮询
|
||||||
|
if (log.type != LogTypeEnum.Running.value) {
|
||||||
|
pause();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resume();
|
||||||
|
};
|
||||||
|
|
||||||
|
const writeLog2Term = (log: any) => {
|
||||||
|
if (!log) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const lines = log.resp.split('\n');
|
||||||
|
for (let line of lines.slice(nowLine.value)) {
|
||||||
|
nowLine.value += 1;
|
||||||
|
terminalRef.value?.writeln2Term(line);
|
||||||
|
}
|
||||||
|
terminalRef.value?.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLog = async () => {
|
||||||
|
if (!logId.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const logRes = await logApi.detail.request({
|
||||||
|
id: logId.value,
|
||||||
|
});
|
||||||
|
log.value = logRes;
|
||||||
|
return logRes;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss"></style>
|
||||||
92
mayfly_go_web/src/components/terminal/themes.js
Normal file
92
mayfly_go_web/src/components/terminal/themes.js
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
export default {
|
||||||
|
dark: {
|
||||||
|
foreground: '#c7c7c7',
|
||||||
|
background: '#000000',
|
||||||
|
cursor: '#c7c7c7',
|
||||||
|
selectionBackground: '#686868',
|
||||||
|
|
||||||
|
black: '#000000',
|
||||||
|
brightBlack: '#676767',
|
||||||
|
|
||||||
|
red: '#c91b00',
|
||||||
|
brightRed: '#ff6d67',
|
||||||
|
|
||||||
|
green: '#00c200',
|
||||||
|
brightGreen: '#5ff967',
|
||||||
|
|
||||||
|
yellow: '#c7c400',
|
||||||
|
brightYellow: '#fefb67',
|
||||||
|
|
||||||
|
blue: '#0225c7',
|
||||||
|
brightBlue: '#6871ff',
|
||||||
|
|
||||||
|
magenta: '#c930c7',
|
||||||
|
brightMagenta: '#ff76ff',
|
||||||
|
|
||||||
|
cyan: '#00c5c7',
|
||||||
|
brightCyan: '#5ffdff',
|
||||||
|
|
||||||
|
white: '#c7c7c7',
|
||||||
|
brightWhite: '#fffefe',
|
||||||
|
},
|
||||||
|
light: {
|
||||||
|
foreground: '#000000',
|
||||||
|
background: '#fffefe',
|
||||||
|
cursor: '#000000',
|
||||||
|
selectionBackground: '#c7c7c7',
|
||||||
|
|
||||||
|
black: '#000000',
|
||||||
|
brightBlack: '#676767',
|
||||||
|
|
||||||
|
red: '#c91b00',
|
||||||
|
brightRed: '#ff6d67',
|
||||||
|
|
||||||
|
green: '#00c200',
|
||||||
|
brightGreen: '#5ff967',
|
||||||
|
|
||||||
|
yellow: '#c7c400',
|
||||||
|
brightYellow: '#fefb67',
|
||||||
|
|
||||||
|
blue: '#0225c7',
|
||||||
|
brightBlue: '#6871ff',
|
||||||
|
|
||||||
|
magenta: '#c930c7',
|
||||||
|
brightMagenta: '#ff76ff',
|
||||||
|
|
||||||
|
cyan: '#00c5c7',
|
||||||
|
brightCyan: '#5ffdff',
|
||||||
|
|
||||||
|
white: '#c7c7c7',
|
||||||
|
brightWhite: '#fffefe',
|
||||||
|
},
|
||||||
|
solarizedLight: {
|
||||||
|
foreground: '#657b83',
|
||||||
|
background: '#fdf6e3',
|
||||||
|
cursor: '#657b83',
|
||||||
|
selectionBackground: '#c7c7c7',
|
||||||
|
|
||||||
|
black: '#073642',
|
||||||
|
brightBlack: '#002b36',
|
||||||
|
|
||||||
|
red: '#dc322f',
|
||||||
|
brightRed: '#cb4b16',
|
||||||
|
|
||||||
|
green: '#859900',
|
||||||
|
brightGreen: '#586e75',
|
||||||
|
|
||||||
|
yellow: '#b58900',
|
||||||
|
brightYellow: '#657b83',
|
||||||
|
|
||||||
|
blue: '#268bd2',
|
||||||
|
brightBlue: '#839496',
|
||||||
|
|
||||||
|
magenta: '#d33682',
|
||||||
|
brightMagenta: '#6c71c4',
|
||||||
|
|
||||||
|
cyan: '#2aa198',
|
||||||
|
brightCyan: '#93a1a1',
|
||||||
|
|
||||||
|
white: '#eee8d5',
|
||||||
|
brightWhite: '#fdf6e3',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import Api from '@/common/Api';
|
import Api from '@/common/Api';
|
||||||
import { isReactive, reactive, toRefs, toValue } from 'vue';
|
import { reactive, toRefs, toValue } from 'vue';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description table 页面操作方法封装
|
* @description table 页面操作方法封装
|
||||||
@@ -41,12 +41,6 @@ export const usePageTable = (
|
|||||||
let sp = toValue(state.searchParams);
|
let sp = toValue(state.searchParams);
|
||||||
if (beforeQueryFn) {
|
if (beforeQueryFn) {
|
||||||
sp = beforeQueryFn(sp);
|
sp = beforeQueryFn(sp);
|
||||||
|
|
||||||
if (isReactive(state.searchParams)) {
|
|
||||||
state.searchParams.value = sp;
|
|
||||||
} else {
|
|
||||||
state.searchParams = sp;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let res = await api.request(sp);
|
let res = await api.request(sp);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const useCustomFetch = createFetch({
|
|||||||
combination: 'chain',
|
combination: 'chain',
|
||||||
options: {
|
options: {
|
||||||
immediate: false,
|
immediate: false,
|
||||||
timeout: 60000,
|
timeout: 600000,
|
||||||
// beforeFetch in pre-configured instance will only run when the newly spawned instance do not pass beforeFetch
|
// beforeFetch in pre-configured instance will only run when the newly spawned instance do not pass beforeFetch
|
||||||
async beforeFetch({ options }) {
|
async beforeFetch({ options }) {
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
@@ -48,9 +48,6 @@ export function useApiFetch<T>(api: Api, params: any = null, reqOptions: Request
|
|||||||
}
|
}
|
||||||
|
|
||||||
let paramsValue = unref(params);
|
let paramsValue = unref(params);
|
||||||
if (api.beforeHandler) {
|
|
||||||
paramsValue = api.beforeHandler(paramsValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
let apiUrl = url;
|
let apiUrl = url;
|
||||||
// 简单判断该url是否是restful风格
|
// 简单判断该url是否是restful风格
|
||||||
@@ -58,6 +55,10 @@ export function useApiFetch<T>(api: Api, params: any = null, reqOptions: Request
|
|||||||
apiUrl = templateResolve(apiUrl, paramsValue);
|
apiUrl = templateResolve(apiUrl, paramsValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (api.beforeHandler) {
|
||||||
|
paramsValue = api.beforeHandler(paramsValue);
|
||||||
|
}
|
||||||
|
|
||||||
if (paramsValue) {
|
if (paramsValue) {
|
||||||
const method = options.method?.toLowerCase();
|
const method = options.method?.toLowerCase();
|
||||||
// post和put使用json格式传参
|
// post和put使用json格式传参
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="layout-navbars-breadcrumb" v-show="themeConfig.isBreadcrumb">
|
<div class="layout-navbars-breadcrumb" v-show="themeConfig.isBreadcrumb">
|
||||||
<SvgIcon class="layout-navbars-breadcrumb-icon" :name="themeConfig.isCollapse ? 'expand' : 'fold'"
|
<SvgIcon class="layout-navbars-breadcrumb-icon" :name="themeConfig.isCollapse ? 'expand' : 'fold'" @click="onThemeConfigChange" />
|
||||||
@click="onThemeConfigChange" />
|
|
||||||
<el-breadcrumb class="layout-navbars-breadcrumb-hide">
|
<el-breadcrumb class="layout-navbars-breadcrumb-hide">
|
||||||
<transition-group name="breadcrumb" mode="out-in">
|
<transition-group name="breadcrumb" mode="out-in">
|
||||||
<el-breadcrumb-item v-for="(v, k) in state.breadcrumbList" :key="v.meta.title">
|
<el-breadcrumb-item v-for="(v, k) in state.breadcrumbList" :key="v.meta.title">
|
||||||
<span v-if="k === state.breadcrumbList.length - 1" class="layout-navbars-breadcrumb-span">
|
<span v-if="k === state.breadcrumbList.length - 1" class="layout-navbars-breadcrumb-span">
|
||||||
<SvgIcon :name="v.meta.icon" class="layout-navbars-breadcrumb-iconfont"
|
<SvgIcon :name="v.meta.icon" class="layout-navbars-breadcrumb-iconfont" v-if="themeConfig.isBreadcrumbIcon" />
|
||||||
v-if="themeConfig.isBreadcrumbIcon" />
|
|
||||||
{{ v.meta.title }}
|
{{ v.meta.title }}
|
||||||
</span>
|
</span>
|
||||||
<a v-else @click.prevent="onBreadcrumbClick(v)">
|
<a v-else @click.prevent="onBreadcrumbClick(v)">
|
||||||
<SvgIcon :name="v.meta.icon" class="layout-navbars-breadcrumb-iconfont"
|
<SvgIcon :name="v.meta.icon" class="layout-navbars-breadcrumb-iconfont" v-if="themeConfig.isBreadcrumbIcon" />
|
||||||
v-if="themeConfig.isBreadcrumbIcon" />
|
|
||||||
{{ v.meta.title }}
|
{{ v.meta.title }}
|
||||||
</a>
|
</a>
|
||||||
</el-breadcrumb-item>
|
</el-breadcrumb-item>
|
||||||
|
|||||||
@@ -1,17 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="layout-search-dialog">
|
<div class="layout-search-dialog">
|
||||||
<el-dialog v-model="state.isShowSearch" width="300px" destroy-on-close :modal="false" fullscreen :show-close="false">
|
<el-dialog v-model="state.isShowSearch" width="300px" destroy-on-close :modal="false" fullscreen :show-close="false">
|
||||||
<el-autocomplete v-model="state.menuQuery" :fetch-suggestions="menuSearch" placeholder="菜单搜索"
|
<el-autocomplete
|
||||||
prefix-icon="el-icon-search" ref="layoutMenuAutocompleteRef" @select="onHandleSelect" @blur="onSearchBlur">
|
v-model="state.menuQuery"
|
||||||
|
:fetch-suggestions="menuSearch"
|
||||||
|
placeholder="菜单搜索"
|
||||||
|
prefix-icon="el-icon-search"
|
||||||
|
ref="layoutMenuAutocompleteRef"
|
||||||
|
@select="onHandleSelect"
|
||||||
|
@blur="onSearchBlur"
|
||||||
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<el-icon class="el-input__icon">
|
<el-icon class="el-input__icon">
|
||||||
<search />
|
<search />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
</template>
|
</template>
|
||||||
<template #default="{ item }">
|
<template #default="{ item }">
|
||||||
<div>
|
<div><SvgIcon :name="item.meta.icon" class="mr5" />{{ item.meta.title }}</div>
|
||||||
<SvgIcon :name="item.meta.icon" class="mr5" />{{ item.meta.title }}
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</el-autocomplete>
|
</el-autocomplete>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
@@ -23,7 +28,7 @@ import { reactive, ref, nextTick } from 'vue';
|
|||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { useRoutesList } from '@/store/routesList';
|
import { useRoutesList } from '@/store/routesList';
|
||||||
|
|
||||||
const layoutMenuAutocompleteRef: any = ref(null);;
|
const layoutMenuAutocompleteRef: any = ref(null);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const state: any = reactive({
|
const state: any = reactive({
|
||||||
isShowSearch: false,
|
isShowSearch: false,
|
||||||
@@ -54,8 +59,7 @@ const menuSearch = (queryString: any, cb: any) => {
|
|||||||
const createFilter = (queryString: any) => {
|
const createFilter = (queryString: any) => {
|
||||||
return (restaurant: any) => {
|
return (restaurant: any) => {
|
||||||
return (
|
return (
|
||||||
restaurant.path.toLowerCase().indexOf(queryString.toLowerCase()) > -1 ||
|
restaurant.path.toLowerCase().indexOf(queryString.toLowerCase()) > -1 || restaurant.meta.title.toLowerCase().indexOf(queryString.toLowerCase()) > -1
|
||||||
restaurant.meta.title.toLowerCase().indexOf(queryString.toLowerCase()) > -1
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -97,7 +101,7 @@ const onSearchBlur = () => {
|
|||||||
closeSearch();
|
closeSearch();
|
||||||
};
|
};
|
||||||
|
|
||||||
defineExpose({openSearch})
|
defineExpose({ openSearch });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
@@ -5,26 +5,39 @@
|
|||||||
<!-- ssh终端主题 -->
|
<!-- ssh终端主题 -->
|
||||||
<el-divider content-position="left">终端主题</el-divider>
|
<el-divider content-position="left">终端主题</el-divider>
|
||||||
<div class="layout-breadcrumb-seting-bar-flex">
|
<div class="layout-breadcrumb-seting-bar-flex">
|
||||||
<div class="layout-breadcrumb-seting-bar-flex-label">字体颜色</div>
|
<div class="layout-breadcrumb-seting-bar-flex-label">主题</div>
|
||||||
<div class="layout-breadcrumb-seting-bar-flex-value">
|
<div class="layout-breadcrumb-seting-bar-flex-value">
|
||||||
<el-color-picker v-model="themeConfig.terminalForeground" size="small" @change="onColorPickerChange('terminalForeground')">
|
<el-select @change="setLocalThemeConfig" v-model="themeConfig.terminalTheme" size="small" style="width: 140px">
|
||||||
</el-color-picker>
|
<el-option v-for="(_, k) in themes" :key="k" :label="k" :value="k"> </el-option>
|
||||||
|
<el-option label="自定义" value="custom"> </el-option>
|
||||||
|
</el-select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="layout-breadcrumb-seting-bar-flex">
|
<template v-if="themeConfig.terminalTheme == 'custom'">
|
||||||
<div class="layout-breadcrumb-seting-bar-flex-label">背景颜色</div>
|
<div class="layout-breadcrumb-seting-bar-flex mt10">
|
||||||
<div class="layout-breadcrumb-seting-bar-flex-value">
|
<div class="layout-breadcrumb-seting-bar-flex-label">字体颜色</div>
|
||||||
<el-color-picker v-model="themeConfig.terminalBackground" size="small" @change="onColorPickerChange('terminalBackground')">
|
<div class="layout-breadcrumb-seting-bar-flex-value">
|
||||||
</el-color-picker>
|
<el-color-picker v-model="themeConfig.terminalForeground" size="small" @change="onColorPickerChange('terminalForeground')">
|
||||||
|
</el-color-picker>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="layout-breadcrumb-seting-bar-flex">
|
||||||
<div class="layout-breadcrumb-seting-bar-flex">
|
<div class="layout-breadcrumb-seting-bar-flex-label">背景颜色</div>
|
||||||
<div class="layout-breadcrumb-seting-bar-flex-label">cursor颜色</div>
|
<div class="layout-breadcrumb-seting-bar-flex-value">
|
||||||
<div class="layout-breadcrumb-seting-bar-flex-value">
|
<el-color-picker v-model="themeConfig.terminalBackground" size="small" @change="onColorPickerChange('terminalBackground')">
|
||||||
<el-color-picker v-model="themeConfig.terminalCursor" size="small" @change="onColorPickerChange('terminalCursor')"> </el-color-picker>
|
</el-color-picker>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="layout-breadcrumb-seting-bar-flex">
|
||||||
<div class="layout-breadcrumb-seting-bar-flex mt15">
|
<div class="layout-breadcrumb-seting-bar-flex-label">cursor颜色</div>
|
||||||
|
<div class="layout-breadcrumb-seting-bar-flex-value">
|
||||||
|
<el-color-picker v-model="themeConfig.terminalCursor" size="small" @change="onColorPickerChange('terminalCursor')">
|
||||||
|
</el-color-picker>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="layout-breadcrumb-seting-bar-flex mt10">
|
||||||
<div class="layout-breadcrumb-seting-bar-flex-label">字体大小</div>
|
<div class="layout-breadcrumb-seting-bar-flex-label">字体大小</div>
|
||||||
<div class="layout-breadcrumb-seting-bar-flex-value">
|
<div class="layout-breadcrumb-seting-bar-flex-value">
|
||||||
<el-input-number
|
<el-input-number
|
||||||
@@ -39,7 +52,7 @@
|
|||||||
</el-input-number>
|
</el-input-number>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="layout-breadcrumb-seting-bar-flex mt15">
|
<div class="layout-breadcrumb-seting-bar-flex mt10">
|
||||||
<div class="layout-breadcrumb-seting-bar-flex-label">字体粗细</div>
|
<div class="layout-breadcrumb-seting-bar-flex-label">字体粗细</div>
|
||||||
<div class="layout-breadcrumb-seting-bar-flex-value">
|
<div class="layout-breadcrumb-seting-bar-flex-value">
|
||||||
<el-select @change="setLocalThemeConfig" v-model="themeConfig.terminalFontWeight" size="small" style="width: 90px">
|
<el-select @change="setLocalThemeConfig" v-model="themeConfig.terminalFontWeight" size="small" style="width: 90px">
|
||||||
@@ -418,6 +431,7 @@ import { useThemeConfig } from '@/store/themeConfig';
|
|||||||
import { getLightColor } from '@/common/utils/theme';
|
import { getLightColor } from '@/common/utils/theme';
|
||||||
import { setLocal, getLocal, removeLocal } from '@/common/utils/storage';
|
import { setLocal, getLocal, removeLocal } from '@/common/utils/storage';
|
||||||
import mittBus from '@/common/utils/mitt';
|
import mittBus from '@/common/utils/mitt';
|
||||||
|
import themes from '@/components/terminal/themes';
|
||||||
|
|
||||||
const copyConfigBtnRef = ref();
|
const copyConfigBtnRef = ref();
|
||||||
const { themeConfig } = storeToRefs(useThemeConfig());
|
const { themeConfig } = storeToRefs(useThemeConfig());
|
||||||
@@ -615,6 +629,9 @@ const setLocalThemeConfigStyle = () => {
|
|||||||
};
|
};
|
||||||
// 一键复制配置
|
// 一键复制配置
|
||||||
const onCopyConfigClick = (target: any) => {
|
const onCopyConfigClick = (target: any) => {
|
||||||
|
if (!target) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let copyThemeConfig = getLocal('themeConfig');
|
let copyThemeConfig = getLocal('themeConfig');
|
||||||
copyThemeConfig.isDrawer = false;
|
copyThemeConfig.isDrawer = false;
|
||||||
const clipboard = new ClipboardJS(target, {
|
const clipboard = new ClipboardJS(target, {
|
||||||
@@ -690,6 +707,25 @@ defineExpose({ openDrawer });
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
::v-deep(.el-drawer) {
|
||||||
|
--el-drawer-padding-primary: unset !important;
|
||||||
|
|
||||||
|
.el-drawer__header {
|
||||||
|
padding: 0 15px !important;
|
||||||
|
height: 50px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
border-bottom: 1px solid var(--el-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-drawer__body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.layout-breadcrumb-seting-bar {
|
.layout-breadcrumb-seting-bar {
|
||||||
height: calc(100vh - 50px);
|
height: calc(100vh - 50px);
|
||||||
padding: 0 15px;
|
padding: 0 15px;
|
||||||
|
|||||||
20
mayfly_go_web/src/layout/navBars/breadcrumb/switchDark.ts
Normal file
20
mayfly_go_web/src/layout/navBars/breadcrumb/switchDark.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { saveThemeConfig } from '@/common/utils/storage';
|
||||||
|
import { isDark } from './user.vue';
|
||||||
|
|
||||||
|
export const switchDark = () => {
|
||||||
|
themeConfig.value.isDark = isDark.value;
|
||||||
|
if (isDark.value) {
|
||||||
|
themeConfig.value.editorTheme = 'vs-dark';
|
||||||
|
} else {
|
||||||
|
themeConfig.value.editorTheme = 'vs';
|
||||||
|
}
|
||||||
|
// 如果终端主题不是自定义主题,则切换主题
|
||||||
|
if (themeConfig.value.terminalTheme != 'custom') {
|
||||||
|
if (isDark.value) {
|
||||||
|
themeConfig.value.terminalTheme = 'dark';
|
||||||
|
} else {
|
||||||
|
themeConfig.value.terminalTheme = 'solarizedLight';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
saveThemeConfig(themeConfig.value);
|
||||||
|
};
|
||||||
@@ -174,12 +174,7 @@ watch(preDark, (newValue) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const switchDark = () => {
|
const switchDark = () => {
|
||||||
themeConfig.value.isDark = isDark.value;
|
themeConfigStore.switchDark(isDark.value);
|
||||||
if (isDark.value) {
|
|
||||||
themeConfig.value.editorTheme = 'vs-dark';
|
|
||||||
} else {
|
|
||||||
themeConfig.value.editorTheme = 'vs';
|
|
||||||
}
|
|
||||||
saveThemeConfig(themeConfig.value);
|
saveThemeConfig(themeConfig.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -284,7 +284,9 @@ const onTagsClick = (v: any, k: number) => {
|
|||||||
state.tagsRefsIndex = k;
|
state.tagsRefsIndex = k;
|
||||||
try {
|
try {
|
||||||
router.push(v);
|
router.push(v);
|
||||||
} catch (e) {}
|
} catch (e) {
|
||||||
|
// skip
|
||||||
|
}
|
||||||
};
|
};
|
||||||
// 更新滚动条显示
|
// 更新滚动条显示
|
||||||
const updateScrollbar = () => {
|
const updateScrollbar = () => {
|
||||||
|
|||||||
@@ -89,13 +89,21 @@ type RouterConvCallbackFunc = (router: any) => void;
|
|||||||
* @param meta.link ==> 外链地址
|
* @param meta.link ==> 外链地址
|
||||||
* */
|
* */
|
||||||
export function backEndRouterConverter(routes: any, callbackFunc: RouterConvCallbackFunc = null as any, parentPath: string = '/') {
|
export function backEndRouterConverter(routes: any, callbackFunc: RouterConvCallbackFunc = null as any, parentPath: string = '/') {
|
||||||
if (!routes) return [];
|
if (!routes) {
|
||||||
return routes.map((item: any) => {
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const routeItems = [];
|
||||||
|
for (let item of routes) {
|
||||||
if (!item.meta) {
|
if (!item.meta) {
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
// 将json字符串的meta转为对象
|
// 将json字符串的meta转为对象
|
||||||
item.meta = JSON.parse(item.meta);
|
item.meta = JSON.parse(item.meta);
|
||||||
|
if (item.meta.isHide) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// 将meta.comoponet 解析为route.component
|
// 将meta.comoponet 解析为route.component
|
||||||
if (item.meta.component) {
|
if (item.meta.component) {
|
||||||
item.component = dynamicImport(dynamicViewsModules, item.meta.component);
|
item.component = dynamicImport(dynamicViewsModules, item.meta.component);
|
||||||
@@ -126,8 +134,10 @@ export function backEndRouterConverter(routes: any, callbackFunc: RouterConvCall
|
|||||||
// 存在回调,则执行回调
|
// 存在回调,则执行回调
|
||||||
callbackFunc && callbackFunc(item);
|
callbackFunc && callbackFunc(item);
|
||||||
item.children && backEndRouterConverter(item.children, callbackFunc, item.path);
|
item.children && backEndRouterConverter(item.children, callbackFunc, item.path);
|
||||||
return item;
|
routeItems.push(item);
|
||||||
});
|
}
|
||||||
|
|
||||||
|
return routeItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ router.beforeEach(async (to, from, next) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 终端不需要连接系统websocket消息
|
// 终端不需要连接系统websocket消息
|
||||||
if (to.path != '/machine/terminal') {
|
if (to.path != '/machine/terminal' && to.path != '/machine/terminal-rdp') {
|
||||||
syssocket.init();
|
syssocket.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,17 @@ export const staticRoutes: Array<RouteRecordRaw> = [
|
|||||||
titleRename: true,
|
titleRename: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/machine/terminal-rdp',
|
||||||
|
name: 'machineTerminalRdp',
|
||||||
|
component: () => import('@/views/ops/machine/RdpTerminalPage.vue'),
|
||||||
|
meta: {
|
||||||
|
// 将路径 'xxx?name=名字' 里的name字段值替换到title里
|
||||||
|
title: '终端 | {name}',
|
||||||
|
// 是否根据query对标题名进行参数替换,即最终显示为‘终端_机器名’
|
||||||
|
titleRename: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// 错误页面路由
|
// 错误页面路由
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ export const useThemeConfig = defineStore('themeConfig', {
|
|||||||
// 默认布局,可选 1、默认 defaults 2、经典 classic 3、横向 transverse 4、分栏 columns
|
// 默认布局,可选 1、默认 defaults 2、经典 classic 3、横向 transverse 4、分栏 columns
|
||||||
layout: 'classic',
|
layout: 'classic',
|
||||||
|
|
||||||
|
terminalTheme: 'solarizedLight',
|
||||||
// ssh终端字体颜色
|
// ssh终端字体颜色
|
||||||
terminalForeground: '#C5C8C6',
|
terminalForeground: '#C5C8C6',
|
||||||
// ssh终端背景色
|
// ssh终端背景色
|
||||||
@@ -192,5 +193,23 @@ export const useThemeConfig = defineStore('themeConfig', {
|
|||||||
setWatermarkNowTime() {
|
setWatermarkNowTime() {
|
||||||
this.themeConfig.watermarkText[1] = dateFormat2('yyyy-MM-dd HH:mm:ss', new Date());
|
this.themeConfig.watermarkText[1] = dateFormat2('yyyy-MM-dd HH:mm:ss', new Date());
|
||||||
},
|
},
|
||||||
|
// 切换暗黑模式
|
||||||
|
switchDark(isDark: boolean) {
|
||||||
|
this.themeConfig.isDark = isDark;
|
||||||
|
// 切换编辑器主题
|
||||||
|
if (isDark) {
|
||||||
|
this.themeConfig.editorTheme = 'vs-dark';
|
||||||
|
} else {
|
||||||
|
this.themeConfig.editorTheme = 'vs';
|
||||||
|
}
|
||||||
|
// 如果终端主题不是自定义主题,则切换主题
|
||||||
|
if (this.themeConfig.terminalTheme != 'custom') {
|
||||||
|
if (isDark) {
|
||||||
|
this.themeConfig.terminalTheme = 'dark';
|
||||||
|
} else {
|
||||||
|
this.themeConfig.terminalTheme = 'solarizedLight';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ body,
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
|
||||||
font-weight: 450;
|
font-weight: 500;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
background-color: var(--bg-main-color);
|
background-color: var(--bg-main-color);
|
||||||
|
|||||||
@@ -7,335 +7,353 @@
|
|||||||
------------------------------- */
|
------------------------------- */
|
||||||
// 菜单搜索
|
// 菜单搜索
|
||||||
.el-autocomplete-suggestion__wrap {
|
.el-autocomplete-suggestion__wrap {
|
||||||
max-height: 280px !important;
|
max-height: 280px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Form 表单
|
/* Form 表单
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
.el-form {
|
.el-form {
|
||||||
// 用于修改弹窗时表单内容间隔太大问题,如系统设置的新增菜单弹窗里的表单内容
|
|
||||||
.el-form-item:last-of-type {
|
// 用于修改弹窗时表单内容间隔太大问题,如系统设置的新增菜单弹窗里的表单内容
|
||||||
margin-bottom: 0 !important;
|
.el-form-item:last-of-type {
|
||||||
}
|
margin-bottom: 0 !important;
|
||||||
// 修复行内表单最后一个 el-form-item 位置下移问题
|
}
|
||||||
&.el-form--inline {
|
|
||||||
.el-form-item--large.el-form-item:last-of-type {
|
// 修复行内表单最后一个 el-form-item 位置下移问题
|
||||||
margin-bottom: 22px !important;
|
&.el-form--inline {
|
||||||
}
|
.el-form-item--large.el-form-item:last-of-type {
|
||||||
.el-form-item--default.el-form-item:last-of-type,
|
margin-bottom: 22px !important;
|
||||||
.el-form-item--small.el-form-item:last-of-type {
|
}
|
||||||
margin-bottom: 18px !important;
|
|
||||||
}
|
.el-form-item--default.el-form-item:last-of-type,
|
||||||
}
|
.el-form-item--small.el-form-item:last-of-type {
|
||||||
// https://gitee.com/lyt-top/vue-next-admin/issues/I5K1PM
|
margin-bottom: 18px !important;
|
||||||
.el-form-item .el-form-item__label .el-icon {
|
}
|
||||||
margin-right: 0px;
|
}
|
||||||
}
|
|
||||||
|
// https://gitee.com/lyt-top/vue-next-admin/issues/I5K1PM
|
||||||
|
.el-form-item .el-form-item__label .el-icon {
|
||||||
|
margin-right: 0px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Alert 警告
|
/* Alert 警告
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
.el-alert {
|
.el-alert {
|
||||||
border: 1px solid;
|
border: 1px solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-alert__title {
|
.el-alert__title {
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Message 消息提示
|
/* Message 消息提示
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
.el-message {
|
.el-message {
|
||||||
min-width: unset !important;
|
min-width: unset !important;
|
||||||
padding: 15px !important;
|
padding: 15px !important;
|
||||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.02);
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.02);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* NavMenu 导航菜单
|
/* NavMenu 导航菜单
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
// 鼠标 hover 时颜色
|
// 鼠标 hover 时颜色
|
||||||
.el-menu-hover-bg-color {
|
.el-menu-hover-bg-color {
|
||||||
background-color: var(--bg-menuBarActiveColor) !important;
|
background-color: var(--bg-menuBarActiveColor) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 默认样式修改
|
// 默认样式修改
|
||||||
.el-menu {
|
.el-menu {
|
||||||
border-right: none !important;
|
border-right: none !important;
|
||||||
width: 220px;
|
width: 220px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-menu-item {
|
.el-menu-item {
|
||||||
height: 56px !important;
|
height: 56px !important;
|
||||||
line-height: 56px !important;
|
line-height: 56px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-menu-item,
|
.el-menu-item,
|
||||||
.el-sub-menu__title {
|
.el-sub-menu__title {
|
||||||
color: var(--bg-menuBarColor);
|
color: var(--bg-menuBarColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 修复点击左侧菜单折叠再展开时,宽度不跟随问题
|
// 修复点击左侧菜单折叠再展开时,宽度不跟随问题
|
||||||
.el-menu--collapse {
|
.el-menu--collapse {
|
||||||
width: 64px !important;
|
width: 64px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 外部链接时
|
// 外部链接时
|
||||||
.el-menu-item a,
|
.el-menu-item a,
|
||||||
.el-menu-item a:hover,
|
.el-menu-item a:hover,
|
||||||
.el-menu-item i,
|
.el-menu-item i,
|
||||||
.el-sub-menu__title i {
|
.el-sub-menu__title i {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 第三方图标字体间距/大小设置
|
// 第三方图标字体间距/大小设置
|
||||||
.el-menu-item .iconfont,
|
.el-menu-item .iconfont,
|
||||||
.el-sub-menu .iconfont,
|
.el-sub-menu .iconfont,
|
||||||
.el-menu-item .fa,
|
.el-menu-item .fa,
|
||||||
.el-sub-menu .fa {
|
.el-sub-menu .fa {
|
||||||
@include generalIcon;
|
@include generalIcon;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 水平菜单、横向菜单高亮 背景色,鼠标 hover 时,有子级菜单的背景色
|
// 水平菜单、横向菜单高亮 背景色,鼠标 hover 时,有子级菜单的背景色
|
||||||
.el-menu-item.is-active,
|
.el-menu-item.is-active,
|
||||||
.el-sub-menu.is-active .el-sub-menu__title,
|
.el-sub-menu.is-active .el-sub-menu__title,
|
||||||
.el-sub-menu:not(.is-opened):hover .el-sub-menu__title {
|
.el-sub-menu:not(.is-opened):hover .el-sub-menu__title {
|
||||||
@extend .el-menu-hover-bg-color;
|
@extend .el-menu-hover-bg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-menu-item:hover {
|
.el-menu-item:hover {
|
||||||
@extend .el-menu-hover-bg-color;
|
@extend .el-menu-hover-bg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-sub-menu.is-active.is-opened .el-sub-menu__title {
|
.el-sub-menu.is-active.is-opened .el-sub-menu__title {
|
||||||
background-color: unset !important;
|
background-color: unset !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 子级菜单背景颜色
|
// 子级菜单背景颜色
|
||||||
// .el-menu--inline {
|
// .el-menu--inline {
|
||||||
// background: var(--next-bg-menuBar-light-1);
|
// background: var(--next-bg-menuBar-light-1);
|
||||||
// }
|
// }
|
||||||
// 水平菜单、横向菜单折叠 a 标签
|
// 水平菜单、横向菜单折叠 a 标签
|
||||||
.el-popper.is-dark a {
|
.el-popper.is-dark a {
|
||||||
color: var(--el-color-white) !important;
|
color: var(--el-color-white) !important;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 水平菜单、横向菜单折叠背景色
|
// 水平菜单、横向菜单折叠背景色
|
||||||
.el-popper.is-pure.is-light {
|
.el-popper.is-pure.is-light {
|
||||||
// 水平菜单
|
|
||||||
.el-menu--vertical {
|
// 水平菜单
|
||||||
background: var(--bg-menuBar);
|
.el-menu--vertical {
|
||||||
.el-sub-menu.is-active .el-sub-menu__title {
|
background: var(--bg-menuBar);
|
||||||
color: var(--el-menu-active-color);
|
|
||||||
}
|
.el-sub-menu.is-active .el-sub-menu__title {
|
||||||
.el-popper.is-pure.is-light {
|
color: var(--el-menu-active-color);
|
||||||
.el-menu--vertical {
|
}
|
||||||
.el-sub-menu .el-sub-menu__title {
|
|
||||||
background-color: unset !important;
|
.el-popper.is-pure.is-light {
|
||||||
color: var(--bg-menuBarColor);
|
.el-menu--vertical {
|
||||||
}
|
.el-sub-menu .el-sub-menu__title {
|
||||||
.el-sub-menu.is-active .el-sub-menu__title {
|
background-color: unset !important;
|
||||||
color: var(--el-menu-active-color);
|
color: var(--bg-menuBarColor);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
.el-sub-menu.is-active .el-sub-menu__title {
|
||||||
}
|
color: var(--el-menu-active-color);
|
||||||
// 横向菜单
|
}
|
||||||
.el-menu--horizontal {
|
}
|
||||||
background: var(--bg-topBar);
|
}
|
||||||
.el-menu-item,
|
}
|
||||||
.el-sub-menu {
|
|
||||||
height: 48px !important;
|
// 横向菜单
|
||||||
line-height: 48px !important;
|
.el-menu--horizontal {
|
||||||
color: var(--bg-topBarColor);
|
background: var(--bg-topBar);
|
||||||
.el-sub-menu__title {
|
|
||||||
height: 48px !important;
|
.el-menu-item,
|
||||||
line-height: 48px !important;
|
.el-sub-menu {
|
||||||
color: var(--bg-topBarColor);
|
height: 48px !important;
|
||||||
}
|
line-height: 48px !important;
|
||||||
.el-popper.is-pure.is-light {
|
color: var(--bg-topBarColor);
|
||||||
.el-menu--horizontal {
|
|
||||||
.el-sub-menu .el-sub-menu__title {
|
.el-sub-menu__title {
|
||||||
background-color: unset !important;
|
height: 48px !important;
|
||||||
color: var(--bg-topBarColor);
|
line-height: 48px !important;
|
||||||
}
|
color: var(--bg-topBarColor);
|
||||||
.el-sub-menu.is-active .el-sub-menu__title {
|
}
|
||||||
color: var(--el-menu-active-color);
|
|
||||||
}
|
.el-popper.is-pure.is-light {
|
||||||
}
|
.el-menu--horizontal {
|
||||||
}
|
.el-sub-menu .el-sub-menu__title {
|
||||||
}
|
background-color: unset !important;
|
||||||
.el-menu-item.is-active,
|
color: var(--bg-topBarColor);
|
||||||
.el-sub-menu.is-active .el-sub-menu__title {
|
}
|
||||||
color: var(--el-menu-active-color);
|
|
||||||
}
|
.el-sub-menu.is-active .el-sub-menu__title {
|
||||||
}
|
color: var(--el-menu-active-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-menu-item.is-active,
|
||||||
|
.el-sub-menu.is-active .el-sub-menu__title {
|
||||||
|
color: var(--el-menu-active-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 横向菜单(经典、横向)布局
|
// 横向菜单(经典、横向)布局
|
||||||
.el-menu.el-menu--horizontal {
|
.el-menu.el-menu--horizontal {
|
||||||
border-bottom: none !important;
|
border-bottom: none !important;
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
.el-menu-item,
|
|
||||||
.el-sub-menu__title {
|
.el-menu-item,
|
||||||
height: 48px !important;
|
.el-sub-menu__title {
|
||||||
color: var(--bg-topBarColor);
|
height: 48px !important;
|
||||||
}
|
color: var(--bg-topBarColor);
|
||||||
.el-menu-item:not(.is-active):hover,
|
}
|
||||||
.el-sub-menu:not(.is-active):hover .el-sub-menu__title {
|
|
||||||
color: var(--bg-topBarColor);
|
.el-menu-item:not(.is-active):hover,
|
||||||
}
|
.el-sub-menu:not(.is-active):hover .el-sub-menu__title {
|
||||||
|
color: var(--bg-topBarColor);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 菜单收起时,图标不居中问题
|
// 菜单收起时,图标不居中问题
|
||||||
.el-menu--collapse {
|
.el-menu--collapse {
|
||||||
.el-menu-item .iconfont,
|
|
||||||
.el-sub-menu .iconfont,
|
.el-menu-item .iconfont,
|
||||||
.el-menu-item .fa,
|
.el-sub-menu .iconfont,
|
||||||
.el-sub-menu .fa {
|
.el-menu-item .fa,
|
||||||
margin-right: 0 !important;
|
.el-sub-menu .fa {
|
||||||
}
|
margin-right: 0 !important;
|
||||||
.el-sub-menu__title {
|
}
|
||||||
padding-right: 0 !important;
|
|
||||||
}
|
.el-sub-menu__title {
|
||||||
|
padding-right: 0 !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tabs 标签页
|
/* Tabs 标签页
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
.el-tabs__nav-wrap::after {
|
.el-tabs__nav-wrap::after {
|
||||||
height: 1px !important;
|
height: 1px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dropdown 下拉菜单
|
/* Dropdown 下拉菜单
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
.el-dropdown-menu {
|
.el-dropdown-menu {
|
||||||
list-style: none !important; /*修复 Dropdown 下拉菜单样式问题 2022.03.04*/
|
list-style: none !important;
|
||||||
}
|
/*修复 Dropdown 下拉菜单样式问题 2022.03.04*/
|
||||||
.el-dropdown-menu .el-dropdown-menu__item {
|
|
||||||
white-space: nowrap;
|
|
||||||
&:not(.is-disabled):hover {
|
|
||||||
background-color: var(--el-dropdown-menuItem-hover-fill);
|
|
||||||
color: var(--el-dropdown-menuItem-hover-color);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Steps 步骤条
|
.el-dropdown-menu .el-dropdown-menu__item {
|
||||||
------------------------------- */
|
white-space: nowrap;
|
||||||
.el-step__icon-inner {
|
|
||||||
font-size: 30px !important;
|
&:not(.is-disabled):hover {
|
||||||
font-weight: 400 !important;
|
background-color: var(--el-dropdown-menuItem-hover-fill);
|
||||||
}
|
color: var(--el-dropdown-menuItem-hover-color);
|
||||||
.el-step__title {
|
}
|
||||||
font-size: 14px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dialog 对话框
|
/* Dialog 对话框
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
.el-overlay {
|
.el-overlay {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
.el-overlay-dialog {
|
|
||||||
display: flex;
|
.el-overlay-dialog {
|
||||||
align-items: center;
|
display: flex;
|
||||||
justify-content: center;
|
align-items: center;
|
||||||
position: unset !important;
|
justify-content: center;
|
||||||
width: 100%;
|
position: unset !important;
|
||||||
height: 100%;
|
width: 100%;
|
||||||
.el-dialog {
|
height: 100%;
|
||||||
margin: 0 auto !important;
|
|
||||||
position: absolute;
|
.el-dialog {
|
||||||
.el-dialog__body {
|
margin: 0 auto !important;
|
||||||
padding: 20px !important;
|
position: absolute;
|
||||||
}
|
|
||||||
}
|
.el-dialog__body {
|
||||||
}
|
padding: 20px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-dialog__body {
|
.el-dialog__body {
|
||||||
max-height: calc(90vh - 111px) !important;
|
max-height: calc(90vh - 111px) !important;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Card 卡片
|
/* Card 卡片
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
.el-card__header {
|
.el-card__header {
|
||||||
padding: 15px 20px;
|
padding: 15px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Table 表格 element plus 2.2.0 版本
|
/* Table 表格 element plus 2.2.0 版本
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
.el-table {
|
.el-table {
|
||||||
.el-button.is-text {
|
.el-button.is-text {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* scrollbar
|
/* scrollbar
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
.el-scrollbar__bar {
|
.el-scrollbar__bar {
|
||||||
z-index: 4;
|
z-index: 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*防止页面切换时,滚动条高度不变的问题(滚动条高度非滚动条滚动高度)*/
|
/*防止页面切换时,滚动条高度不变的问题(滚动条高度非滚动条滚动高度)*/
|
||||||
.el-scrollbar__wrap {
|
.el-scrollbar__wrap {
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-select-dropdown .el-scrollbar__wrap {
|
.el-select-dropdown .el-scrollbar__wrap {
|
||||||
overflow-x: scroll !important;
|
overflow-x: scroll !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*修复Select 选择器高度问题*/
|
/*修复Select 选择器高度问题*/
|
||||||
.el-select-dropdown__wrap {
|
.el-select-dropdown__wrap {
|
||||||
max-height: 274px !important;
|
max-height: 274px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*修复Cascader 级联选择器高度问题*/
|
/*修复Cascader 级联选择器高度问题*/
|
||||||
.el-cascader-menu__wrap.el-scrollbar__wrap {
|
.el-cascader-menu__wrap.el-scrollbar__wrap {
|
||||||
height: 204px !important;
|
height: 204px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*用于界面高度自适应(main.vue),区分 scrollbar__view,防止其它使用 scrollbar 的地方出现滚动条消失*/
|
/*用于界面高度自适应(main.vue),区分 scrollbar__view,防止其它使用 scrollbar 的地方出现滚动条消失*/
|
||||||
.layout-container-view .el-scrollbar__view {
|
.layout-container-view .el-scrollbar__view {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*防止分栏布局二级菜单很多时,滚动条消失问题*/
|
/*防止分栏布局二级菜单很多时,滚动条消失问题*/
|
||||||
.layout-columns-warp .layout-aside .el-scrollbar__view {
|
.layout-columns-warp .layout-aside .el-scrollbar__view {
|
||||||
height: unset !important;
|
height: unset !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Pagination 分页
|
/* Pagination 分页
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
.el-pagination__editor {
|
.el-pagination__editor {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*深色模式时分页高亮问题*/
|
/*深色模式时分页高亮问题*/
|
||||||
.el-pagination.is-background .btn-next.is-active,
|
.el-pagination.is-background .btn-next.is-active,
|
||||||
.el-pagination.is-background .btn-prev.is-active,
|
.el-pagination.is-background .btn-prev.is-active,
|
||||||
.el-pagination.is-background .el-pager li.is-active {
|
.el-pagination.is-background .el-pager li.is-active {
|
||||||
background-color: var(--el-color-primary) !important;
|
background-color: var(--el-color-primary) !important;
|
||||||
color: var(--el-color-white) !important;
|
color: var(--el-color-white) !important;
|
||||||
}
|
|
||||||
|
|
||||||
/* Drawer 抽屉
|
|
||||||
------------------------------- */
|
|
||||||
.el-drawer {
|
|
||||||
--el-drawer-padding-primary: unset !important;
|
|
||||||
.el-drawer__header {
|
|
||||||
padding: 0 15px !important;
|
|
||||||
height: 50px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 0 !important;
|
|
||||||
border-bottom: 1px solid var(--el-border-color);
|
|
||||||
color: var(--el-text-color-primary);
|
|
||||||
}
|
|
||||||
.el-drawer__body {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Breadcrumb 面包屑
|
/* Breadcrumb 面包屑
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
.el-breadcrumb__inner a:hover,
|
.el-breadcrumb__inner a:hover,
|
||||||
.el-breadcrumb__inner.is-link:hover {
|
.el-breadcrumb__inner.is-link:hover {
|
||||||
color: var(--el-color-primary);
|
color: var(--el-color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-breadcrumb__inner a,
|
.el-breadcrumb__inner a,
|
||||||
.el-breadcrumb__inner.is-link {
|
.el-breadcrumb__inner.is-link {
|
||||||
color: var(--bg-topBarColor);
|
color: var(--bg-topBarColor);
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
// el-tooltip使用自定义主题时的样式
|
// el-tooltip使用自定义主题时的样式
|
||||||
.el-popper.is-customized {
|
.el-popper.is-customized {
|
||||||
/* Set padding to ensure the height is 32px */
|
/* Set padding to ensure the height is 32px */
|
||||||
// padding: 6px 12px;
|
// padding: 6px 12px;
|
||||||
background: linear-gradient(90deg, rgb(159, 229, 151), rgb(204, 229, 129));
|
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);
|
background: linear-gradient(45deg, #b2e68d, #bce689);
|
||||||
right: 0;
|
right: 0;
|
||||||
@@ -343,7 +361,9 @@
|
|||||||
|
|
||||||
|
|
||||||
.el-dialog {
|
.el-dialog {
|
||||||
border-radius: 6px; /* 设置圆角 */
|
border-radius: 6px;
|
||||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); /* 添加轻微阴影效果 */
|
/* 设置圆角 */
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
/* 添加轻微阴影效果 */
|
||||||
border: 1px solid var(--el-border-color-lighter);
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
}
|
}
|
||||||
1
mayfly_go_web/src/types/pinia.d.ts
vendored
1
mayfly_go_web/src/types/pinia.d.ts
vendored
@@ -52,6 +52,7 @@ declare interface ThemeConfigState {
|
|||||||
logoIcon: string;
|
logoIcon: string;
|
||||||
globalI18n: string;
|
globalI18n: string;
|
||||||
globalComponentSize: string;
|
globalComponentSize: string;
|
||||||
|
terminalTheme: string;
|
||||||
terminalForeground: string;
|
terminalForeground: string;
|
||||||
terminalBackground: string;
|
terminalBackground: string;
|
||||||
terminalCursor: string;
|
terminalCursor: string;
|
||||||
|
|||||||
2
mayfly_go_web/src/types/shim.d.ts
vendored
2
mayfly_go_web/src/types/shim.d.ts
vendored
@@ -1,6 +1,6 @@
|
|||||||
// 申明外部 npm 插件模块
|
// 申明外部 npm 插件模块
|
||||||
declare module 'sql-formatter';
|
|
||||||
declare module 'jsoneditor';
|
declare module 'jsoneditor';
|
||||||
declare module 'asciinema-player';
|
declare module 'asciinema-player';
|
||||||
declare module 'vue-grid-layout';
|
declare module 'vue-grid-layout';
|
||||||
declare module 'splitpanes';
|
declare module 'splitpanes';
|
||||||
|
declare module 'uuid';
|
||||||
|
|||||||
@@ -98,4 +98,3 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@/router/staticRouter
|
|
||||||
|
|||||||
211
mayfly_go_web/src/views/flow/ProcdefEdit.vue
Executable file
211
mayfly_go_web/src/views/flow/ProcdefEdit.vue
Executable file
@@ -0,0 +1,211 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-drawer @open="initSort" :title="title" v-model="visible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false">
|
||||||
|
<template #header>
|
||||||
|
<DrawerHeader :header="title" :back="cancel" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-form :model="form" ref="formRef" :rules="rules" label-width="auto">
|
||||||
|
<el-form-item prop="name" label="名称">
|
||||||
|
<el-input v-model.trim="form.name" placeholder="请输入流程名称" auto-complete="off" clearable></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="defKey" label="key">
|
||||||
|
<el-input :disabled="form.id" v-model.trim="form.defKey" placeholder="请输入流程key" auto-complete="off" clearable></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="status" label="状态">
|
||||||
|
<el-select v-model="form.status" placeholder="请选择状态">
|
||||||
|
<el-option v-for="item in ProcdefStatus" :key="item.value" :label="item.label" :value="item.value"> </el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="remark" label="备注">
|
||||||
|
<el-input v-model.trim="form.remark" placeholder="备注" auto-complete="off" clearable></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-divider content-position="left">审批节点</el-divider>
|
||||||
|
|
||||||
|
<el-table ref="taskTableRef" :data="tasks" row-key="taskKey" stripe style="width: 100%">
|
||||||
|
<el-table-column prop="name" label="名称" min-width="100px">
|
||||||
|
<template #header>
|
||||||
|
<el-button class="ml0" type="primary" circle size="small" icon="Plus" @click="addTask()"> </el-button>
|
||||||
|
<span class="ml10">节点名称</span>
|
||||||
|
<el-tooltip content="点击指定节点可进行拖拽排序" placement="top">
|
||||||
|
<el-icon class="ml5">
|
||||||
|
<question-filled />
|
||||||
|
</el-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
</template>
|
||||||
|
<template #default="scope">
|
||||||
|
<el-input v-model="scope.row.name"> </el-input>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="userId" label="审核人员" min-width="150px" show-overflow-tooltip>
|
||||||
|
<template #default="scope">
|
||||||
|
<AccountSelectFormItem v-model="scope.row.userId" label="" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="60px">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-link @click="deleteTask(scope.$index)" class="ml5" type="danger" icon="delete" plain></el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div>
|
||||||
|
<el-button @click="cancel()">取 消</el-button>
|
||||||
|
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk">确 定</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-drawer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { toRefs, reactive, watch, ref, nextTick } from 'vue';
|
||||||
|
import { procdefApi } from './api';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||||
|
import AccountSelectFormItem from '@/views/system/account/components/AccountSelectFormItem.vue';
|
||||||
|
import Sortable from 'sortablejs';
|
||||||
|
import { randomUuid } from '../../common/utils/string';
|
||||||
|
import { ProcdefStatus } from './enums';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
data: {
|
||||||
|
type: [Boolean, Object],
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const visible = defineModel<boolean>('visible', { default: false });
|
||||||
|
|
||||||
|
//定义事件
|
||||||
|
const emit = defineEmits(['cancel', 'val-change']);
|
||||||
|
|
||||||
|
const formRef: any = ref(null);
|
||||||
|
const taskTableRef: any = ref(null);
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
name: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请输入流程名称',
|
||||||
|
trigger: ['change', 'blur'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
defKey: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请输入流程key',
|
||||||
|
trigger: ['change', 'blur'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
tasks: [] as any,
|
||||||
|
form: {
|
||||||
|
id: null,
|
||||||
|
name: null,
|
||||||
|
defKey: null,
|
||||||
|
status: null,
|
||||||
|
remark: null,
|
||||||
|
// 流程的审批节点任务
|
||||||
|
tasks: '',
|
||||||
|
},
|
||||||
|
sortable: '' as any,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { form, tasks } = toRefs(state);
|
||||||
|
|
||||||
|
const { isFetching: saveBtnLoading, execute: saveFlowDefExec } = procdefApi.save.useApi(form);
|
||||||
|
|
||||||
|
watch(props, (newValue: any) => {
|
||||||
|
if (newValue.data) {
|
||||||
|
state.form = { ...newValue.data };
|
||||||
|
const tasks = JSON.parse(state.form.tasks);
|
||||||
|
tasks.forEach((t: any) => {
|
||||||
|
t.userId = Number.parseInt(t.userId);
|
||||||
|
});
|
||||||
|
state.tasks = tasks;
|
||||||
|
} else {
|
||||||
|
state.form = { status: ProcdefStatus.Enable.value } as any;
|
||||||
|
state.tasks = [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const initSort = () => {
|
||||||
|
nextTick(() => {
|
||||||
|
const table = taskTableRef.value.$el.querySelector('table > tbody') as any;
|
||||||
|
state.sortable = Sortable.create(table, {
|
||||||
|
animation: 200,
|
||||||
|
//拖拽结束事件
|
||||||
|
onEnd: (evt) => {
|
||||||
|
const curRow = state.tasks.splice(evt.oldIndex, 1)[0];
|
||||||
|
state.tasks.splice(evt.newIndex, 0, curRow);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const addTask = () => {
|
||||||
|
state.tasks.push({ taskKey: randomUuid() });
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteTask = (idx: any) => {
|
||||||
|
state.tasks.splice(idx, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const btnOk = async () => {
|
||||||
|
formRef.value.validate(async (valid: boolean) => {
|
||||||
|
if (!valid) {
|
||||||
|
ElMessage.error('表单填写有误');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const checkRes = checkTasks();
|
||||||
|
if (checkRes.err) {
|
||||||
|
ElMessage.error(checkRes.err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.form.tasks = JSON.stringify(checkRes.tasks);
|
||||||
|
await saveFlowDefExec();
|
||||||
|
ElMessage.success('操作成功');
|
||||||
|
emit('val-change', state.form);
|
||||||
|
//重置表单域
|
||||||
|
formRef.value.resetFields();
|
||||||
|
state.form = {} as any;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkTasks = () => {
|
||||||
|
if (state.tasks?.length == 0) {
|
||||||
|
return { err: '请完善审批节点任务' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const tasks = [];
|
||||||
|
for (let i = 0; i < state.tasks.length; i++) {
|
||||||
|
const task = { ...state.tasks[i] };
|
||||||
|
if (!task.name || !task.userId) {
|
||||||
|
return { err: `请完善第${i + 1}个审批节点任务信息` };
|
||||||
|
}
|
||||||
|
// 转为字符串(方便后续万一需要调整啥的)
|
||||||
|
task.userId = `${task.userId}`;
|
||||||
|
if (!task.taskKey) {
|
||||||
|
task.taskKey = randomUuid();
|
||||||
|
}
|
||||||
|
tasks.push(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { tasks: tasks };
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
visible.value = false;
|
||||||
|
emit('cancel');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss"></style>
|
||||||
141
mayfly_go_web/src/views/flow/ProcdefList.vue
Normal file
141
mayfly_go_web/src/views/flow/ProcdefList.vue
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<page-table
|
||||||
|
ref="pageTableRef"
|
||||||
|
:page-api="procdefApi.list"
|
||||||
|
:search-items="searchItems"
|
||||||
|
v-model:query-form="query"
|
||||||
|
:show-selection="true"
|
||||||
|
v-model:selection-data="selectionData"
|
||||||
|
:columns="columns"
|
||||||
|
>
|
||||||
|
<template #tableHeader>
|
||||||
|
<el-button v-auth="perms.save" type="primary" icon="plus" @click="editFlowDef(false)">添加</el-button>
|
||||||
|
<el-button v-auth="perms.del" :disabled="state.selectionData.length < 1" @click="deleteProcdef()" type="danger" icon="delete">删除</el-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #tasks="{ data }">
|
||||||
|
<el-link @click="showProcdefTasks(data)" icon="view" type="primary" :underline="false"> </el-link>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #action="{ data }">
|
||||||
|
<el-button link v-if="actionBtns[perms.save]" @click="editFlowDef(data)" type="primary">编辑</el-button>
|
||||||
|
</template>
|
||||||
|
</page-table>
|
||||||
|
|
||||||
|
<el-dialog v-model="flowTasksDialog.visible" :title="flowTasksDialog.title">
|
||||||
|
<procdef-tasks :tasks="flowTasksDialog.tasks" />
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<procdef-edit v-model:visible="flowDefEditor.visible" :title="flowDefEditor.title" v-model:data="flowDefEditor.data" @val-change="valChange()" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, toRefs, reactive, onMounted, Ref } from 'vue';
|
||||||
|
import { procdefApi } from './api';
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||||
|
import { TableColumn } from '@/components/pagetable';
|
||||||
|
import { hasPerms } from '@/components/auth/auth';
|
||||||
|
import { SearchItem } from '@/components/SearchForm';
|
||||||
|
import ProcdefEdit from './ProcdefEdit.vue';
|
||||||
|
import ProcdefTasks from './components/ProcdefTasks.vue';
|
||||||
|
import { ProcdefStatus } from './enums';
|
||||||
|
|
||||||
|
const perms = {
|
||||||
|
save: 'flow:procdef:save',
|
||||||
|
del: 'flow:procdef:del',
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchItems = [SearchItem.input('name', '名称'), SearchItem.input('defKey', 'key')];
|
||||||
|
const columns = [
|
||||||
|
TableColumn.new('name', '名称'),
|
||||||
|
TableColumn.new('defKey', 'key'),
|
||||||
|
TableColumn.new('status', '状态').typeTag(ProcdefStatus),
|
||||||
|
TableColumn.new('remark', '备注'),
|
||||||
|
TableColumn.new('tasks', '审批节点').isSlot().alignCenter().setMinWidth(60),
|
||||||
|
TableColumn.new('creator', '创建账号'),
|
||||||
|
TableColumn.new('createTime', '创建时间').isTime(),
|
||||||
|
];
|
||||||
|
|
||||||
|
// 该用户拥有的的操作列按钮权限
|
||||||
|
const actionBtns = hasPerms([perms.save, perms.del]);
|
||||||
|
const actionColumn = TableColumn.new('action', '操作').isSlot().fixedRight().setMinWidth(160).noShowOverflowTooltip().alignCenter();
|
||||||
|
|
||||||
|
const pageTableRef: Ref<any> = ref(null);
|
||||||
|
const state = reactive({
|
||||||
|
/**
|
||||||
|
* 选中的数据
|
||||||
|
*/
|
||||||
|
selectionData: [],
|
||||||
|
/**
|
||||||
|
* 查询条件
|
||||||
|
*/
|
||||||
|
query: {
|
||||||
|
name: '',
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 0,
|
||||||
|
},
|
||||||
|
flowDefEditor: {
|
||||||
|
title: '新建流程定义',
|
||||||
|
visible: false,
|
||||||
|
data: null as any,
|
||||||
|
},
|
||||||
|
flowTasksDialog: {
|
||||||
|
title: '',
|
||||||
|
visible: false,
|
||||||
|
tasks: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { selectionData, query, flowDefEditor, flowTasksDialog } = toRefs(state);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (Object.keys(actionBtns).length > 0) {
|
||||||
|
columns.push(actionColumn);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const search = async () => {
|
||||||
|
pageTableRef.value.search();
|
||||||
|
};
|
||||||
|
|
||||||
|
const showProcdefTasks = (procdef: any) => {
|
||||||
|
state.flowTasksDialog.tasks = procdef.tasks;
|
||||||
|
state.flowTasksDialog.title = procdef.name + '-审批节点';
|
||||||
|
state.flowTasksDialog.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const editFlowDef = (data: any) => {
|
||||||
|
if (!data) {
|
||||||
|
state.flowDefEditor.data = null;
|
||||||
|
state.flowDefEditor.title = '新建流程定义';
|
||||||
|
} else {
|
||||||
|
state.flowDefEditor.data = data;
|
||||||
|
state.flowDefEditor.title = '编辑流程定义';
|
||||||
|
}
|
||||||
|
state.flowDefEditor.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const valChange = () => {
|
||||||
|
state.flowDefEditor.visible = false;
|
||||||
|
search();
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteProcdef = async () => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确定删除【${state.selectionData.map((x: any) => x.name).join(', ')}】的流程定义?`, '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
});
|
||||||
|
await procdefApi.del.request({ id: state.selectionData.map((x: any) => x.id).join(',') });
|
||||||
|
ElMessage.success('删除成功');
|
||||||
|
search();
|
||||||
|
} catch (err) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss"></style>
|
||||||
171
mayfly_go_web/src/views/flow/ProcinstDetail.vue
Executable file
171
mayfly_go_web/src/views/flow/ProcinstDetail.vue
Executable file
@@ -0,0 +1,171 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-drawer :title="props.title" v-model="visible" :before-close="cancel" size="40%" :close-on-click-modal="!props.instTaskId">
|
||||||
|
<template #header>
|
||||||
|
<DrawerHeader :header="title" :back="cancel" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<el-divider content-position="left">流程信息</el-divider>
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="流程名">{{ procinst.procdefName }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="业务">
|
||||||
|
<enum-tag :enums="FlowBizType" :value="procinst.bizType"></enum-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item label="发起人">
|
||||||
|
<AccountInfo :account-id="procinst.creatorId" :username="procinst.creator" />
|
||||||
|
<!-- {{ procinst.creator }} -->
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="发起时间">{{ dateFormat(procinst.createTime) }}</el-descriptions-item>
|
||||||
|
|
||||||
|
<div v-if="procinst.duration">
|
||||||
|
<el-descriptions-item label="持续时间">{{ formatTime(procinst.duration) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="结束时间">{{ dateFormat(procinst.endTime) }}</el-descriptions-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-descriptions-item label="流程状态">
|
||||||
|
<enum-tag :enums="ProcinstStatus" :value="procinst.status"></enum-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="业务状态">
|
||||||
|
<enum-tag :enums="ProcinstBizStatus" :value="procinst.bizStatus"></enum-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item label="备注">
|
||||||
|
{{ procinst.remark }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<el-divider content-position="left">审批节点</el-divider>
|
||||||
|
<procdef-tasks :tasks="procinst?.procdef?.tasks" :procinst-tasks="procinst.procinstTasks" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<el-divider content-position="left">业务信息</el-divider>
|
||||||
|
<component
|
||||||
|
v-if="procinst.bizType"
|
||||||
|
ref="keyValueRef"
|
||||||
|
:is="bizComponents[procinst.bizType]"
|
||||||
|
:biz-key="procinst.bizKey"
|
||||||
|
:biz-form="procinst.bizForm"
|
||||||
|
>
|
||||||
|
</component>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="props.instTaskId">
|
||||||
|
<el-divider content-position="left">审批表单</el-divider>
|
||||||
|
<el-form :model="form" label-width="auto">
|
||||||
|
<el-form-item prop="status" label="结果" required>
|
||||||
|
<el-select v-model="form.status" placeholder="请选择审批结果">
|
||||||
|
<el-option :label="ProcinstTaskStatus.Pass.label" :value="ProcinstTaskStatus.Pass.value"> </el-option>
|
||||||
|
<!-- <el-option :label="ProcinstTaskStatus.Back.label" :value="ProcinstTaskStatus.Back.value"> </el-option> -->
|
||||||
|
<el-option :label="ProcinstTaskStatus.Reject.label" :value="ProcinstTaskStatus.Reject.value"> </el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="remark" label="备注">
|
||||||
|
<el-input v-model.trim="form.remark" placeholder="备注" type="textarea" clearable></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer v-if="props.instTaskId">
|
||||||
|
<div>
|
||||||
|
<el-button @click="cancel()">取 消</el-button>
|
||||||
|
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk">确 定</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-drawer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { toRefs, reactive, watch, defineAsyncComponent, shallowReactive } from 'vue';
|
||||||
|
import { procinstApi } from './api';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||||
|
import { FlowBizType, ProcinstBizStatus, ProcinstTaskStatus, ProcinstStatus } from './enums';
|
||||||
|
import { dateFormat } from '@/common/utils/date';
|
||||||
|
import ProcdefTasks from './components/ProcdefTasks.vue';
|
||||||
|
import { formatTime } from '@/common/utils/format';
|
||||||
|
import EnumTag from '@/components/enumtag/EnumTag.vue';
|
||||||
|
import AccountInfo from '@/views/system/account/components/AccountInfo.vue';
|
||||||
|
|
||||||
|
const DbSqlExecBiz = defineAsyncComponent(() => import('./flowbiz/DbSqlExecBiz.vue'));
|
||||||
|
const RedisRunWriteCmdBiz = defineAsyncComponent(() => import('./flowbiz/RedisRunWriteCmdBiz.vue'));
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
procinstId: {
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
|
// 流程实例任务id(存在则展示审批相关信息)
|
||||||
|
instTaskId: {
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const visible = defineModel<boolean>('visible', { default: false });
|
||||||
|
|
||||||
|
//定义事件
|
||||||
|
const emit = defineEmits(['cancel', 'val-change']);
|
||||||
|
|
||||||
|
// 业务组件
|
||||||
|
const bizComponents = shallowReactive({
|
||||||
|
db_sql_exec_flow: DbSqlExecBiz,
|
||||||
|
redis_run_write_cmd_flow: RedisRunWriteCmdBiz,
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
procinst: {} as any,
|
||||||
|
tasks: [] as any,
|
||||||
|
form: {
|
||||||
|
status: ProcinstTaskStatus.Pass.value,
|
||||||
|
remark: '',
|
||||||
|
},
|
||||||
|
saveBtnLoading: false,
|
||||||
|
sortable: '' as any,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { procinst, form, saveBtnLoading } = toRefs(state);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.procinstId,
|
||||||
|
async (newValue: any) => {
|
||||||
|
if (newValue) {
|
||||||
|
state.procinst = await procinstApi.detail.request({ id: newValue });
|
||||||
|
} else {
|
||||||
|
state.procinst = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const btnOk = async () => {
|
||||||
|
const status = state.form.status;
|
||||||
|
let api = procinstApi.completeTask;
|
||||||
|
if (status === ProcinstTaskStatus.Back.value) {
|
||||||
|
api = procinstApi.backTask;
|
||||||
|
} else if (status === ProcinstTaskStatus.Reject.value) {
|
||||||
|
api = procinstApi.rejectTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
state.saveBtnLoading = true;
|
||||||
|
await api.request({ id: props.instTaskId, remark: state.form.remark });
|
||||||
|
ElMessage.success('操作成功');
|
||||||
|
cancel();
|
||||||
|
emit('val-change');
|
||||||
|
} finally {
|
||||||
|
state.saveBtnLoading = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
visible.value = false;
|
||||||
|
emit('cancel');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss"></style>
|
||||||
126
mayfly_go_web/src/views/flow/ProcinstList.vue
Normal file
126
mayfly_go_web/src/views/flow/ProcinstList.vue
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<page-table
|
||||||
|
ref="pageTableRef"
|
||||||
|
:page-api="procinstApi.list"
|
||||||
|
:search-items="searchItems"
|
||||||
|
v-model:query-form="query"
|
||||||
|
v-model:selection-data="selectionData"
|
||||||
|
:columns="columns"
|
||||||
|
>
|
||||||
|
<template #tableHeader>
|
||||||
|
<!-- <el-button v-auth="perms.addAccount" type="primary" icon="plus" @click="editFlowDef(false)">添加</el-button> -->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #action="{ data }">
|
||||||
|
<el-button link @click="showProcinst(data)" type="primary">查看</el-button>
|
||||||
|
|
||||||
|
<el-popconfirm
|
||||||
|
v-if="data.status == ProcinstStatus.Active.value || data.status == ProcinstStatus.Suspended.value"
|
||||||
|
title="确认取消该流程?"
|
||||||
|
width="160"
|
||||||
|
@confirm="procinstCancel(data)"
|
||||||
|
>
|
||||||
|
<template #reference>
|
||||||
|
<el-button link type="warning">取消</el-button>
|
||||||
|
</template>
|
||||||
|
</el-popconfirm>
|
||||||
|
</template>
|
||||||
|
</page-table>
|
||||||
|
|
||||||
|
<ProcinstDetail
|
||||||
|
v-model:visible="procinstDetail.visible"
|
||||||
|
:title="procinstDetail.title"
|
||||||
|
:procinst-id="procinstDetail.procinstId"
|
||||||
|
:inst-task-id="procinstDetail.instTaskId"
|
||||||
|
@val-change="valChange()"
|
||||||
|
@cancel="procinstDetail.procinstId = 0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, toRefs, reactive, Ref } from 'vue';
|
||||||
|
import { procinstApi } from './api';
|
||||||
|
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||||
|
import { TableColumn } from '@/components/pagetable';
|
||||||
|
import { SearchItem } from '@/components/SearchForm';
|
||||||
|
import ProcinstDetail from './ProcinstDetail.vue';
|
||||||
|
import { FlowBizType, ProcinstBizStatus, ProcinstStatus } from './enums';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { formatTime } from '@/common/utils/format';
|
||||||
|
|
||||||
|
const searchItems = [
|
||||||
|
SearchItem.select('status', '流程状态').withEnum(ProcinstStatus),
|
||||||
|
SearchItem.select('bizType', '业务类型').withEnum(FlowBizType),
|
||||||
|
SearchItem.input('bizKey', '业务key'),
|
||||||
|
];
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
TableColumn.new('bizType', '业务').typeTag(FlowBizType),
|
||||||
|
TableColumn.new('remark', '备注'),
|
||||||
|
TableColumn.new('creator', '发起人'),
|
||||||
|
TableColumn.new('bizKey', '业务key'),
|
||||||
|
TableColumn.new('procdefName', '流程名'),
|
||||||
|
TableColumn.new('status', '流程状态').typeTag(ProcinstStatus),
|
||||||
|
TableColumn.new('bizStatus', '业务状态').typeTag(ProcinstBizStatus),
|
||||||
|
TableColumn.new('createTime', '发起时间').isTime(),
|
||||||
|
TableColumn.new('endTime', '结束时间').isTime(),
|
||||||
|
TableColumn.new('duration', '持续时间').setFormatFunc((data: any, prop: string) => {
|
||||||
|
const duration = data[prop];
|
||||||
|
if (!duration) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return formatTime(duration);
|
||||||
|
}),
|
||||||
|
TableColumn.new('bizHandleRes', '业务处理结果'),
|
||||||
|
TableColumn.new('action', '操作').isSlot().fixedRight().setMinWidth(160).noShowOverflowTooltip().alignCenter(),
|
||||||
|
];
|
||||||
|
|
||||||
|
const pageTableRef: Ref<any> = ref(null);
|
||||||
|
const state = reactive({
|
||||||
|
/**
|
||||||
|
* 选中的数据
|
||||||
|
*/
|
||||||
|
selectionData: [],
|
||||||
|
/**
|
||||||
|
* 查询条件
|
||||||
|
*/
|
||||||
|
query: {
|
||||||
|
status: null,
|
||||||
|
bizType: '',
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 0,
|
||||||
|
},
|
||||||
|
procinstDetail: {
|
||||||
|
title: '查看流程',
|
||||||
|
visible: false,
|
||||||
|
procinstId: 0,
|
||||||
|
instTaskId: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { selectionData, query, procinstDetail } = toRefs(state);
|
||||||
|
|
||||||
|
const search = async () => {
|
||||||
|
pageTableRef.value.search();
|
||||||
|
};
|
||||||
|
|
||||||
|
const procinstCancel = async (data: any) => {
|
||||||
|
await procinstApi.cancel.request({ id: data.id });
|
||||||
|
ElMessage.success('操作成功');
|
||||||
|
search();
|
||||||
|
};
|
||||||
|
|
||||||
|
const showProcinst = (data: any) => {
|
||||||
|
state.procinstDetail.procinstId = data.id;
|
||||||
|
state.procinstDetail.title = '流程查看';
|
||||||
|
state.procinstDetail.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const valChange = () => {
|
||||||
|
state.procinstDetail.visible = false;
|
||||||
|
search();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss"></style>
|
||||||
111
mayfly_go_web/src/views/flow/ProcinstTaskList.vue
Normal file
111
mayfly_go_web/src/views/flow/ProcinstTaskList.vue
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<page-table
|
||||||
|
ref="pageTableRef"
|
||||||
|
:page-api="procinstApi.tasks"
|
||||||
|
:search-items="searchItems"
|
||||||
|
v-model:query-form="query"
|
||||||
|
v-model:selection-data="selectionData"
|
||||||
|
:columns="columns"
|
||||||
|
>
|
||||||
|
<template #tableHeader>
|
||||||
|
<!-- <el-button v-auth="perms.addAccount" type="primary" icon="plus" @click="editFlowDef(false)">添加</el-button> -->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #action="{ data }">
|
||||||
|
<el-button link @click="showProcinst(data, false)" type="primary">查看</el-button>
|
||||||
|
<el-button v-if="data.status == ProcinstTaskStatus.Process.value" link @click="showProcinst(data, true)" type="primary">审核</el-button>
|
||||||
|
</template>
|
||||||
|
</page-table>
|
||||||
|
|
||||||
|
<ProcinstDetail
|
||||||
|
v-model:visible="procinstDetail.visible"
|
||||||
|
:title="procinstDetail.title"
|
||||||
|
:procinst-id="procinstDetail.procinstId"
|
||||||
|
:inst-task-id="procinstDetail.instTaskId"
|
||||||
|
@val-change="valChange()"
|
||||||
|
@cancel="procinstDetail.procinstId = 0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, toRefs, reactive, Ref } from 'vue';
|
||||||
|
import { procinstApi } from './api';
|
||||||
|
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||||
|
import { TableColumn } from '@/components/pagetable';
|
||||||
|
import { SearchItem } from '@/components/SearchForm';
|
||||||
|
import ProcinstDetail from './ProcinstDetail.vue';
|
||||||
|
import { FlowBizType, ProcinstStatus, ProcinstTaskStatus } from './enums';
|
||||||
|
import { formatTime } from '@/common/utils/format';
|
||||||
|
|
||||||
|
const searchItems = [SearchItem.select('status', '任务状态').withEnum(ProcinstTaskStatus), SearchItem.select('bizType', '业务类型').withEnum(FlowBizType)];
|
||||||
|
const columns = [
|
||||||
|
TableColumn.new('procinst.bizType', '业务').typeTag(FlowBizType),
|
||||||
|
TableColumn.new('procinst.remark', '备注'),
|
||||||
|
TableColumn.new('procinst.creator', '发起人'),
|
||||||
|
TableColumn.new('procinst.status', '流程状态').typeTag(ProcinstStatus),
|
||||||
|
TableColumn.new('status', '任务状态').typeTag(ProcinstTaskStatus),
|
||||||
|
TableColumn.new('procinst.bizKey', '业务key'),
|
||||||
|
TableColumn.new('procinst.procdefName', '流程名'),
|
||||||
|
TableColumn.new('taskName', '当前节点'),
|
||||||
|
TableColumn.new('procinst.createTime', '发起时间').isTime(),
|
||||||
|
TableColumn.new('createTime', '开始时间').isTime(),
|
||||||
|
TableColumn.new('endTime', '结束时间').isTime(),
|
||||||
|
TableColumn.new('duration', '持续时间').setFormatFunc((data: any, prop: string) => {
|
||||||
|
const duration = data[prop];
|
||||||
|
if (!duration) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return formatTime(duration);
|
||||||
|
}),
|
||||||
|
TableColumn.new('action', '操作').isSlot().fixedRight().setMinWidth(160).noShowOverflowTooltip().alignCenter(),
|
||||||
|
];
|
||||||
|
|
||||||
|
const pageTableRef: Ref<any> = ref(null);
|
||||||
|
const state = reactive({
|
||||||
|
/**
|
||||||
|
* 选中的数据
|
||||||
|
*/
|
||||||
|
selectionData: [],
|
||||||
|
/**
|
||||||
|
* 查询条件
|
||||||
|
*/
|
||||||
|
query: {
|
||||||
|
status: ProcinstTaskStatus.Process.value,
|
||||||
|
bizType: '',
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 0,
|
||||||
|
},
|
||||||
|
procinstDetail: {
|
||||||
|
title: '查看流程',
|
||||||
|
visible: false,
|
||||||
|
procinstId: 0,
|
||||||
|
instTaskId: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { selectionData, query, procinstDetail } = toRefs(state);
|
||||||
|
|
||||||
|
const search = async () => {
|
||||||
|
pageTableRef.value.search();
|
||||||
|
};
|
||||||
|
|
||||||
|
const showProcinst = (data: any, audit: boolean) => {
|
||||||
|
state.procinstDetail.procinstId = data.procinstId;
|
||||||
|
if (!audit) {
|
||||||
|
state.procinstDetail.instTaskId = 0;
|
||||||
|
state.procinstDetail.title = '流程查看';
|
||||||
|
} else {
|
||||||
|
state.procinstDetail.instTaskId = data.id;
|
||||||
|
state.procinstDetail.title = '流程审批';
|
||||||
|
}
|
||||||
|
state.procinstDetail.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const valChange = () => {
|
||||||
|
state.procinstDetail.visible = false;
|
||||||
|
search();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss"></style>
|
||||||
20
mayfly_go_web/src/views/flow/api.ts
Normal file
20
mayfly_go_web/src/views/flow/api.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import Api from '@/common/Api';
|
||||||
|
|
||||||
|
export const procdefApi = {
|
||||||
|
list: Api.newGet('/flow/procdefs'),
|
||||||
|
getByKey: Api.newGet('/flow/procdefs/{key}'),
|
||||||
|
save: Api.newPost('/flow/procdefs'),
|
||||||
|
del: Api.newDelete('/flow/procdefs/{id}'),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const procinstApi = {
|
||||||
|
list: Api.newGet('/flow/procinsts'),
|
||||||
|
detail: Api.newGet('/flow/procinsts/{id}'),
|
||||||
|
cancel: Api.newPost('/flow/procinsts/{id}/cancel'),
|
||||||
|
tasks: Api.newGet('/flow/procinsts/tasks'),
|
||||||
|
completeTask: Api.newPost('/flow/procinsts/tasks/complete'),
|
||||||
|
backTask: Api.newPost('/flow/procinsts/tasks/back'),
|
||||||
|
rejectTask: Api.newPost('/flow/procinsts/tasks/reject'),
|
||||||
|
save: Api.newPost('/flow/procdefs'),
|
||||||
|
del: Api.newDelete('/flow/procdefs/{id}'),
|
||||||
|
};
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<template>
|
||||||
|
<el-form-item :label="props.label">
|
||||||
|
<el-select style="width: 100%" v-model="procdefKey" filterable placeholder="绑定流程则开启对应审批流程" v-bind="$attrs" clearable>
|
||||||
|
<el-option v-for="item in procdefs" :key="item.defKey" :label="`${item.defKey} [${item.name}]`" :value="item.defKey"> </el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import { procdefApi } from '../api';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: '工单流程',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getProcdefs();
|
||||||
|
});
|
||||||
|
|
||||||
|
const procdefKey = defineModel('modelValue');
|
||||||
|
|
||||||
|
const procdefs: any = ref([]);
|
||||||
|
|
||||||
|
const getProcdefs = () => {
|
||||||
|
procdefApi.list.request({ pageSize: 200 }).then((res) => {
|
||||||
|
procdefs.value = res.list;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
136
mayfly_go_web/src/views/flow/components/ProcdefTasks.vue
Executable file
136
mayfly_go_web/src/views/flow/components/ProcdefTasks.vue
Executable file
@@ -0,0 +1,136 @@
|
|||||||
|
<template>
|
||||||
|
<el-steps align-center :active="stepActive">
|
||||||
|
<el-step v-for="task in tasksArr" :status="getStepStatus(task)" :title="task.name" :key="task.taskKey">
|
||||||
|
<template #description>
|
||||||
|
<div>{{ `${task.accountUsername}(${task.accountName})` }}</div>
|
||||||
|
<div v-if="task.completeTime">{{ `${dateFormat(task.completeTime)}` }}</div>
|
||||||
|
<div v-if="task.remark">{{ task.remark }}</div>
|
||||||
|
</template>
|
||||||
|
</el-step>
|
||||||
|
</el-steps>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { toRefs, reactive, watch, onMounted } from 'vue';
|
||||||
|
import { accountApi } from '../../system/api';
|
||||||
|
import { ProcinstTaskStatus } from '../enums';
|
||||||
|
import { dateFormat } from '@/common/utils/date';
|
||||||
|
import { procdefApi } from '../api';
|
||||||
|
import { ElSteps, ElStep } from 'element-plus';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
// 流程定义任务
|
||||||
|
tasks: {
|
||||||
|
type: [String, Object],
|
||||||
|
},
|
||||||
|
procdefKey: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
// 流程实例任务列表
|
||||||
|
procinstTasks: {
|
||||||
|
type: [Array],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
tasksArr: [] as any,
|
||||||
|
stepActive: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { tasksArr, stepActive } = toRefs(state);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.tasks,
|
||||||
|
(newValue: any) => {
|
||||||
|
parseTasks(newValue);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.procinstTasks,
|
||||||
|
() => {
|
||||||
|
parseTasks(props.tasks);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.procdefKey,
|
||||||
|
async (newValue: any) => {
|
||||||
|
if (newValue) {
|
||||||
|
parseTasksByKey(newValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.procdefKey) {
|
||||||
|
parseTasksByKey(props.procdefKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
parseTasks(props.tasks);
|
||||||
|
});
|
||||||
|
|
||||||
|
const parseTasksByKey = async (key: string) => {
|
||||||
|
const procdef = await procdefApi.getByKey.request({ key });
|
||||||
|
parseTasks(procdef.tasks);
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseTasks = async (tasksStr: any) => {
|
||||||
|
if (!tasksStr) return;
|
||||||
|
const tasks = JSON.parse(tasksStr);
|
||||||
|
const userIds = tasks.map((x: any) => x.userId);
|
||||||
|
const usersRes = await accountApi.querySimple.request({ ids: [...new Set(userIds)].join(','), pageSize: 50 });
|
||||||
|
const users = usersRes.list;
|
||||||
|
// 将数组转换为 Map 结构,以 id 为 key
|
||||||
|
const userMap = users.reduce((acc: any, obj: any) => {
|
||||||
|
acc.set(obj.id, obj);
|
||||||
|
return acc;
|
||||||
|
}, new Map());
|
||||||
|
|
||||||
|
// 流程实例任务(用于显示完成时间,完成到哪一步等)
|
||||||
|
let instTasksMap: any;
|
||||||
|
if (props.procinstTasks) {
|
||||||
|
state.stepActive = props.procinstTasks.length - 1;
|
||||||
|
instTasksMap = props.procinstTasks.reduce((acc: any, obj: any) => {
|
||||||
|
acc.set(obj.taskKey, obj);
|
||||||
|
return acc;
|
||||||
|
}, new Map());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let task of tasks) {
|
||||||
|
const user = userMap.get(Number.parseInt(task.userId));
|
||||||
|
task.accountUsername = user.username;
|
||||||
|
task.accountName = user.name;
|
||||||
|
|
||||||
|
// 存在实例任务,则赋值实例任务对应的完成时间和备注
|
||||||
|
const instTask = instTasksMap?.get(task.taskKey);
|
||||||
|
if (instTask) {
|
||||||
|
task.status = instTask.status;
|
||||||
|
task.completeTime = instTask.endTime;
|
||||||
|
task.remark = instTask.remark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.tasksArr = tasks;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStepStatus = (task: any): any => {
|
||||||
|
const taskStatus = task.status;
|
||||||
|
if (!taskStatus) {
|
||||||
|
return 'wait';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (taskStatus == ProcinstTaskStatus.Pass.value) {
|
||||||
|
return 'success';
|
||||||
|
}
|
||||||
|
if (taskStatus == ProcinstTaskStatus.Process.value) {
|
||||||
|
return 'proccess';
|
||||||
|
}
|
||||||
|
if (taskStatus == ProcinstTaskStatus.Back.value || taskStatus == ProcinstTaskStatus.Reject.value) {
|
||||||
|
return 'error';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'wait';
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss"></style>
|
||||||
34
mayfly_go_web/src/views/flow/enums.ts
Normal file
34
mayfly_go_web/src/views/flow/enums.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { EnumValue } from '@/common/Enum';
|
||||||
|
|
||||||
|
export const ProcdefStatus = {
|
||||||
|
Enable: EnumValue.of(1, '启用').setTagType('success'),
|
||||||
|
Disable: EnumValue.of(-1, '禁用').setTagType('warning'),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProcinstStatus = {
|
||||||
|
Active: EnumValue.of(1, '执行中').setTagType('primary'),
|
||||||
|
Completed: EnumValue.of(2, '完成').setTagType('success'),
|
||||||
|
Suspended: EnumValue.of(-1, '挂起').setTagType('warning'),
|
||||||
|
Terminated: EnumValue.of(-2, '终止').setTagType('danger'),
|
||||||
|
Cancelled: EnumValue.of(-3, '取消').setTagType('warning'),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProcinstBizStatus = {
|
||||||
|
Wait: EnumValue.of(1, '待处理').setTagType('primary'),
|
||||||
|
Success: EnumValue.of(2, '处理成功').setTagType('success'),
|
||||||
|
Fail: EnumValue.of(-2, '处理失败').setTagType('danger'),
|
||||||
|
No: EnumValue.of(-1, '不处理').setTagType('warning'),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProcinstTaskStatus = {
|
||||||
|
Process: EnumValue.of(1, '待处理').setTagType('primary'),
|
||||||
|
Pass: EnumValue.of(2, '通过').setTagType('success'),
|
||||||
|
Reject: EnumValue.of(-1, '拒绝').setTagType('danger'),
|
||||||
|
Back: EnumValue.of(-2, '驳回').setTagType('warning'),
|
||||||
|
Canceled: EnumValue.of(-3, '取消').setTagType('warning'),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FlowBizType = {
|
||||||
|
DbSqlExec: EnumValue.of('db_sql_exec_flow', 'DBMS-执行SQL'),
|
||||||
|
RedisRunWriteCmd: EnumValue.of('redis_run_write_cmd_flow', 'Redis-执行write命令'),
|
||||||
|
};
|
||||||
79
mayfly_go_web/src/views/flow/flowbiz/DbSqlExecBiz.vue
Executable file
79
mayfly_go_web/src/views/flow/flowbiz/DbSqlExecBiz.vue
Executable file
@@ -0,0 +1,79 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-descriptions :column="3" border>
|
||||||
|
<el-descriptions-item :span="2" label="名称">{{ db?.name }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item :span="1" label="id">{{ db?.id }}</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item :span="3" label="关联标签"><ResourceTags :tags="db.tags" /></el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item :span="1" label="主机">{{ `${db?.host}:${db?.port}` }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item :span="1" label="类型">
|
||||||
|
<SvgIcon :name="getDbDialect(db?.type).getInfo().icon" :size="20" />{{ db?.type }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item :span="1" label="用户名">{{ db?.username }}</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item label="数据库">{{ sqlExec.db }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="表">
|
||||||
|
{{ sqlExec.table }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="类型">
|
||||||
|
<el-tag size="small">{{ EnumValue.getLabelByValue(DbSqlExecTypeEnum, sqlExec.type) }}</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="执行SQL">
|
||||||
|
<monaco-editor height="300px" language="sql" v-model="sqlExec.sql" :options="{ readOnly: true }" />
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { toRefs, reactive, watch, onMounted } from 'vue';
|
||||||
|
import EnumValue from '@/common/Enum';
|
||||||
|
import { dbApi } from '@/views/ops/db/api';
|
||||||
|
import { DbSqlExecTypeEnum } from '@/views/ops/db/enums';
|
||||||
|
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||||
|
import { getDbDialect } from '@/views/ops/db/dialect';
|
||||||
|
import ResourceTags from '@/views/ops/component/ResourceTags.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
// 业务key
|
||||||
|
bizKey: {
|
||||||
|
type: [String],
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
sqlExec: {
|
||||||
|
sql: '',
|
||||||
|
} as any,
|
||||||
|
db: {} as any,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { sqlExec, db } = toRefs(state);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getDbSqlExec(props.bizKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.bizKey,
|
||||||
|
(newValue: any) => {
|
||||||
|
getDbSqlExec(newValue);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const getDbSqlExec = async (bizKey: string) => {
|
||||||
|
if (!bizKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const res = await dbApi.getSqlExecs.request({ flowBizKey: bizKey });
|
||||||
|
if (!res.list) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.sqlExec = res.list?.[0];
|
||||||
|
const dbRes = await dbApi.dbs.request({ id: state.sqlExec.dbId });
|
||||||
|
state.db = dbRes.list?.[0];
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss"></style>
|
||||||
80
mayfly_go_web/src/views/flow/flowbiz/RedisRunWriteCmdBiz.vue
Executable file
80
mayfly_go_web/src/views/flow/flowbiz/RedisRunWriteCmdBiz.vue
Executable file
@@ -0,0 +1,80 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-descriptions :column="3" border>
|
||||||
|
<el-descriptions-item :span="1" label="名称">{{ redis?.name }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item :span="1" label="id">{{ redis?.id }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item :span="1" label="用户名">{{ redis?.username }}</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item :span="3" label="关联标签"><ResourceTags :tags="redis.tags" /></el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item :span="1" label="主机">{{ `${redis?.host}` }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item :span="1" label="库">{{ state.db }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item :span="1" label="mode">
|
||||||
|
{{ redis.mode }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item :span="3" label="执行Cmd">
|
||||||
|
<el-input type="textarea" disabled v-model="cmd" rows="5" />
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { toRefs, reactive, watch, onMounted } from 'vue';
|
||||||
|
import ResourceTags from '@/views/ops/component/ResourceTags.vue';
|
||||||
|
import { redisApi } from '@/views/ops/redis/api';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
// 业务表单
|
||||||
|
bizForm: {
|
||||||
|
type: [String],
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
cmd: '',
|
||||||
|
db: 0,
|
||||||
|
redis: {} as any,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { cmd, redis } = toRefs(state);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
parseRunCmdForm(props.bizForm);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.bizForm,
|
||||||
|
(newValue: any) => {
|
||||||
|
parseRunCmdForm(newValue);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const parseRunCmdForm = async (bizForm: string) => {
|
||||||
|
if (!bizForm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const form = JSON.parse(bizForm);
|
||||||
|
|
||||||
|
const cmds = form.cmd.map((item: any, index: number) => {
|
||||||
|
if (index === 0) {
|
||||||
|
return item; // 第一个元素直接返回原值
|
||||||
|
}
|
||||||
|
if (typeof item === 'string') {
|
||||||
|
return `'${item}'`; // 字符串加单引号
|
||||||
|
}
|
||||||
|
return item; // 其他类型直接返回
|
||||||
|
});
|
||||||
|
state.cmd = cmds.join(' ');
|
||||||
|
state.db = form.db;
|
||||||
|
|
||||||
|
const res = await redisApi.redisList.request({ id: form.id });
|
||||||
|
if (!res.list) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.redis = res.list?.[0];
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss"></style>
|
||||||
@@ -73,12 +73,28 @@ const currentTime = computed(() => {
|
|||||||
|
|
||||||
// 初始化数字滚动
|
// 初始化数字滚动
|
||||||
const initNumCountUp = async () => {
|
const initNumCountUp = async () => {
|
||||||
const res: any = await indexApi.getIndexCount.request();
|
indexApi.machineDashbord.request().then((res: any) => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
new CountUp('mongoNum', res.mongoNum).start();
|
new CountUp('machineNum', res.machineNum).start();
|
||||||
new CountUp('machineNum', res.machineNum).start();
|
});
|
||||||
new CountUp('dbNum', res.dbNum).start();
|
});
|
||||||
new CountUp('redisNum', res.redisNum).start();
|
|
||||||
|
indexApi.dbDashbord.request().then((res: any) => {
|
||||||
|
nextTick(() => {
|
||||||
|
new CountUp('dbNum', res.dbNum).start();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
indexApi.redisDashbord.request().then((res: any) => {
|
||||||
|
nextTick(() => {
|
||||||
|
new CountUp('redisNum', res.redisNum).start();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
indexApi.mongoDashbord.request().then((res: any) => {
|
||||||
|
nextTick(() => {
|
||||||
|
new CountUp('mongoNum', res.mongoNum).start();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -93,7 +109,7 @@ const toPage = (item: any) => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'machineNum': {
|
case 'machineNum': {
|
||||||
router.push('/machine/machines');
|
router.push('/machine/machines-op');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'dbNum': {
|
case 'dbNum': {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import Api from '@/common/Api';
|
import Api from '@/common/Api';
|
||||||
|
|
||||||
export const indexApi = {
|
export const indexApi = {
|
||||||
getIndexCount: Api.newGet("/common/index/count"),
|
machineDashbord: Api.newGet('/machines/dashbord'),
|
||||||
}
|
dbDashbord: Api.newGet('/dbs/dashbord'),
|
||||||
|
redisDashbord: Api.newGet('/redis/dashbord'),
|
||||||
|
mongoDashbord: Api.newGet('/mongos/dashbord'),
|
||||||
|
};
|
||||||
|
|||||||
28
mayfly_go_web/src/views/ops/component/ResourceAuthCert.vue
Normal file
28
mayfly_go_web/src/views/ops/component/ResourceAuthCert.vue
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="props.authCerts">
|
||||||
|
<el-select default-first-option value-key="name" style="width: 100%" v-model="selectAuthCert" size="small">
|
||||||
|
<el-option v-for="item in props.authCerts" :key="item.name" :label="item.username" :value="item">
|
||||||
|
{{ item.username }}
|
||||||
|
<el-divider direction="vertical" border-style="dashed" />
|
||||||
|
<EnumTag :value="item.type" :enums="AuthCertTypeEnum" />
|
||||||
|
<el-divider direction="vertical" border-style="dashed" />
|
||||||
|
<EnumTag :value="item.ciphertextType" :enums="AuthCertCiphertextTypeEnum" />
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import EnumTag from '@/components/enumtag/EnumTag.vue';
|
||||||
|
import { AuthCertTypeEnum, AuthCertCiphertextTypeEnum } from '../tag/enums';
|
||||||
|
const props = defineProps({
|
||||||
|
authCerts: {
|
||||||
|
type: [Array<any>],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectAuthCert = defineModel('selectAuthCert');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss"></style>
|
||||||
262
mayfly_go_web/src/views/ops/component/ResourceAuthCertEdit.vue
Normal file
262
mayfly_go_web/src/views/ops/component/ResourceAuthCertEdit.vue
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
<template>
|
||||||
|
<div class="auth-cert-edit">
|
||||||
|
<el-dialog :title="props.title" v-model="dialogVisible" :show-close="false" width="500px" :destroy-on-close="true" :close-on-click-modal="false">
|
||||||
|
<el-form ref="acForm" :model="state.form" label-width="auto" :rules="rules">
|
||||||
|
<el-form-item prop="type" label="凭证类型" required>
|
||||||
|
<el-select @change="changeType" v-model="form.type" placeholder="请选择凭证类型">
|
||||||
|
<el-option
|
||||||
|
v-for="item in AuthCertTypeEnum"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
v-show="!props.disableType?.includes(item.value)"
|
||||||
|
>
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item prop="ciphertextType" label="密文类型" required>
|
||||||
|
<el-select v-model="form.ciphertextType" placeholder="请选择密文类型" @change="changeCiphertextType">
|
||||||
|
<el-option
|
||||||
|
v-for="item in AuthCertCiphertextTypeEnum"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
v-show="!props.disableCiphertextType?.includes(item.value)"
|
||||||
|
:disabled="item.value == AuthCertCiphertextTypeEnum.Public.value && form.type == AuthCertTypeEnum.Public.value"
|
||||||
|
>
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<template v-if="showResourceEdit">
|
||||||
|
<el-form-item prop="type" label="资源类型" required>
|
||||||
|
<el-select :disabled="form.id" v-model="form.resourceType" placeholder="请选择资源类型">
|
||||||
|
<el-option
|
||||||
|
:key="TagResourceTypeEnum.Machine.value"
|
||||||
|
:label="TagResourceTypeEnum.Machine.label"
|
||||||
|
:value="TagResourceTypeEnum.Machine.value"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<el-option :key="TagResourceTypeEnum.Db.value" :label="TagResourceTypeEnum.Db.label" :value="TagResourceTypeEnum.Db.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="resourceCode" label="资源编号" required>
|
||||||
|
<el-input :disabled="form.id" v-model="form.resourceCode" placeholder="请输入资源编号"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-form-item prop="name" label="名称" required>
|
||||||
|
<el-input :disabled="form.id" v-model="form.name" placeholder="请输入凭证名 (全局唯一)"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<template v-if="form.ciphertextType != AuthCertCiphertextTypeEnum.Public.value">
|
||||||
|
<el-form-item prop="username" label="用户名">
|
||||||
|
<el-input v-model="form.username"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item v-if="form.ciphertextType == AuthCertCiphertextTypeEnum.Password.value" prop="ciphertext" label="密码">
|
||||||
|
<el-input type="password" show-password clearable v-model.trim="form.ciphertext" placeholder="请输入密码" autocomplete="new-password">
|
||||||
|
<template #suffix>
|
||||||
|
<SvgIcon v-if="form.id" v-auth="'authcert:showciphertext'" @click="getCiphertext" name="search" />
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item v-if="form.ciphertextType == AuthCertCiphertextTypeEnum.PrivateKey.value" prop="ciphertext" label="秘钥">
|
||||||
|
<div class="w100" style="position: relative">
|
||||||
|
<SvgIcon
|
||||||
|
v-if="form.id"
|
||||||
|
v-auth="'authcert:showciphertext'"
|
||||||
|
@click="getCiphertext"
|
||||||
|
name="search"
|
||||||
|
style="position: absolute; top: 5px; right: 5px; cursor: pointer; z-index: 1"
|
||||||
|
/>
|
||||||
|
<el-input type="textarea" :rows="5" v-model="form.ciphertext" placeholder="请将私钥文件内容拷贝至此"> </el-input>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item v-if="form.ciphertextType == AuthCertCiphertextTypeEnum.PrivateKey.value" prop="passphrase" label="秘钥密码">
|
||||||
|
<el-input type="password" show-password v-model="form.extra.passphrase"> </el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<el-form-item label="公共凭证">
|
||||||
|
<el-select default-first-option filterable v-model="form.ciphertext" @change="changePublicAuthCert">
|
||||||
|
<el-option v-for="item in state.publicAuthCerts" :key="item.name" :label="item.name" :value="item.name">
|
||||||
|
{{ item.name }}
|
||||||
|
<el-divider direction="vertical" border-style="dashed" />
|
||||||
|
{{ item.username }}
|
||||||
|
<el-divider direction="vertical" border-style="dashed" />
|
||||||
|
<EnumTag :value="item.ciphertextType" :enums="AuthCertCiphertextTypeEnum" />
|
||||||
|
<el-divider direction="vertical" border-style="dashed" />
|
||||||
|
{{ item.remark }}
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-form-item label="备注">
|
||||||
|
<el-input v-model="form.remark" type="textarea" :rows="2"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button @click="cancelEdit">取 消</el-button>
|
||||||
|
<el-button type="primary" :loading="btnLoading" @click="btnOk">确 定</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { reactive, ref, toRefs, onMounted, watch, computed } from 'vue';
|
||||||
|
import { AuthCertTypeEnum, AuthCertCiphertextTypeEnum } from '../tag/enums';
|
||||||
|
import EnumTag from '@/components/enumtag/EnumTag.vue';
|
||||||
|
import { resourceAuthCertApi } from '../tag/api';
|
||||||
|
import { ResourceCodePattern } from '@/common/pattern';
|
||||||
|
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: '凭证保存',
|
||||||
|
},
|
||||||
|
authCert: {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
disableCiphertextType: {
|
||||||
|
type: Array,
|
||||||
|
},
|
||||||
|
disableType: {
|
||||||
|
type: Array,
|
||||||
|
},
|
||||||
|
// 是否为资源编辑该授权凭证,即机器编辑等页面等
|
||||||
|
resourceEdit: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const DefaultForm = {
|
||||||
|
id: null,
|
||||||
|
name: '',
|
||||||
|
username: '',
|
||||||
|
ciphertextType: AuthCertCiphertextTypeEnum.Password.value,
|
||||||
|
type: AuthCertTypeEnum.Private.value,
|
||||||
|
resourceType: TagResourceTypeEnum.AuthCert.value,
|
||||||
|
resourceCode: '',
|
||||||
|
ciphertext: '',
|
||||||
|
extra: {} as any,
|
||||||
|
remark: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
name: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请输入凭证名',
|
||||||
|
trigger: ['change', 'blur'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: ResourceCodePattern.pattern,
|
||||||
|
message: ResourceCodePattern.message,
|
||||||
|
trigger: ['blur'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const emit = defineEmits(['confirm', 'cancel']);
|
||||||
|
|
||||||
|
const dialogVisible = defineModel<boolean>('visible', { default: false });
|
||||||
|
|
||||||
|
const acForm: any = ref(null);
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
form: { ...DefaultForm },
|
||||||
|
btnLoading: false,
|
||||||
|
publicAuthCerts: [] as any,
|
||||||
|
});
|
||||||
|
|
||||||
|
const showResourceEdit = computed(() => {
|
||||||
|
return state.form.type != AuthCertTypeEnum.Public.value && !props.resourceEdit;
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
setForm(props.authCert);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.authCert,
|
||||||
|
(val: any) => {
|
||||||
|
setForm(val);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const setForm = (val: any) => {
|
||||||
|
val = { ...val };
|
||||||
|
if (!val.extra) {
|
||||||
|
val.extra = {};
|
||||||
|
}
|
||||||
|
state.form = val;
|
||||||
|
if (state.form.ciphertextType == AuthCertCiphertextTypeEnum.Public.value) {
|
||||||
|
getPublicAuthCerts();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { form, btnLoading } = toRefs(state);
|
||||||
|
|
||||||
|
const changeType = (val: any) => {
|
||||||
|
// 如果选择了公共凭证,则需要保证密文类型不能为公共凭证
|
||||||
|
if (val == AuthCertTypeEnum.Public.value && state.form.ciphertextType == AuthCertCiphertextTypeEnum.Public.value) {
|
||||||
|
state.form.ciphertextType = AuthCertCiphertextTypeEnum.Password.value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeCiphertextType = (val: any) => {
|
||||||
|
if (val == AuthCertCiphertextTypeEnum.Public.value) {
|
||||||
|
state.form.type = AuthCertTypeEnum.Private.value;
|
||||||
|
getPublicAuthCerts();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const changePublicAuthCert = (val: string) => {
|
||||||
|
// 使用公共授权凭证名称赋值username
|
||||||
|
state.form.username = val;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPublicAuthCerts = async () => {
|
||||||
|
const res = await resourceAuthCertApi.listByQuery.request({
|
||||||
|
type: AuthCertTypeEnum.Public.value,
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 100,
|
||||||
|
});
|
||||||
|
state.publicAuthCerts = res.list;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCiphertext = async () => {
|
||||||
|
const res = await resourceAuthCertApi.detail.request({ name: state.form.name });
|
||||||
|
state.form.ciphertext = res.ciphertext;
|
||||||
|
state.form.extra.passphrase = res.extra?.passphrase;
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelEdit = () => {
|
||||||
|
dialogVisible.value = false;
|
||||||
|
emit('cancel');
|
||||||
|
setTimeout(() => {
|
||||||
|
acForm.value?.resetFields();
|
||||||
|
state.form = { ...DefaultForm };
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
const btnOk = async () => {
|
||||||
|
acForm.value.validate(async (valid: boolean) => {
|
||||||
|
if (valid) {
|
||||||
|
emit('confirm', state.form);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss"></style>
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
<template>
|
||||||
|
<div class="auth-cert-manage">
|
||||||
|
<el-table :data="authCerts" max-height="180" stripe size="small">
|
||||||
|
<el-table-column min-wdith="120px">
|
||||||
|
<template #header>
|
||||||
|
<el-button v-auth="'authcert:save'" class="ml0" type="primary" circle size="small" icon="Plus" @click="edit(null)"> </el-button>
|
||||||
|
</template>
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button v-auth="'authcert:save'" @click="edit(scope.row, scope.$index)" type="primary" icon="edit" link></el-button>
|
||||||
|
<el-button class="ml1" v-auth="'authcert:del'" type="danger" @click="deleteRow(scope.$index)" icon="delete" link></el-button>
|
||||||
|
|
||||||
|
<el-button
|
||||||
|
title="测试连接"
|
||||||
|
:loading="props.testConnBtnLoading && scope.$index == state.idx"
|
||||||
|
:disabled="props.testConnBtnLoading"
|
||||||
|
class="ml1"
|
||||||
|
type="success"
|
||||||
|
@click="testConn(scope.row, scope.$index)"
|
||||||
|
icon="Link"
|
||||||
|
link
|
||||||
|
></el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="name" label="名称" show-overflow-tooltip min-width="100px"> </el-table-column>
|
||||||
|
<el-table-column prop="username" label="用户名" min-width="120px" show-overflow-tooltip> </el-table-column>
|
||||||
|
<el-table-column prop="ciphertextType" label="密文类型" width="100px">
|
||||||
|
<template #default="scope">
|
||||||
|
<EnumTag :value="scope.row.ciphertextType" :enums="AuthCertCiphertextTypeEnum" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="type" label="凭证类型" width="100px">
|
||||||
|
<template #default="scope">
|
||||||
|
<EnumTag :value="scope.row.type" :enums="AuthCertTypeEnum" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="remark" label="备注" show-overflow-tooltip width="120px"> </el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<ResourceAuthCertEdit
|
||||||
|
v-model:visible="state.dvisible"
|
||||||
|
:auth-cert="state.form"
|
||||||
|
@confirm="btnOk"
|
||||||
|
@cancel="cancelEdit"
|
||||||
|
:disable-type="[AuthCertTypeEnum.Public.value]"
|
||||||
|
:disable-ciphertext-type="props.disableCiphertextType"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted, reactive } from 'vue';
|
||||||
|
import { AuthCertTypeEnum, AuthCertCiphertextTypeEnum } from '../tag/enums';
|
||||||
|
import { resourceAuthCertApi } from '../tag/api';
|
||||||
|
import EnumTag from '@/components/enumtag/EnumTag.vue';
|
||||||
|
import ResourceAuthCertEdit from './ResourceAuthCertEdit.vue';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
resourceType: { type: Number },
|
||||||
|
resourceCode: { type: String },
|
||||||
|
disableCiphertextType: {
|
||||||
|
type: Array,
|
||||||
|
},
|
||||||
|
testConnBtnLoading: { type: Boolean },
|
||||||
|
});
|
||||||
|
|
||||||
|
const authCerts = defineModel<any>('modelValue', { required: true, default: [] });
|
||||||
|
const emit = defineEmits(['testConn']);
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
dvisible: false,
|
||||||
|
params: [] as any,
|
||||||
|
form: {},
|
||||||
|
idx: -1,
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getAuthCerts();
|
||||||
|
});
|
||||||
|
|
||||||
|
const getAuthCerts = async () => {
|
||||||
|
if (!props.resourceCode || !props.resourceType) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const res = await resourceAuthCertApi.listByQuery.request({
|
||||||
|
resourceCode: props.resourceCode,
|
||||||
|
resourceType: props.resourceType,
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 100,
|
||||||
|
});
|
||||||
|
authCerts.value = res.list?.reverse() || [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const testConn = async (row: any, idx: number) => {
|
||||||
|
state.idx = idx;
|
||||||
|
emit('testConn', row);
|
||||||
|
};
|
||||||
|
|
||||||
|
const edit = (form: any, idx = -1) => {
|
||||||
|
state.idx = idx;
|
||||||
|
if (form) {
|
||||||
|
state.form = form;
|
||||||
|
} else {
|
||||||
|
state.form = { ciphertextType: AuthCertCiphertextTypeEnum.Password.value, type: AuthCertTypeEnum.Private.value, extra: {} };
|
||||||
|
}
|
||||||
|
state.dvisible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteRow = (idx: any) => {
|
||||||
|
authCerts.value.splice(idx, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelEdit = () => {
|
||||||
|
state.dvisible = false;
|
||||||
|
setTimeout(() => {
|
||||||
|
state.form = {};
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
const btnOk = async (authCert: any) => {
|
||||||
|
const isEdit = authCert.id;
|
||||||
|
if (!isEdit) {
|
||||||
|
const res = await resourceAuthCertApi.listByQuery.request({
|
||||||
|
name: authCert.name,
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 100,
|
||||||
|
});
|
||||||
|
if (res.total) {
|
||||||
|
ElMessage.error('该授权凭证名称已存在');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEdit || state.idx >= 0) {
|
||||||
|
authCerts.value[state.idx] = authCert;
|
||||||
|
cancelEdit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authCerts.value?.filter((x: any) => x.username == authCert.username || x.name == authCert.name).length > 0) {
|
||||||
|
ElMessage.error('该名称或用户名已存在于该账号列表中');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
authCerts.value.push(authCert);
|
||||||
|
cancelEdit();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss"></style>
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
<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>
|
|
||||||
33
mayfly_go_web/src/views/ops/component/ResourceTags.vue
Normal file
33
mayfly_go_web/src/views/ops/component/ResourceTags.vue
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="props.tags">
|
||||||
|
<el-row v-for="(tag, idx) in props.tags?.slice(0, 1)" :key="idx">
|
||||||
|
<TagInfo :tag-path="tag.tagPath" />
|
||||||
|
<span class="ml3">{{ tag.tagPath }}</span>
|
||||||
|
|
||||||
|
<!-- 展示剩余的标签信息 -->
|
||||||
|
<el-popover :show-after="300" v-if="props.tags.length > 1 && idx == 0" placement="top-start" width="230" trigger="hover">
|
||||||
|
<template #reference>
|
||||||
|
<SvgIcon class="mt5 ml5" color="var(--el-color-primary)" name="MoreFilled" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-row v-for="i in props.tags.slice(1)" :key="i">
|
||||||
|
<TagInfo :tag-path="i.tagPath" />
|
||||||
|
<span class="ml3">{{ i.tagPath }}</span>
|
||||||
|
</el-row>
|
||||||
|
</el-popover>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||||
|
import TagInfo from './TagInfo.vue';
|
||||||
|
const props = defineProps({
|
||||||
|
tags: {
|
||||||
|
type: [Array<any>],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss"></style>
|
||||||
@@ -46,7 +46,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
const getSshTunnelMachines = async () => {
|
const getSshTunnelMachines = async () => {
|
||||||
if (state.sshTunnelMachineList.length == 0) {
|
if (state.sshTunnelMachineList.length == 0) {
|
||||||
const res = await machineApi.list.request({ pageNum: 1, pageSize: 100 });
|
const res = await machineApi.list.request({ pageNum: 1, pageSize: 100, ssh: 1 });
|
||||||
state.sshTunnelMachineList = res.list;
|
state.sshTunnelMachineList = res.list;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="tag-tree card pd5">
|
<div class="card pd5">
|
||||||
<el-scrollbar>
|
<el-input v-model="filterText" placeholder="输入关键字->搜索已展开节点信息" clearable size="small" class="mb5 w100" />
|
||||||
<el-input v-model="filterText" placeholder="输入关键字->搜索已展开节点信息" clearable size="small" class="mb5 w100" />
|
<el-scrollbar class="tag-tree">
|
||||||
<el-tree
|
<el-tree
|
||||||
ref="treeRef"
|
ref="treeRef"
|
||||||
:highlight-current="true"
|
:highlight-current="true"
|
||||||
@@ -10,14 +10,14 @@
|
|||||||
:props="treeProps"
|
:props="treeProps"
|
||||||
lazy
|
lazy
|
||||||
node-key="key"
|
node-key="key"
|
||||||
:expand-on-click-node="true"
|
:expand-on-click-node="false"
|
||||||
:filter-node-method="filterNode"
|
:filter-node-method="filterNode"
|
||||||
@node-click="treeNodeClick"
|
@node-click="treeNodeClick"
|
||||||
@node-expand="treeNodeClick"
|
@node-expand="treeNodeClick"
|
||||||
@node-contextmenu="nodeContextmenu"
|
@node-contextmenu="nodeContextmenu"
|
||||||
>
|
>
|
||||||
<template #default="{ node, data }">
|
<template #default="{ node, data }">
|
||||||
<span>
|
<span @dblclick="treeNodeDblclick(data)" :class="data.type.nodeDblclickFunc ? 'none-select' : ''">
|
||||||
<span v-if="data.type.value == TagTreeNode.TagPath">
|
<span v-if="data.type.value == TagTreeNode.TagPath">
|
||||||
<tag-info :tag-path="data.label" />
|
<tag-info :tag-path="data.label" />
|
||||||
</span>
|
</span>
|
||||||
@@ -25,7 +25,13 @@
|
|||||||
<slot v-else :node="node" :data="data" name="prefix"></slot>
|
<slot v-else :node="node" :data="data" name="prefix"></slot>
|
||||||
|
|
||||||
<span class="ml3" :title="data.labelRemark">
|
<span class="ml3" :title="data.labelRemark">
|
||||||
<slot name="label" :data="data"> {{ data.label }}</slot>
|
<slot name="label" :data="data" v-if="!data.disabled"> {{ data.label }}</slot>
|
||||||
|
<!-- 禁用状态 -->
|
||||||
|
<slot name="disabledLabel" :data="data" v-else>
|
||||||
|
<el-link type="danger" disabled :underline="false">
|
||||||
|
{{ `${data.label}` }}
|
||||||
|
</el-link>
|
||||||
|
</slot>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<slot :node="node" :data="data" name="suffix"></slot>
|
<slot :node="node" :data="data" name="suffix"></slot>
|
||||||
@@ -134,16 +140,30 @@ const loadNode = async (node: any, resolve: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const treeNodeClick = (data: any) => {
|
const treeNodeClick = (data: any) => {
|
||||||
emit('nodeClick', data);
|
if (!data.disabled && !data.type.nodeDblclickFunc && data.type.nodeClickFunc) {
|
||||||
if (data.type.nodeClickFunc) {
|
emit('nodeClick', data);
|
||||||
data.type.nodeClickFunc(data);
|
data.type.nodeClickFunc(data);
|
||||||
}
|
}
|
||||||
// 关闭可能存在的右击菜单
|
// 关闭可能存在的右击菜单
|
||||||
contextmenuRef.value.closeContextmenu();
|
contextmenuRef.value.closeContextmenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 树节点双击事件
|
||||||
|
const treeNodeDblclick = (data: any) => {
|
||||||
|
// emit('nodeDblick', data);
|
||||||
|
if (!data.disabled && data.type.nodeDblclickFunc) {
|
||||||
|
data.type.nodeDblclickFunc(data);
|
||||||
|
}
|
||||||
|
// 关闭可能存在的右击菜单
|
||||||
|
contextmenuRef.value.closeContextmenu();
|
||||||
|
};
|
||||||
|
|
||||||
// 树节点右击事件
|
// 树节点右击事件
|
||||||
const nodeContextmenu = (event: any, data: any) => {
|
const nodeContextmenu = (event: any, data: any) => {
|
||||||
|
if (data.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 加载当前节点是否需要显示右击菜单
|
// 加载当前节点是否需要显示右击菜单
|
||||||
let items = data.type.contextMenuItems;
|
let items = data.type.contextMenuItems;
|
||||||
if (!items || items.length == 0) {
|
if (!items || items.length == 0) {
|
||||||
@@ -186,7 +206,7 @@ defineExpose({
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.tag-tree {
|
.tag-tree {
|
||||||
height: calc(100vh - 108px);
|
height: calc(100vh - 148px);
|
||||||
|
|
||||||
.el-tree {
|
.el-tree {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|||||||
@@ -14,6 +14,9 @@
|
|||||||
v-model="modelValue"
|
v-model="modelValue"
|
||||||
@change="changeNode"
|
@change="changeNode"
|
||||||
>
|
>
|
||||||
|
<template #prefix="{ node, data }">
|
||||||
|
<slot name="iconPrefix" :node="node" :data="data" />
|
||||||
|
</template>
|
||||||
<template #default="{ node, data }">
|
<template #default="{ node, data }">
|
||||||
<span>
|
<span>
|
||||||
<span v-if="data.type.value == TagTreeNode.TagPath">
|
<span v-if="data.type.value == TagTreeNode.TagPath">
|
||||||
@@ -33,7 +36,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, reactive, ref, watch, toRefs } from 'vue';
|
import { onMounted, reactive, ref, toRefs, watch } from 'vue';
|
||||||
import { NodeType, TagTreeNode } from './tag';
|
import { NodeType, TagTreeNode } from './tag';
|
||||||
import TagInfo from './TagInfo.vue';
|
import TagInfo from './TagInfo.vue';
|
||||||
import { tagApi } from '../tag/api';
|
import { tagApi } from '../tag/api';
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
<div>
|
<div>
|
||||||
<el-tree-select
|
<el-tree-select
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
v-model="selectTags"
|
v-model="state.selectTags"
|
||||||
@change="changeTag"
|
@change="changeTag"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
:data="tags"
|
:data="tags"
|
||||||
placeholder="请选择关联标签"
|
placeholder="请选择关联标签"
|
||||||
:render-after-expand="true"
|
:render-after-expand="true"
|
||||||
:default-expanded-keys="[selectTags]"
|
:default-expanded-keys="[state.selectTags]"
|
||||||
show-checkbox
|
show-checkbox
|
||||||
node-key="id"
|
node-key="id"
|
||||||
:props="{
|
:props="{
|
||||||
@@ -40,35 +40,25 @@ import { tagApi } from '../tag/api';
|
|||||||
const emit = defineEmits(['update:modelValue', 'changeTag', 'input']);
|
const emit = defineEmits(['update:modelValue', 'changeTag', 'input']);
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
resourceCode: {
|
selectTags: {
|
||||||
type: [String],
|
type: [Array<any>],
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
resourceType: {
|
|
||||||
type: [Number],
|
|
||||||
required: true,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
tags: [],
|
tags: [],
|
||||||
// 单选则为id,多选为id数组
|
// 单选则为id,多选为id数组
|
||||||
selectTags: [],
|
selectTags: [] as any,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { tags, selectTags } = toRefs(state);
|
const { tags } = toRefs(state);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (props.resourceCode) {
|
if (props.selectTags) {
|
||||||
const resourceTags = await tagApi.getTagResources.request({
|
state.selectTags = props.selectTags;
|
||||||
resourceCode: props.resourceCode,
|
|
||||||
resourceType: props.resourceType,
|
|
||||||
});
|
|
||||||
state.selectTags = resourceTags.map((x: any) => x.tagId);
|
|
||||||
changeTag();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
state.tags = await tagApi.getTagTrees.request(null);
|
state.tags = await tagApi.getTagTrees.request({ type: -1 });
|
||||||
});
|
});
|
||||||
|
|
||||||
const changeTag = () => {
|
const changeTag = () => {
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ export class TagTreeNode {
|
|||||||
*/
|
*/
|
||||||
isLeaf: boolean = false;
|
isLeaf: boolean = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否禁用状态
|
||||||
|
*/
|
||||||
|
disabled: boolean = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 额外需要传递的参数
|
* 额外需要传递的参数
|
||||||
*/
|
*/
|
||||||
@@ -53,6 +58,11 @@ export class TagTreeNode {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
withDisabled(disabled: boolean) {
|
||||||
|
this.disabled = disabled;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
withParams(params: any) {
|
withParams(params: any) {
|
||||||
this.params = params;
|
this.params = params;
|
||||||
return this;
|
return this;
|
||||||
@@ -91,8 +101,14 @@ export class NodeType {
|
|||||||
|
|
||||||
loadNodesFunc: (parentNode: TagTreeNode) => Promise<TagTreeNode[]>;
|
loadNodesFunc: (parentNode: TagTreeNode) => Promise<TagTreeNode[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 节点点击事件
|
||||||
|
*/
|
||||||
nodeClickFunc: (node: TagTreeNode) => void;
|
nodeClickFunc: (node: TagTreeNode) => void;
|
||||||
|
|
||||||
|
// 节点双击事件
|
||||||
|
nodeDblclickFunc: (node: TagTreeNode) => void;
|
||||||
|
|
||||||
constructor(value: number) {
|
constructor(value: number) {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
}
|
}
|
||||||
@@ -117,6 +133,16 @@ export class NodeType {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 赋值节点双击事件回调函数
|
||||||
|
* @param func 节点双击事件回调函数
|
||||||
|
* @returns this
|
||||||
|
*/
|
||||||
|
withNodeDblclickFunc(func: (node: TagTreeNode) => void) {
|
||||||
|
this.nodeDblclickFunc = func;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 赋值右击菜单按钮选项
|
* 赋值右击菜单按钮选项
|
||||||
* @param contextMenuItems 右击菜单按钮选项
|
* @param contextMenuItems 右击菜单按钮选项
|
||||||
|
|||||||
@@ -23,13 +23,16 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item prop="name" label="任务名称">
|
<el-form-item prop="name" label="任务名称">
|
||||||
<el-input v-model.number="state.form.name" type="text" placeholder="任务名称"></el-input>
|
<el-input v-model="state.form.name" type="text" placeholder="任务名称"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item prop="startTime" label="开始时间">
|
<el-form-item prop="startTime" label="开始时间">
|
||||||
<el-date-picker v-model="state.form.startTime" type="datetime" placeholder="开始时间" />
|
<el-date-picker v-model="state.form.startTime" type="datetime" placeholder="开始时间" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item prop="intervalDay" label="备份周期">
|
<el-form-item prop="intervalDay" label="备份周期(天)">
|
||||||
<el-input v-model.number="state.form.intervalDay" type="number" placeholder="备份周期(单位:天)"></el-input>
|
<el-input v-model.number="state.form.intervalDay" type="number" placeholder="单位:天"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="maxSaveDays" label="备份历史保留天数">
|
||||||
|
<el-input v-model.number="state.form.maxSaveDays" type="number" placeholder="0: 永久保留"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
@@ -92,6 +95,14 @@ const rules = {
|
|||||||
trigger: ['change', 'blur'],
|
trigger: ['change', 'blur'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
maxSaveDays: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
pattern: /^[0-9]\d*$/,
|
||||||
|
message: '请输入非负整数',
|
||||||
|
trigger: ['change', 'blur'],
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const backupForm: any = ref(null);
|
const backupForm: any = ref(null);
|
||||||
@@ -101,10 +112,11 @@ const state = reactive({
|
|||||||
id: 0,
|
id: 0,
|
||||||
dbId: 0,
|
dbId: 0,
|
||||||
dbNames: '',
|
dbNames: '',
|
||||||
name: null as any,
|
name: '',
|
||||||
intervalDay: null,
|
intervalDay: 1,
|
||||||
startTime: null as any,
|
startTime: null as any,
|
||||||
repeated: null as any,
|
repeated: true,
|
||||||
|
maxSaveDays: 0,
|
||||||
},
|
},
|
||||||
btnLoading: false,
|
btnLoading: false,
|
||||||
dbNamesSelected: [] as any,
|
dbNamesSelected: [] as any,
|
||||||
@@ -137,12 +149,14 @@ const init = (data: any) => {
|
|||||||
state.form.name = data.name;
|
state.form.name = data.name;
|
||||||
state.form.intervalDay = data.intervalDay;
|
state.form.intervalDay = data.intervalDay;
|
||||||
state.form.startTime = data.startTime;
|
state.form.startTime = data.startTime;
|
||||||
|
state.form.maxSaveDays = data.maxSaveDays;
|
||||||
} else {
|
} else {
|
||||||
state.editOrCreate = false;
|
state.editOrCreate = false;
|
||||||
state.form.name = '';
|
state.form.name = '';
|
||||||
state.form.intervalDay = null;
|
state.form.intervalDay = 1;
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
state.form.startTime = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
|
state.form.startTime = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
|
||||||
|
state.form.maxSaveDays = 0;
|
||||||
getDbNamesWithoutBackup();
|
getDbNamesWithoutBackup();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
155
mayfly_go_web/src/views/ops/db/DbBackupHistoryList.vue
Normal file
155
mayfly_go_web/src/views/ops/db/DbBackupHistoryList.vue
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
<template>
|
||||||
|
<div class="db-backup-history">
|
||||||
|
<page-table
|
||||||
|
height="100%"
|
||||||
|
ref="pageTableRef"
|
||||||
|
:page-api="dbApi.getDbBackupHistories"
|
||||||
|
:show-selection="true"
|
||||||
|
v-model:selection-data="state.selectedData"
|
||||||
|
:searchItems="searchItems"
|
||||||
|
:before-query-fn="beforeQueryFn"
|
||||||
|
v-model:query-form="query"
|
||||||
|
:columns="columns"
|
||||||
|
>
|
||||||
|
<template #dbSelect>
|
||||||
|
<el-select v-model="query.dbName" placeholder="请选择数据库" style="width: 200px" filterable clearable>
|
||||||
|
<el-option v-for="item in props.dbNames" :key="item" :label="`${item}`" :value="item"> </el-option>
|
||||||
|
</el-select>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #tableHeader>
|
||||||
|
<el-button type="primary" icon="back" @click="restoreDbBackupHistory(null)">立即恢复</el-button>
|
||||||
|
<el-button type="danger" icon="delete" @click="deleteDbBackupHistory(null)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #action="{ data }">
|
||||||
|
<div>
|
||||||
|
<el-button @click="restoreDbBackupHistory(data)" type="primary" link>立即恢复</el-button>
|
||||||
|
<el-button @click="deleteDbBackupHistory(data)" type="danger" link>删除</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</page-table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { toRefs, reactive, Ref, ref } from 'vue';
|
||||||
|
import { dbApi } from './api';
|
||||||
|
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||||
|
import { TableColumn } from '@/components/pagetable';
|
||||||
|
import { SearchItem } from '@/components/SearchForm';
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
|
||||||
|
const pageTableRef: Ref<any> = ref(null);
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
dbId: {
|
||||||
|
type: [Number],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
dbNames: {
|
||||||
|
type: [Array<String>],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchItems = [SearchItem.slot('dbName', '数据库名称', 'dbSelect')];
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
TableColumn.new('dbName', '数据库名称'),
|
||||||
|
TableColumn.new('name', '备份名称'),
|
||||||
|
TableColumn.new('createTime', '创建时间').isTime(),
|
||||||
|
TableColumn.new('lastResult', '恢复结果'),
|
||||||
|
TableColumn.new('lastTime', '恢复时间').isTime(),
|
||||||
|
TableColumn.new('action', '操作').isSlot().setMinWidth(160).fixedRight(),
|
||||||
|
];
|
||||||
|
|
||||||
|
const emptyQuery = {
|
||||||
|
dbId: 0,
|
||||||
|
dbName: '',
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
data: [],
|
||||||
|
total: 0,
|
||||||
|
query: emptyQuery,
|
||||||
|
/**
|
||||||
|
* 选中的数据
|
||||||
|
*/
|
||||||
|
selectedData: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { query } = toRefs(state);
|
||||||
|
|
||||||
|
const beforeQueryFn = (query: any) => {
|
||||||
|
query.dbId = props.dbId;
|
||||||
|
return query;
|
||||||
|
};
|
||||||
|
|
||||||
|
const search = async () => {
|
||||||
|
await pageTableRef.value.search();
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteDbBackupHistory = async (data: any) => {
|
||||||
|
let backupHistoryId: string;
|
||||||
|
if (data) {
|
||||||
|
backupHistoryId = data.id;
|
||||||
|
} else if (state.selectedData.length > 0) {
|
||||||
|
backupHistoryId = state.selectedData.map((x: any) => x.id).join(' ');
|
||||||
|
} else {
|
||||||
|
ElMessage.error('请选择需要删除的数据库备份历史');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await ElMessageBox.confirm(`确定删除 “数据库备份历史” 吗?`, '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
});
|
||||||
|
await dbApi.deleteDbBackupHistory.request({ dbId: props.dbId, backupHistoryId: backupHistoryId });
|
||||||
|
await search();
|
||||||
|
ElMessage.success('删除成功');
|
||||||
|
};
|
||||||
|
|
||||||
|
const restoreDbBackupHistory = async (data: any) => {
|
||||||
|
let backupHistoryId: string;
|
||||||
|
if (data) {
|
||||||
|
backupHistoryId = data.id;
|
||||||
|
} else if (state.selectedData.length > 0) {
|
||||||
|
const pluralDbNames: string[] = [];
|
||||||
|
const dbNames: Map<string, boolean> = new Map();
|
||||||
|
state.selectedData.forEach((item: any) => {
|
||||||
|
if (!dbNames.has(item.dbName)) {
|
||||||
|
dbNames.set(item.dbName, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!dbNames.get(item.dbName)) {
|
||||||
|
dbNames.set(item.dbName, true);
|
||||||
|
pluralDbNames.push(item.dbName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (pluralDbNames.length > 0) {
|
||||||
|
ElMessage.error('多次选择相同数据库:' + pluralDbNames.join(', '));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
backupHistoryId = state.selectedData.map((x: any) => x.id).join(' ');
|
||||||
|
} else {
|
||||||
|
ElMessage.error('请选择需要恢复的数据库备份历史');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await ElMessageBox.confirm(`确定从 “数据库备份历史” 中恢复数据库吗?`, '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
});
|
||||||
|
|
||||||
|
await dbApi.restoreDbBackupHistory.request({
|
||||||
|
dbId: props.dbId,
|
||||||
|
backupHistoryId: backupHistoryId,
|
||||||
|
});
|
||||||
|
await search();
|
||||||
|
ElMessage.success('成功创建数据库恢复任务');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss"></style>
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
<el-button type="primary" icon="plus" @click="createDbBackup()">添加</el-button>
|
<el-button type="primary" icon="plus" @click="createDbBackup()">添加</el-button>
|
||||||
<el-button type="primary" icon="video-play" @click="enableDbBackup(null)">启用</el-button>
|
<el-button type="primary" icon="video-play" @click="enableDbBackup(null)">启用</el-button>
|
||||||
<el-button type="primary" icon="video-pause" @click="disableDbBackup(null)">禁用</el-button>
|
<el-button type="primary" icon="video-pause" @click="disableDbBackup(null)">禁用</el-button>
|
||||||
|
<el-button type="danger" icon="delete" @click="deleteDbBackup(null)">删除</el-button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #action="{ data }">
|
<template #action="{ data }">
|
||||||
@@ -29,6 +30,7 @@
|
|||||||
<el-button v-if="!data.enabled" @click="enableDbBackup(data)" type="primary" link>启用</el-button>
|
<el-button v-if="!data.enabled" @click="enableDbBackup(data)" type="primary" link>启用</el-button>
|
||||||
<el-button v-if="data.enabled" @click="disableDbBackup(data)" type="primary" link>禁用</el-button>
|
<el-button v-if="data.enabled" @click="disableDbBackup(data)" type="primary" link>禁用</el-button>
|
||||||
<el-button v-if="data.enabled" @click="startDbBackup(data)" type="primary" link>立即备份</el-button>
|
<el-button v-if="data.enabled" @click="startDbBackup(data)" type="primary" link>立即备份</el-button>
|
||||||
|
<el-button @click="deleteDbBackup(data)" type="danger" link>删除</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</page-table>
|
</page-table>
|
||||||
@@ -49,7 +51,7 @@ import { dbApi } from './api';
|
|||||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||||
import { TableColumn } from '@/components/pagetable';
|
import { TableColumn } from '@/components/pagetable';
|
||||||
import { SearchItem } from '@/components/SearchForm';
|
import { SearchItem } from '@/components/SearchForm';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
|
||||||
const DbBackupEdit = defineAsyncComponent(() => import('./DbBackupEdit.vue'));
|
const DbBackupEdit = defineAsyncComponent(() => import('./DbBackupEdit.vue'));
|
||||||
const pageTableRef: Ref<any> = ref(null);
|
const pageTableRef: Ref<any> = ref(null);
|
||||||
@@ -72,10 +74,10 @@ const columns = [
|
|||||||
TableColumn.new('name', '任务名称'),
|
TableColumn.new('name', '任务名称'),
|
||||||
TableColumn.new('startTime', '启动时间').isTime(),
|
TableColumn.new('startTime', '启动时间').isTime(),
|
||||||
TableColumn.new('intervalDay', '备份周期'),
|
TableColumn.new('intervalDay', '备份周期'),
|
||||||
TableColumn.new('enabled', '是否启用'),
|
TableColumn.new('enabledDesc', '是否启用'),
|
||||||
TableColumn.new('lastResult', '执行结果'),
|
TableColumn.new('lastResult', '执行结果'),
|
||||||
TableColumn.new('lastTime', '执行时间').isTime(),
|
TableColumn.new('lastTime', '执行时间').isTime(),
|
||||||
TableColumn.new('action', '操作').isSlot().setMinWidth(180).fixedRight(),
|
TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight(),
|
||||||
];
|
];
|
||||||
|
|
||||||
const emptyQuery = {
|
const emptyQuery = {
|
||||||
@@ -168,5 +170,25 @@ const startDbBackup = async (data: any) => {
|
|||||||
await search();
|
await search();
|
||||||
ElMessage.success('备份任务启动成功');
|
ElMessage.success('备份任务启动成功');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deleteDbBackup = async (data: any) => {
|
||||||
|
let backupId: string;
|
||||||
|
if (data) {
|
||||||
|
backupId = data.id;
|
||||||
|
} else if (state.selectedData.length > 0) {
|
||||||
|
backupId = state.selectedData.map((x: any) => x.id).join(' ');
|
||||||
|
} else {
|
||||||
|
ElMessage.error('请选择需要删除的数据库备份任务');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await ElMessageBox.confirm(`确定删除 “数据库备份任务” 吗?`, '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
});
|
||||||
|
await dbApi.deleteDbBackup.request({ dbId: props.dbId, backupId: backupId });
|
||||||
|
await search();
|
||||||
|
ElMessage.success('删除成功');
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss"></style>
|
<style lang="scss"></style>
|
||||||
|
|||||||
@@ -19,8 +19,7 @@
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
multiple
|
multiple
|
||||||
:resource-code="form.code"
|
:select-tags="form.tagId"
|
||||||
:resource-type="TagResourceTypeEnum.Db.value"
|
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -31,13 +30,14 @@
|
|||||||
remote
|
remote
|
||||||
:remote-method="getInstances"
|
:remote-method="getInstances"
|
||||||
@change="changeInstance"
|
@change="changeInstance"
|
||||||
v-model="form.instanceId"
|
v-model="state.selectInstalce"
|
||||||
|
value-key="id"
|
||||||
placeholder="请输入实例名称搜索并选择实例"
|
placeholder="请输入实例名称搜索并选择实例"
|
||||||
filterable
|
filterable
|
||||||
clearable
|
clearable
|
||||||
class="w100"
|
class="w100"
|
||||||
>
|
>
|
||||||
<el-option v-for="item in state.instances" :key="item.id" :label="`${item.name}`" :value="item.id">
|
<el-option v-for="item in state.instances" :key="item.id" :label="`${item.name}`" :value="item">
|
||||||
{{ item.name }}
|
{{ item.name }}
|
||||||
<el-divider direction="vertical" border-style="dashed" />
|
<el-divider direction="vertical" border-style="dashed" />
|
||||||
|
|
||||||
@@ -48,6 +48,26 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item prop="authCertName" label="授权凭证" required>
|
||||||
|
<el-select @focus="getAuthCerts" @change="changeAuthCert" v-model="form.authCertName" placeholder="请选择授权凭证" filterable>
|
||||||
|
<el-option v-for="item in state.authCerts" :key="item.id" :label="`${item.name}`" :value="item.name">
|
||||||
|
{{ item.name }}
|
||||||
|
|
||||||
|
<el-divider direction="vertical" border-style="dashed" />
|
||||||
|
{{ item.username }}
|
||||||
|
|
||||||
|
<el-divider direction="vertical" border-style="dashed" />
|
||||||
|
<EnumTag :value="item.ciphertextType" :enums="AuthCertCiphertextTypeEnum" />
|
||||||
|
|
||||||
|
<el-divider direction="vertical" border-style="dashed" />
|
||||||
|
{{ item.remark }}
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item prop="code" label="编号" required>
|
||||||
|
<el-input :disabled="form.id" v-model.trim="form.code" placeholder="请输入编号 (数字字母下划线), 不可修改" auto-complete="off"></el-input>
|
||||||
|
</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-input v-model.trim="form.name" placeholder="请输入数据库别名" auto-complete="off"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -75,6 +95,8 @@
|
|||||||
<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-input v-model.trim="form.remark" auto-complete="off" type="textarea"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
|
<procdef-select-form-item v-model="form.flowProcdefKey" />
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -92,8 +114,14 @@ import { toRefs, reactive, watch, ref } from 'vue';
|
|||||||
import { dbApi } from './api';
|
import { dbApi } from './api';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
import TagTreeSelect from '../component/TagTreeSelect.vue';
|
import TagTreeSelect from '../component/TagTreeSelect.vue';
|
||||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
|
||||||
import type { CheckboxValueType } from 'element-plus';
|
import type { CheckboxValueType } from 'element-plus';
|
||||||
|
import ProcdefSelectFormItem from '@/views/flow/components/ProcdefSelectFormItem.vue';
|
||||||
|
import { DbType } from '@/views/ops/db/dialect';
|
||||||
|
import { ResourceCodePattern } from '@/common/pattern';
|
||||||
|
import { resourceAuthCertApi } from '../tag/api';
|
||||||
|
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||||
|
import EnumTag from '@/components/enumtag/EnumTag.vue';
|
||||||
|
import { AuthCertCiphertextTypeEnum } from '../tag/enums';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
visible: {
|
visible: {
|
||||||
@@ -126,7 +154,18 @@ const rules = {
|
|||||||
trigger: ['change', 'blur'],
|
trigger: ['change', 'blur'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
code: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请输入编码',
|
||||||
|
trigger: ['change', 'blur'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: ResourceCodePattern.pattern,
|
||||||
|
message: ResourceCodePattern.message,
|
||||||
|
trigger: ['blur'],
|
||||||
|
},
|
||||||
|
],
|
||||||
name: [
|
name: [
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
@@ -155,6 +194,8 @@ const state = reactive({
|
|||||||
dbNamesSelected: [] as any,
|
dbNamesSelected: [] as any,
|
||||||
dbNamesFiltered: [] as any,
|
dbNamesFiltered: [] as any,
|
||||||
filterString: '',
|
filterString: '',
|
||||||
|
selectInstalce: {} as any,
|
||||||
|
authCerts: [] as any,
|
||||||
form: {
|
form: {
|
||||||
id: null,
|
id: null,
|
||||||
tagId: [],
|
tagId: [],
|
||||||
@@ -163,6 +204,8 @@ const state = reactive({
|
|||||||
database: '',
|
database: '',
|
||||||
remark: '',
|
remark: '',
|
||||||
instanceId: null as any,
|
instanceId: null as any,
|
||||||
|
authCertName: '',
|
||||||
|
flowProcdefKey: '',
|
||||||
},
|
},
|
||||||
instances: [] as any,
|
instances: [] as any,
|
||||||
});
|
});
|
||||||
@@ -178,7 +221,7 @@ watch(props, async (newValue: any) => {
|
|||||||
}
|
}
|
||||||
if (newValue.db) {
|
if (newValue.db) {
|
||||||
state.form = { ...newValue.db };
|
state.form = { ...newValue.db };
|
||||||
|
state.form.tagId = newValue.db.tags.map((t: any) => t.tagId);
|
||||||
// 将数据库名使用空格切割,获取所有数据库列表
|
// 将数据库名使用空格切割,获取所有数据库列表
|
||||||
state.dbNamesSelected = newValue.db.database.split(' ');
|
state.dbNamesSelected = newValue.db.database.split(' ');
|
||||||
} else {
|
} else {
|
||||||
@@ -187,14 +230,34 @@ watch(props, async (newValue: any) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const changeInstance = () => {
|
const changeInstance = async () => {
|
||||||
state.dbNamesSelected = [];
|
state.dbNamesSelected = [];
|
||||||
getAllDatabase();
|
state.form.instanceId = state.selectInstalce.id;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAllDatabase = async () => {
|
const getAuthCerts = async () => {
|
||||||
|
const res = await resourceAuthCertApi.listByQuery.request({
|
||||||
|
resourceCode: state.selectInstalce.code,
|
||||||
|
resourceType: TagResourceTypeEnum.Db.value,
|
||||||
|
pageSize: 100,
|
||||||
|
});
|
||||||
|
state.authCerts = res.list || [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeAuthCert = (val: string) => {
|
||||||
|
getAllDatabase(val);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAllDatabase = async (authCertName: string) => {
|
||||||
if (state.form.instanceId > 0) {
|
if (state.form.instanceId > 0) {
|
||||||
state.allDatabases = await dbApi.getAllDatabase.request({ instanceId: state.form.instanceId });
|
let dbs = await dbApi.getAllDatabase.request({ instanceId: state.form.instanceId, authCertName });
|
||||||
|
state.allDatabases = dbs;
|
||||||
|
|
||||||
|
// 如果是oracle,且没查出数据库列表,则取实例sid
|
||||||
|
let instance = state.instances.find((item: any) => item.id === state.form.instanceId);
|
||||||
|
if (instance && instance.type === DbType.oracle && dbs.length === 0) {
|
||||||
|
state.allDatabases = [instance.sid];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -213,8 +276,9 @@ const open = async () => {
|
|||||||
if (state.form.instanceId) {
|
if (state.form.instanceId) {
|
||||||
// 根据id获取,因为需要回显实例名称
|
// 根据id获取,因为需要回显实例名称
|
||||||
await getInstances('', state.form.instanceId);
|
await getInstances('', state.form.instanceId);
|
||||||
|
state.selectInstalce = state.instances[0];
|
||||||
|
await getAllDatabase(state.form.authCertName);
|
||||||
}
|
}
|
||||||
await getAllDatabase();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const btnOk = async () => {
|
const btnOk = async () => {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
:show-selection="true"
|
:show-selection="true"
|
||||||
v-model:selection-data="state.selectionData"
|
v-model:selection-data="state.selectionData"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
|
lazy
|
||||||
>
|
>
|
||||||
<template #instanceSelect>
|
<template #instanceSelect>
|
||||||
<el-select remote :remote-method="getInstances" v-model="query.instanceId" placeholder="输入并选择实例" filterable clearable>
|
<el-select remote :remote-method="getInstances" v-model="query.instanceId" placeholder="输入并选择实例" filterable clearable>
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #tagPath="{ data }">
|
<template #tagPath="{ data }">
|
||||||
<resource-tag :resource-code="data.code" :resource-type="TagResourceTypeEnum.Db.value" />
|
<ResourceTags :tags="data.tags" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #action="{ data }">
|
<template #action="{ data }">
|
||||||
@@ -61,30 +62,43 @@
|
|||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
<el-dropdown-menu>
|
<el-dropdown-menu>
|
||||||
<el-dropdown-item :command="{ type: 'detail', data }"> 详情 </el-dropdown-item>
|
<el-dropdown-item :command="{ type: 'detail', data }"> 详情 </el-dropdown-item>
|
||||||
<el-dropdown-item :command="{ type: 'dumpDb', data }" v-if="supportAction('dumpDb', data.type)"> 导出 </el-dropdown-item>
|
<el-dropdown-item :command="{ type: 'dumpDb', data }"> 导出 </el-dropdown-item>
|
||||||
<el-dropdown-item :command="{ type: 'dbBackup', data }" v-if="supportAction('dbBackup', data.type)"> 备份 </el-dropdown-item>
|
<el-dropdown-item :command="{ type: 'backupDb', data }" v-if="actionBtns[perms.backupDb] && supportAction('backupDb', data.type)">
|
||||||
<el-dropdown-item :command="{ type: 'dbRestore', data }" v-if="supportAction('dbRestore', data.type)"> 恢复 </el-dropdown-item>
|
备份任务
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item
|
||||||
|
:command="{ type: 'backupHistory', data }"
|
||||||
|
v-if="actionBtns[perms.backupDb] && supportAction('backupDb', data.type)"
|
||||||
|
>
|
||||||
|
备份历史
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item
|
||||||
|
:command="{ type: 'restoreDb', data }"
|
||||||
|
v-if="actionBtns[perms.restoreDb] && supportAction('restoreDb', data.type)"
|
||||||
|
>
|
||||||
|
恢复任务
|
||||||
|
</el-dropdown-item>
|
||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
</template>
|
</template>
|
||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
</template>
|
</template>
|
||||||
</page-table>
|
</page-table>
|
||||||
|
|
||||||
<el-dialog width="720px" :title="`${db} 数据库导出`" v-model="exportDialog.visible">
|
<el-dialog width="750px" :title="`${db} 数据库导出`" v-model="exportDialog.visible">
|
||||||
<el-row justify="space-between">
|
<el-row justify="space-between">
|
||||||
<el-col :span="9">
|
<el-col :span="9">
|
||||||
<el-form-item label="导出内容: ">
|
<el-form-item label="导出内容: ">
|
||||||
<el-checkbox-group v-model="exportDialog.contents" :min="1">
|
<el-checkbox-group v-model="exportDialog.contents" :min="1">
|
||||||
<el-checkbox label="结构" />
|
<el-checkbox label="结构" value="结构" />
|
||||||
<el-checkbox label="数据" />
|
<el-checkbox label="数据" value="数据" />
|
||||||
</el-checkbox-group>
|
</el-checkbox-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="9">
|
<el-col :span="9">
|
||||||
<el-form-item label="扩展名: ">
|
<el-form-item label="扩展名: ">
|
||||||
<el-radio-group v-model="exportDialog.extName">
|
<el-radio-group v-model="exportDialog.extName">
|
||||||
<el-radio label="sql" />
|
<el-radio label="sql" value="sql" />
|
||||||
<el-radio label="gzip" />
|
<el-radio label="gzip" value="gzip" />
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
@@ -131,6 +145,16 @@
|
|||||||
<db-backup-list :dbId="dbBackupDialog.dbId" :dbNames="dbBackupDialog.dbs" />
|
<db-backup-list :dbId="dbBackupDialog.dbId" :dbNames="dbBackupDialog.dbs" />
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
width="80%"
|
||||||
|
:title="`${dbBackupHistoryDialog.title} - 数据库备份历史`"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
:destroy-on-close="true"
|
||||||
|
v-model="dbBackupHistoryDialog.visible"
|
||||||
|
>
|
||||||
|
<db-backup-history-list :dbId="dbBackupHistoryDialog.dbId" :dbNames="dbBackupHistoryDialog.dbs" />
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog
|
<el-dialog
|
||||||
width="80%"
|
width="80%"
|
||||||
:title="`${dbRestoreDialog.title} - 数据库恢复`"
|
:title="`${dbRestoreDialog.title} - 数据库恢复`"
|
||||||
@@ -141,27 +165,36 @@
|
|||||||
<db-restore-list :dbId="dbRestoreDialog.dbId" :dbNames="dbRestoreDialog.dbs" />
|
<db-restore-list :dbId="dbRestoreDialog.dbId" :dbNames="dbRestoreDialog.dbs" />
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog v-model="infoDialog.visible" :before-close="onBeforeCloseInfoDialog" :close-on-click-modal="false">
|
<el-dialog v-if="infoDialog.visible" v-model="infoDialog.visible" :before-close="onBeforeCloseInfoDialog">
|
||||||
<el-descriptions title="详情" :column="3" border>
|
<el-descriptions title="详情" :column="3" border>
|
||||||
<!-- <el-descriptions-item :span="3" label="标签路径">{{ infoDialog.data?.tagPath }}</el-descriptions-item> -->
|
|
||||||
<el-descriptions-item :span="2" label="名称">{{ infoDialog.data?.name }}</el-descriptions-item>
|
<el-descriptions-item :span="2" label="名称">{{ infoDialog.data?.name }}</el-descriptions-item>
|
||||||
<el-descriptions-item :span="1" label="id">{{ infoDialog.data?.id }}</el-descriptions-item>
|
<el-descriptions-item :span="1" label="id">{{ infoDialog.data?.id }}</el-descriptions-item>
|
||||||
<el-descriptions-item :span="3" label="数据库">{{ infoDialog.data?.database }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item :span="3" label="备注">{{ infoDialog.data?.remark }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item :span="2" label="创建时间">{{ dateFormat(infoDialog.data?.createTime) }} </el-descriptions-item>
|
|
||||||
<el-descriptions-item :span="1" label="创建者">{{ infoDialog.data?.creator }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item :span="2" label="更新时间">{{ dateFormat(infoDialog.data?.updateTime) }} </el-descriptions-item>
|
|
||||||
<el-descriptions-item :span="1" label="修改者">{{ infoDialog.data?.modifier }}</el-descriptions-item>
|
|
||||||
|
|
||||||
|
<el-descriptions-item :span="3" label="关联标签"><ResourceTags :tags="infoDialog.data.tags" /></el-descriptions-item>
|
||||||
<el-descriptions-item :span="3" label="数据库实例名称">{{ infoDialog.instance?.name }}</el-descriptions-item>
|
<el-descriptions-item :span="3" label="数据库实例名称">{{ infoDialog.instance?.name }}</el-descriptions-item>
|
||||||
|
|
||||||
<el-descriptions-item :span="2" label="主机">{{ infoDialog.instance?.host }}</el-descriptions-item>
|
<el-descriptions-item :span="2" label="主机">{{ infoDialog.instance?.host }}</el-descriptions-item>
|
||||||
<el-descriptions-item :span="1" label="端口">{{ infoDialog.instance?.port }}</el-descriptions-item>
|
<el-descriptions-item :span="1" label="端口">{{ infoDialog.instance?.port }}</el-descriptions-item>
|
||||||
|
|
||||||
<el-descriptions-item :span="2" label="用户名">{{ infoDialog.instance?.username }}</el-descriptions-item>
|
<el-descriptions-item :span="2" label="用户名">{{ infoDialog.instance?.username }}</el-descriptions-item>
|
||||||
<el-descriptions-item :span="1" label="类型">{{ infoDialog.instance?.type }}</el-descriptions-item>
|
<el-descriptions-item :span="1" label="类型">
|
||||||
|
<SvgIcon :name="getDbDialect(infoDialog.instance?.type).getInfo().icon" :size="20" />{{ infoDialog.instance?.type }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item :span="3" label="数据库">{{ infoDialog.data?.database }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item :span="3" label="备注">{{ infoDialog.data?.remark }}</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item :span="3" label="工单流程key">{{ infoDialog.data?.flowProcdefKey }}</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item :span="2" label="创建时间">{{ dateFormat(infoDialog.data?.createTime) }} </el-descriptions-item>
|
||||||
|
<el-descriptions-item :span="1" label="创建者">{{ infoDialog.data?.creator }}</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item :span="2" label="更新时间">{{ dateFormat(infoDialog.data?.updateTime) }} </el-descriptions-item>
|
||||||
|
<el-descriptions-item :span="1" label="修改者">{{ infoDialog.data?.modifier }}</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<db-edit @val-change="search" :title="dbEditDialog.title" v-model:visible="dbEditDialog.visible" v-model:db="dbEditDialog.data"></db-edit>
|
<db-edit @val-change="search()" :title="dbEditDialog.title" v-model:visible="dbEditDialog.visible" v-model:db="dbEditDialog.data"></db-edit>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -173,7 +206,6 @@ import config from '@/common/config';
|
|||||||
import { joinClientParams } from '@/common/request';
|
import { joinClientParams } from '@/common/request';
|
||||||
import { isTrue } from '@/common/assert';
|
import { isTrue } from '@/common/assert';
|
||||||
import { dateFormat } from '@/common/utils/date';
|
import { dateFormat } from '@/common/utils/date';
|
||||||
import ResourceTag from '../component/ResourceTag.vue';
|
|
||||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||||
import { TableColumn } from '@/components/pagetable';
|
import { TableColumn } from '@/components/pagetable';
|
||||||
import { hasPerms } from '@/components/auth/auth';
|
import { hasPerms } from '@/components/auth/auth';
|
||||||
@@ -185,30 +217,49 @@ import { getDbDialect } from './dialect/index';
|
|||||||
import { getTagPathSearchItem } from '../component/tag';
|
import { getTagPathSearchItem } from '../component/tag';
|
||||||
import { SearchItem } from '@/components/SearchForm';
|
import { SearchItem } from '@/components/SearchForm';
|
||||||
import DbBackupList from './DbBackupList.vue';
|
import DbBackupList from './DbBackupList.vue';
|
||||||
|
import DbBackupHistoryList from './DbBackupHistoryList.vue';
|
||||||
import DbRestoreList from './DbRestoreList.vue';
|
import DbRestoreList from './DbRestoreList.vue';
|
||||||
|
import ResourceTags from '../component/ResourceTags.vue';
|
||||||
|
import { sleep } from '@/common/utils/loading';
|
||||||
|
|
||||||
const DbEdit = defineAsyncComponent(() => import('./DbEdit.vue'));
|
const DbEdit = defineAsyncComponent(() => import('./DbEdit.vue'));
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
lazy: {
|
||||||
|
type: [Boolean],
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const perms = {
|
const perms = {
|
||||||
base: 'db',
|
base: 'db',
|
||||||
saveDb: 'db:save',
|
saveDb: 'db:save',
|
||||||
delDb: 'db:del',
|
delDb: 'db:del',
|
||||||
|
backupDb: 'db:backup',
|
||||||
|
restoreDb: 'db:restore',
|
||||||
};
|
};
|
||||||
|
|
||||||
const searchItems = [getTagPathSearchItem(TagResourceTypeEnum.Db.value), SearchItem.slot('instanceId', '实例', 'instanceSelect')];
|
const searchItems = [
|
||||||
|
getTagPathSearchItem(TagResourceTypeEnum.Db.value),
|
||||||
|
SearchItem.slot('instanceId', '实例', 'instanceSelect'),
|
||||||
|
SearchItem.input('code', '编号'),
|
||||||
|
];
|
||||||
|
|
||||||
const columns = ref([
|
const columns = ref([
|
||||||
|
TableColumn.new('tags[0].tagPath', '关联标签').isSlot('tagPath').setAddWidth(20),
|
||||||
|
TableColumn.new('code', '编号'),
|
||||||
TableColumn.new('name', '名称'),
|
TableColumn.new('name', '名称'),
|
||||||
TableColumn.new('type', '类型').isSlot().setAddWidth(-15).alignCenter(),
|
TableColumn.new('type', '类型').isSlot().setAddWidth(-15).alignCenter(),
|
||||||
TableColumn.new('instanceName', '实例名'),
|
TableColumn.new('instanceName', '实例名'),
|
||||||
TableColumn.new('host', 'ip:port').isSlot().setAddWidth(40),
|
TableColumn.new('host', 'ip:port').isSlot().setAddWidth(40),
|
||||||
TableColumn.new('username', 'username'),
|
TableColumn.new('username', 'username'),
|
||||||
TableColumn.new('tagPath', '关联标签').isSlot().setAddWidth(10).alignCenter(),
|
TableColumn.new('flowProcdefKey', '关联流程'),
|
||||||
TableColumn.new('remark', '备注'),
|
TableColumn.new('remark', '备注'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 该用户拥有的的操作列按钮权限
|
// 该用户拥有的的操作列按钮权限
|
||||||
const actionBtns = hasPerms([perms.base, perms.saveDb]);
|
// const actionBtns = hasPerms([perms.base, perms.saveDb]);
|
||||||
|
const actionBtns = hasPerms(Object.values(perms));
|
||||||
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight().alignCenter();
|
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight().alignCenter();
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -253,6 +304,13 @@ const state = reactive({
|
|||||||
dbs: [],
|
dbs: [],
|
||||||
dbId: 0,
|
dbId: 0,
|
||||||
},
|
},
|
||||||
|
// 数据库备份历史弹框
|
||||||
|
dbBackupHistoryDialog: {
|
||||||
|
title: '',
|
||||||
|
visible: false,
|
||||||
|
dbs: [],
|
||||||
|
dbId: 0,
|
||||||
|
},
|
||||||
// 数据库恢复弹框
|
// 数据库恢复弹框
|
||||||
dbRestoreDialog: {
|
dbRestoreDialog: {
|
||||||
title: '',
|
title: '',
|
||||||
@@ -285,12 +343,16 @@ const state = reactive({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { db, selectionData, query, infoDialog, sqlExecLogDialog, exportDialog, dbEditDialog, dbBackupDialog, dbRestoreDialog } = toRefs(state);
|
const { db, selectionData, query, infoDialog, sqlExecLogDialog, exportDialog, dbEditDialog, dbBackupDialog, dbBackupHistoryDialog, dbRestoreDialog } =
|
||||||
|
toRefs(state);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (Object.keys(actionBtns).length > 0) {
|
if (Object.keys(actionBtns).length > 0) {
|
||||||
columns.value.push(actionColumn);
|
columns.value.push(actionColumn);
|
||||||
}
|
}
|
||||||
|
if (!props.lazy) {
|
||||||
|
search();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const checkRouteTagPath = (query: any) => {
|
const checkRouteTagPath = (query: any) => {
|
||||||
@@ -300,7 +362,10 @@ const checkRouteTagPath = (query: any) => {
|
|||||||
return query;
|
return query;
|
||||||
};
|
};
|
||||||
|
|
||||||
const search = async () => {
|
const search = async (tagPath: string = '') => {
|
||||||
|
if (tagPath) {
|
||||||
|
state.query.tagPath = tagPath;
|
||||||
|
}
|
||||||
pageTableRef.value.search();
|
pageTableRef.value.search();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -345,11 +410,15 @@ const handleMoreActionCommand = (commond: any) => {
|
|||||||
onDumpDbs(data);
|
onDumpDbs(data);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case 'dbBackup': {
|
case 'backupDb': {
|
||||||
onShowDbBackupDialog(data);
|
onShowDbBackupDialog(data);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case 'dbRestore': {
|
case 'backupHistory': {
|
||||||
|
onShowDbBackupHistoryDialog(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'restoreDb': {
|
||||||
onShowDbRestoreDialog(data);
|
onShowDbRestoreDialog(data);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -402,6 +471,13 @@ const onShowDbBackupDialog = async (row: any) => {
|
|||||||
state.dbBackupDialog.visible = true;
|
state.dbBackupDialog.visible = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onShowDbBackupHistoryDialog = async (row: any) => {
|
||||||
|
state.dbBackupHistoryDialog.title = `${row.name}`;
|
||||||
|
state.dbBackupHistoryDialog.dbId = row.id;
|
||||||
|
state.dbBackupHistoryDialog.dbs = row.database.split(' ');
|
||||||
|
state.dbBackupHistoryDialog.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
const onShowDbRestoreDialog = async (row: any) => {
|
const onShowDbRestoreDialog = async (row: any) => {
|
||||||
state.dbRestoreDialog.title = `${row.name}`;
|
state.dbRestoreDialog.title = `${row.name}`;
|
||||||
state.dbRestoreDialog.dbId = row.id;
|
state.dbRestoreDialog.dbId = row.id;
|
||||||
@@ -429,9 +505,8 @@ const onDumpDbs = async (row: any) => {
|
|||||||
/**
|
/**
|
||||||
* 数据库信息导出
|
* 数据库信息导出
|
||||||
*/
|
*/
|
||||||
const dumpDbs = () => {
|
const dumpDbs = async () => {
|
||||||
isTrue(state.exportDialog.value.length > 0, '请添加要导出的数据库');
|
isTrue(state.exportDialog.value.length > 0, '请添加要导出的数据库');
|
||||||
const a = document.createElement('a');
|
|
||||||
let type = 0;
|
let type = 0;
|
||||||
for (let c of state.exportDialog.contents) {
|
for (let c of state.exportDialog.contents) {
|
||||||
if (c == '结构') {
|
if (c == '结构') {
|
||||||
@@ -440,13 +515,15 @@ const dumpDbs = () => {
|
|||||||
type += 2;
|
type += 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
a.setAttribute(
|
for (let db of state.exportDialog.value) {
|
||||||
'href',
|
const a = document.createElement('a');
|
||||||
`${config.baseApiUrl}/dbs/${state.exportDialog.dbId}/dump?db=${state.exportDialog.value.join(',')}&type=${type}&extName=${
|
a.setAttribute(
|
||||||
state.exportDialog.extName
|
'href',
|
||||||
}&${joinClientParams()}`
|
`${config.baseApiUrl}/dbs/${state.exportDialog.dbId}/dump?db=${db}&type=${type}&extName=${state.exportDialog.extName}&${joinClientParams()}`
|
||||||
);
|
);
|
||||||
a.click();
|
a.click();
|
||||||
|
await sleep(500);
|
||||||
|
}
|
||||||
state.exportDialog.visible = false;
|
state.exportDialog.visible = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -455,10 +532,12 @@ const supportAction = (action: string, dbType: string): boolean => {
|
|||||||
switch (dbType) {
|
switch (dbType) {
|
||||||
case DbType.mysql:
|
case DbType.mysql:
|
||||||
case DbType.mariadb:
|
case DbType.mariadb:
|
||||||
actions = ['dumpDb', 'dbBackup', 'dbRestore'];
|
actions = ['dumpDb', 'backupDb', 'restoreDb'];
|
||||||
}
|
}
|
||||||
return actions.includes(action);
|
return actions.includes(action);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
defineExpose({ search });
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.db-list {
|
.db-list {
|
||||||
|
|||||||
@@ -35,7 +35,13 @@
|
|||||||
clearable
|
clearable
|
||||||
class="w100"
|
class="w100"
|
||||||
>
|
>
|
||||||
<el-option v-for="item in state.histories" :key="item.id" :label="item.name" :value="item"> </el-option>
|
<el-option
|
||||||
|
v-for="item in state.histories"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.name + (item.binlogFileName ? ' ' : ' 不') + '支持指定时间点恢复'"
|
||||||
|
:value="item"
|
||||||
|
>
|
||||||
|
</el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item prop="startTime" label="开始时间">
|
<el-form-item prop="startTime" label="开始时间">
|
||||||
@@ -56,7 +62,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, reactive, ref, watch } from 'vue';
|
import { onMounted, reactive, ref, watch } from 'vue';
|
||||||
import { dbApi } from './api';
|
import { dbApi } from './api';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
data: {
|
data: {
|
||||||
@@ -83,20 +89,30 @@ const visible = defineModel<boolean>('visible', {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const validatePointInTime = (rule: any, value: any, callback: any) => {
|
const validatePointInTime = (rule: any, value: any, callback: any) => {
|
||||||
if (!state.histories || state.histories.length == 0) {
|
|
||||||
callback(new Error('数据库没有备份记录'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const history = state.histories[state.histories.length - 1];
|
|
||||||
if (value < new Date(history.createTime)) {
|
|
||||||
callback(new Error('在此之前数据库没有备份记录'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (value > new Date()) {
|
if (value > new Date()) {
|
||||||
callback(new Error('恢复时间点晚于当前时间'));
|
callback(new Error('恢复时间点晚于当前时间'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
callback();
|
if (!state.histories || state.histories.length == 0) {
|
||||||
|
callback(new Error('数据库没有备份记录'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let last = null;
|
||||||
|
for (const history of state.histories) {
|
||||||
|
if (!history.binlogFileName || history.binlogFileName.length === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (new Date(history.createTime) < value) {
|
||||||
|
callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
last = history;
|
||||||
|
}
|
||||||
|
if (!last) {
|
||||||
|
callback(new Error('现有数据库备份不支持指定时间恢复'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback(last.name + ' 之前的数据库备份不支持指定时间恢复');
|
||||||
};
|
};
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
@@ -110,7 +126,6 @@ const rules = {
|
|||||||
pointInTime: [
|
pointInTime: [
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
// message: '请选择恢复时间点',
|
|
||||||
validator: validatePointInTime,
|
validator: validatePointInTime,
|
||||||
trigger: ['change', 'blur'],
|
trigger: ['change', 'blur'],
|
||||||
},
|
},
|
||||||
@@ -146,7 +161,7 @@ const state = reactive({
|
|||||||
id: 0,
|
id: 0,
|
||||||
dbId: 0,
|
dbId: 0,
|
||||||
dbName: null as any,
|
dbName: null as any,
|
||||||
intervalDay: 1,
|
intervalDay: 0,
|
||||||
startTime: null as any,
|
startTime: null as any,
|
||||||
repeated: null as any,
|
repeated: null as any,
|
||||||
dbBackupId: null as any,
|
dbBackupId: null as any,
|
||||||
@@ -218,7 +233,8 @@ const init = async (data: any) => {
|
|||||||
} else {
|
} else {
|
||||||
state.form.dbName = '';
|
state.form.dbName = '';
|
||||||
state.editOrCreate = false;
|
state.editOrCreate = false;
|
||||||
state.form.intervalDay = 1;
|
state.form.intervalDay = 0;
|
||||||
|
state.form.repeated = false;
|
||||||
state.form.pointInTime = new Date();
|
state.form.pointInTime = new Date();
|
||||||
state.form.startTime = new Date();
|
state.form.startTime = new Date();
|
||||||
state.histories = [];
|
state.histories = [];
|
||||||
@@ -237,6 +253,12 @@ const getDbNamesWithoutRestore = async () => {
|
|||||||
const btnOk = async () => {
|
const btnOk = async () => {
|
||||||
restoreForm.value.validate(async (valid: any) => {
|
restoreForm.value.validate(async (valid: any) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
|
await ElMessageBox.confirm(`确定恢复数据库吗?`, '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
});
|
||||||
|
|
||||||
if (state.restoreMode == 'point-in-time') {
|
if (state.restoreMode == 'point-in-time') {
|
||||||
state.form.dbBackupId = 0;
|
state.form.dbBackupId = 0;
|
||||||
state.form.dbBackupHistoryId = 0;
|
state.form.dbBackupHistoryId = 0;
|
||||||
@@ -245,13 +267,14 @@ const btnOk = async () => {
|
|||||||
state.form.pointInTime = null;
|
state.form.pointInTime = null;
|
||||||
}
|
}
|
||||||
state.form.repeated = false;
|
state.form.repeated = false;
|
||||||
|
state.form.intervalDay = 0;
|
||||||
const reqForm = { ...state.form };
|
const reqForm = { ...state.form };
|
||||||
let api = dbApi.createDbRestore;
|
let api = dbApi.createDbRestore;
|
||||||
if (props.data) {
|
if (props.data) {
|
||||||
api = dbApi.saveDbRestore;
|
api = dbApi.saveDbRestore;
|
||||||
}
|
}
|
||||||
api.request(reqForm).then(() => {
|
api.request(reqForm).then(() => {
|
||||||
ElMessage.success('保存成功');
|
ElMessage.success('成功创建数据库恢复任务');
|
||||||
emit('val-change', state.form);
|
emit('val-change', state.form);
|
||||||
state.btnLoading = true;
|
state.btnLoading = true;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
@@ -21,12 +21,14 @@
|
|||||||
<el-button type="primary" icon="plus" @click="createDbRestore()">添加</el-button>
|
<el-button type="primary" icon="plus" @click="createDbRestore()">添加</el-button>
|
||||||
<el-button type="primary" icon="video-play" @click="enableDbRestore(null)">启用</el-button>
|
<el-button type="primary" icon="video-play" @click="enableDbRestore(null)">启用</el-button>
|
||||||
<el-button type="primary" icon="video-pause" @click="disableDbRestore(null)">禁用</el-button>
|
<el-button type="primary" icon="video-pause" @click="disableDbRestore(null)">禁用</el-button>
|
||||||
|
<el-button type="danger" icon="delete" @click="deleteDbRestore(null)">删除</el-button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #action="{ data }">
|
<template #action="{ data }">
|
||||||
<el-button @click="showDbRestore(data)" type="primary" link>详情</el-button>
|
<el-button @click="showDbRestore(data)" type="primary" link>详情</el-button>
|
||||||
<el-button @click="enableDbRestore(data)" v-if="!data.enabled" type="primary" link>启用</el-button>
|
<el-button @click="enableDbRestore(data)" v-if="!data.enabled" type="primary" link>启用</el-button>
|
||||||
<el-button @click="disableDbRestore(data)" v-if="data.enabled" type="primary" link>禁用</el-button>
|
<el-button @click="disableDbRestore(data)" v-if="data.enabled" type="primary" link>禁用</el-button>
|
||||||
|
<el-button @click="deleteDbRestore(data)" type="danger" link>删除</el-button>
|
||||||
</template>
|
</template>
|
||||||
</page-table>
|
</page-table>
|
||||||
|
|
||||||
@@ -49,7 +51,7 @@
|
|||||||
infoDialog.data.dbBackupHistoryName
|
infoDialog.data.dbBackupHistoryName
|
||||||
}}</el-descriptions-item>
|
}}</el-descriptions-item>
|
||||||
<el-descriptions-item :span="1" label="开始时间">{{ dateFormat(infoDialog.data.startTime) }}</el-descriptions-item>
|
<el-descriptions-item :span="1" label="开始时间">{{ dateFormat(infoDialog.data.startTime) }}</el-descriptions-item>
|
||||||
<el-descriptions-item :span="1" label="是否启用">{{ infoDialog.data.enabled }}</el-descriptions-item>
|
<el-descriptions-item :span="1" label="是否启用">{{ infoDialog.data.enabledDesc }}</el-descriptions-item>
|
||||||
<el-descriptions-item :span="1" label="执行时间">{{ dateFormat(infoDialog.data.lastTime) }}</el-descriptions-item>
|
<el-descriptions-item :span="1" label="执行时间">{{ dateFormat(infoDialog.data.lastTime) }}</el-descriptions-item>
|
||||||
<el-descriptions-item :span="1" label="执行结果">{{ infoDialog.data.lastResult }}</el-descriptions-item>
|
<el-descriptions-item :span="1" label="执行结果">{{ infoDialog.data.lastResult }}</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
@@ -63,7 +65,7 @@ import { dbApi } from './api';
|
|||||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||||
import { TableColumn } from '@/components/pagetable';
|
import { TableColumn } from '@/components/pagetable';
|
||||||
import { SearchItem } from '@/components/SearchForm';
|
import { SearchItem } from '@/components/SearchForm';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
import { dateFormat } from '@/common/utils/date';
|
import { dateFormat } from '@/common/utils/date';
|
||||||
const DbRestoreEdit = defineAsyncComponent(() => import('./DbRestoreEdit.vue'));
|
const DbRestoreEdit = defineAsyncComponent(() => import('./DbRestoreEdit.vue'));
|
||||||
const pageTableRef: Ref<any> = ref(null);
|
const pageTableRef: Ref<any> = ref(null);
|
||||||
@@ -85,7 +87,7 @@ const searchItems = [SearchItem.slot('dbName', '数据库名称', 'dbSelect')];
|
|||||||
const columns = [
|
const columns = [
|
||||||
TableColumn.new('dbName', '数据库名称'),
|
TableColumn.new('dbName', '数据库名称'),
|
||||||
TableColumn.new('startTime', '启动时间').isTime(),
|
TableColumn.new('startTime', '启动时间').isTime(),
|
||||||
TableColumn.new('enabled', '是否启用'),
|
TableColumn.new('enabledDesc', '是否启用'),
|
||||||
TableColumn.new('lastTime', '执行时间').isTime(),
|
TableColumn.new('lastTime', '执行时间').isTime(),
|
||||||
TableColumn.new('lastResult', '执行结果'),
|
TableColumn.new('lastResult', '执行结果'),
|
||||||
TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight().alignCenter(),
|
TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight().alignCenter(),
|
||||||
@@ -135,19 +137,39 @@ const createDbRestore = async () => {
|
|||||||
state.dbRestoreEditDialog.visible = true;
|
state.dbRestoreEditDialog.visible = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deleteDbRestore = async (data: any) => {
|
||||||
|
let restoreId: string;
|
||||||
|
if (data) {
|
||||||
|
restoreId = data.id;
|
||||||
|
} else if (state.selectedData.length > 0) {
|
||||||
|
restoreId = state.selectedData.map((x: any) => x.id).join(' ');
|
||||||
|
} else {
|
||||||
|
ElMessage.error('请选择需要删除的数据库恢复任务');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await ElMessageBox.confirm(`确定删除 “数据库恢复任务” 吗?`, '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
});
|
||||||
|
await dbApi.deleteDbRestore.request({ dbId: props.dbId, restoreId: restoreId });
|
||||||
|
await search();
|
||||||
|
ElMessage.success('删除成功');
|
||||||
|
};
|
||||||
|
|
||||||
const showDbRestore = async (data: any) => {
|
const showDbRestore = async (data: any) => {
|
||||||
state.infoDialog.data = data;
|
state.infoDialog.data = data;
|
||||||
state.infoDialog.visible = true;
|
state.infoDialog.visible = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const enableDbRestore = async (data: any) => {
|
const enableDbRestore = async (data: any) => {
|
||||||
let restoreId: String;
|
let restoreId: string;
|
||||||
if (data) {
|
if (data) {
|
||||||
restoreId = data.id;
|
restoreId = data.id;
|
||||||
} else if (state.selectedData.length > 0) {
|
} else if (state.selectedData.length > 0) {
|
||||||
restoreId = state.selectedData.map((x: any) => x.id).join(' ');
|
restoreId = state.selectedData.map((x: any) => x.id).join(' ');
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error('请选择需要启用的恢复任务');
|
ElMessage.error('请选择需要启用的数据库恢复任务');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await dbApi.enableDbRestore.request({ dbId: props.dbId, restoreId: restoreId });
|
await dbApi.enableDbRestore.request({ dbId: props.dbId, restoreId: restoreId });
|
||||||
@@ -156,13 +178,13 @@ const enableDbRestore = async (data: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const disableDbRestore = async (data: any) => {
|
const disableDbRestore = async (data: any) => {
|
||||||
let restoreId: String;
|
let restoreId: string;
|
||||||
if (data) {
|
if (data) {
|
||||||
restoreId = data.id;
|
restoreId = data.id;
|
||||||
} else if (state.selectedData.length > 0) {
|
} else if (state.selectedData.length > 0) {
|
||||||
restoreId = state.selectedData.map((x: any) => x.id).join(' ');
|
restoreId = state.selectedData.map((x: any) => x.id).join(' ');
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error('请选择需要禁用的恢复任务');
|
ElMessage.error('请选择需要禁用的数据库恢复任务');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await dbApi.disableDbRestore.request({ dbId: props.dbId, restoreId: restoreId });
|
await dbApi.disableDbRestore.request({ dbId: props.dbId, restoreId: restoreId });
|
||||||
|
|||||||
@@ -17,7 +17,10 @@
|
|||||||
|
|
||||||
<template #action="{ data }">
|
<template #action="{ data }">
|
||||||
<el-link
|
<el-link
|
||||||
v-if="data.type == DbSqlExecTypeEnum.Update.value || data.type == DbSqlExecTypeEnum.Delete.value"
|
v-if="
|
||||||
|
data.status == DbSqlExecStatusEnum.Success.value &&
|
||||||
|
(data.type == DbSqlExecTypeEnum.Update.value || data.type == DbSqlExecTypeEnum.Delete.value)
|
||||||
|
"
|
||||||
type="primary"
|
type="primary"
|
||||||
plain
|
plain
|
||||||
size="small"
|
size="small"
|
||||||
@@ -36,9 +39,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { toRefs, watch, reactive, onMounted, Ref, ref } from 'vue';
|
import { onMounted, reactive, Ref, ref, toRefs, watch } from 'vue';
|
||||||
import { dbApi } from './api';
|
import { dbApi } from './api';
|
||||||
import { DbSqlExecTypeEnum } from './enums';
|
import { DbSqlExecTypeEnum, DbSqlExecStatusEnum } from './enums';
|
||||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||||
import { TableColumn } from '@/components/pagetable';
|
import { TableColumn } from '@/components/pagetable';
|
||||||
import { SearchItem } from '@/components/SearchForm';
|
import { SearchItem } from '@/components/SearchForm';
|
||||||
@@ -66,9 +69,11 @@ const columns = ref([
|
|||||||
TableColumn.new('type', '类型').typeTag(DbSqlExecTypeEnum).setAddWidth(10),
|
TableColumn.new('type', '类型').typeTag(DbSqlExecTypeEnum).setAddWidth(10),
|
||||||
TableColumn.new('creator', '执行人'),
|
TableColumn.new('creator', '执行人'),
|
||||||
TableColumn.new('sql', 'SQL').canBeautify(),
|
TableColumn.new('sql', 'SQL').canBeautify(),
|
||||||
TableColumn.new('oldValue', '原值').canBeautify(),
|
|
||||||
TableColumn.new('createTime', '执行时间').isTime(),
|
|
||||||
TableColumn.new('remark', '备注'),
|
TableColumn.new('remark', '备注'),
|
||||||
|
TableColumn.new('status', '执行状态').typeTag(DbSqlExecStatusEnum),
|
||||||
|
TableColumn.new('res', '执行结果'),
|
||||||
|
TableColumn.new('createTime', '执行时间').isTime(),
|
||||||
|
TableColumn.new('oldValue', '原值').canBeautify(),
|
||||||
TableColumn.new('action', '操作').isSlot().setMinWidth(90).fixedRight().alignCenter(),
|
TableColumn.new('action', '操作').isSlot().setMinWidth(90).fixedRight().alignCenter(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -80,6 +85,7 @@ const state = reactive({
|
|||||||
dbId: 0,
|
dbId: 0,
|
||||||
db: '',
|
db: '',
|
||||||
table: '',
|
table: '',
|
||||||
|
status: [DbSqlExecStatusEnum.Success.value, DbSqlExecStatusEnum.Fail.value].join(','),
|
||||||
type: null,
|
type: null,
|
||||||
pageNum: 1,
|
pageNum: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
@@ -120,6 +126,12 @@ const onShowRollbackSql = async (sqlExecLog: any) => {
|
|||||||
const primaryKey = getPrimaryKey(columns);
|
const primaryKey = getPrimaryKey(columns);
|
||||||
const oldValue = JSON.parse(sqlExecLog.oldValue);
|
const oldValue = JSON.parse(sqlExecLog.oldValue);
|
||||||
|
|
||||||
|
let schema = '';
|
||||||
|
let dbArr = sqlExecLog.db.split('/');
|
||||||
|
if (dbArr.length == 2) {
|
||||||
|
schema = dbArr[1] + '.';
|
||||||
|
}
|
||||||
|
|
||||||
const rollbackSqls = [];
|
const rollbackSqls = [];
|
||||||
if (sqlExecLog.type == DbSqlExecTypeEnum.Update.value) {
|
if (sqlExecLog.type == DbSqlExecTypeEnum.Update.value) {
|
||||||
for (let ov of oldValue) {
|
for (let ov of oldValue) {
|
||||||
@@ -130,7 +142,7 @@ const onShowRollbackSql = async (sqlExecLog: any) => {
|
|||||||
}
|
}
|
||||||
setItems.push(`${key} = ${wrapValue(ov[key])}`);
|
setItems.push(`${key} = ${wrapValue(ov[key])}`);
|
||||||
}
|
}
|
||||||
rollbackSqls.push(`UPDATE ${sqlExecLog.table} SET ${setItems.join(', ')} WHERE ${primaryKey} = ${wrapValue(ov[primaryKey])};`);
|
rollbackSqls.push(`UPDATE ${schema}${sqlExecLog.table} SET ${setItems.join(', ')} WHERE ${primaryKey} = ${wrapValue(ov[primaryKey])};`);
|
||||||
}
|
}
|
||||||
} else if (sqlExecLog.type == DbSqlExecTypeEnum.Delete.value) {
|
} else if (sqlExecLog.type == DbSqlExecTypeEnum.Delete.value) {
|
||||||
const columnNames = columns.map((c: any) => c.columnName);
|
const columnNames = columns.map((c: any) => c.columnName);
|
||||||
@@ -139,7 +151,7 @@ const onShowRollbackSql = async (sqlExecLog: any) => {
|
|||||||
for (let column of columnNames) {
|
for (let column of columnNames) {
|
||||||
values.push(wrapValue(ov[column]));
|
values.push(wrapValue(ov[column]));
|
||||||
}
|
}
|
||||||
rollbackSqls.push(`INSERT INTO ${sqlExecLog.table} (${columnNames.join(', ')}) VALUES (${values.join(', ')});`);
|
rollbackSqls.push(`INSERT INTO ${schema}${sqlExecLog.table} (${columnNames.join(', ')}) VALUES (${values.join(', ')});`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,7 +160,7 @@ const onShowRollbackSql = async (sqlExecLog: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getPrimaryKey = (columns: any) => {
|
const getPrimaryKey = (columns: any) => {
|
||||||
const col = columns.find((c: any) => c.columnKey == 'PRI');
|
const col = columns.find((c: any) => c.isPrimaryKey);
|
||||||
if (col) {
|
if (col) {
|
||||||
return col.columnName;
|
return col.columnName;
|
||||||
}
|
}
|
||||||
|
|||||||
331
mayfly_go_web/src/views/ops/db/DbTransferEdit.vue
Normal file
331
mayfly_go_web/src/views/ops/db/DbTransferEdit.vue
Normal file
@@ -0,0 +1,331 @@
|
|||||||
|
<template>
|
||||||
|
<div class="db-transfer-edit">
|
||||||
|
<el-dialog
|
||||||
|
:title="title"
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:before-close="cancel"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
:close-on-press-escape="false"
|
||||||
|
:destroy-on-close="true"
|
||||||
|
width="850px"
|
||||||
|
>
|
||||||
|
<el-form :model="form" ref="dbForm" :rules="rules" label-width="auto">
|
||||||
|
<el-tabs v-model="tabActiveName">
|
||||||
|
<el-tab-pane label="基本信息" :name="basicTab">
|
||||||
|
<el-form-item prop="srcDbId" label="源数据库" required>
|
||||||
|
<db-select-tree
|
||||||
|
placeholder="请选择源数据库"
|
||||||
|
v-model:db-id="form.srcDbId"
|
||||||
|
v-model:inst-name="form.srcInstName"
|
||||||
|
v-model:db-name="form.srcDbName"
|
||||||
|
v-model:tag-path="form.srcTagPath"
|
||||||
|
v-model:db-type="form.srcDbType"
|
||||||
|
@select-db="onSelectSrcDb"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item prop="targetDbId" label="目标数据库" required>
|
||||||
|
<db-select-tree
|
||||||
|
placeholder="请选择目标数据库"
|
||||||
|
v-model:db-id="form.targetDbId"
|
||||||
|
v-model:inst-name="form.targetInstName"
|
||||||
|
v-model:db-name="form.targetDbName"
|
||||||
|
v-model:tag-path="form.targetTagPath"
|
||||||
|
v-model:db-type="form.targetDbType"
|
||||||
|
@select-db="onSelectTargetDb"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item prop="strategy" label="迁移策略" required>
|
||||||
|
<el-select v-model="form.strategy" filterable placeholder="迁移策略">
|
||||||
|
<el-option label="全量" :value="1" />
|
||||||
|
<el-option label="增量(暂不可用)" disabled :value="2" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item prop="nameCase" label="转换表、字段名" required>
|
||||||
|
<el-select v-model="form.nameCase">
|
||||||
|
<el-option label="无" :value="1" />
|
||||||
|
<el-option label="大写" :value="2" />
|
||||||
|
<el-option label="小写" :value="3" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="deleteTable" label="创建前删除表" required>
|
||||||
|
<el-select v-model="form.deleteTable">
|
||||||
|
<el-option label="是" :value="1" />
|
||||||
|
<el-option label="否" :value="2" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="数据库对象" :name="tableTab" :disabled="!baseFieldCompleted">
|
||||||
|
<el-form-item>
|
||||||
|
<el-input v-model="state.filterSrcTableText" style="width: 240px" placeholder="过滤表" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-tree
|
||||||
|
ref="srcTreeRef"
|
||||||
|
style="width: 760px; max-height: 400px; overflow-y: auto"
|
||||||
|
default-expand-all
|
||||||
|
:expand-on-click-node="false"
|
||||||
|
:data="state.srcTableTree"
|
||||||
|
node-key="id"
|
||||||
|
show-checkbox
|
||||||
|
@check-change="handleSrcTableCheckChange"
|
||||||
|
:filter-node-method="filterSrcTableTreeNode"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div>
|
||||||
|
<el-button @click="cancel()">取 消</el-button>
|
||||||
|
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk">确 定</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, nextTick, reactive, ref, toRefs, watch } from 'vue';
|
||||||
|
import { dbApi } from './api';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
data: {
|
||||||
|
type: [Boolean, Object],
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
//定义事件
|
||||||
|
const emit = defineEmits(['update:visible', 'cancel', 'val-change']);
|
||||||
|
|
||||||
|
const dialogVisible = defineModel<boolean>('visible', { default: false });
|
||||||
|
|
||||||
|
const rules = {};
|
||||||
|
|
||||||
|
const dbForm: any = ref(null);
|
||||||
|
|
||||||
|
const basicTab = 'basic';
|
||||||
|
const tableTab = 'table';
|
||||||
|
|
||||||
|
type FormData = {
|
||||||
|
id?: number;
|
||||||
|
srcDbId?: number;
|
||||||
|
srcDbName?: string;
|
||||||
|
srcDbType?: string;
|
||||||
|
srcInstName?: string;
|
||||||
|
srcTagPath?: string;
|
||||||
|
srcTableNames?: string;
|
||||||
|
targetDbId?: number;
|
||||||
|
targetInstName?: string;
|
||||||
|
targetDbName?: string;
|
||||||
|
targetTagPath?: string;
|
||||||
|
targetDbType?: string;
|
||||||
|
strategy: 1 | 2;
|
||||||
|
nameCase: 1 | 2 | 3;
|
||||||
|
deleteTable?: 1 | 2;
|
||||||
|
checkedKeys: string;
|
||||||
|
runningState: 1 | 2;
|
||||||
|
};
|
||||||
|
|
||||||
|
const basicFormData = {
|
||||||
|
strategy: 1,
|
||||||
|
nameCase: 1,
|
||||||
|
deleteTable: 1,
|
||||||
|
checkedKeys: '',
|
||||||
|
runningState: 1,
|
||||||
|
} as FormData;
|
||||||
|
|
||||||
|
const srcTableList = ref<{ tableName: string; tableComment: string }[]>([]);
|
||||||
|
const srcTableListDisabled = ref(false);
|
||||||
|
|
||||||
|
const defaultKeys = ['tab-check', 'all', 'table-list'];
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
tabActiveName: 'basic',
|
||||||
|
form: basicFormData,
|
||||||
|
submitForm: {} as any,
|
||||||
|
srcTableFields: [] as string[],
|
||||||
|
targetColumnList: [] as any[],
|
||||||
|
filterSrcTableText: '',
|
||||||
|
srcTableTree: [
|
||||||
|
{
|
||||||
|
id: 'tab-check',
|
||||||
|
label: '表',
|
||||||
|
children: [
|
||||||
|
{ id: 'all', label: '全部表(*)' },
|
||||||
|
{
|
||||||
|
id: 'table-list',
|
||||||
|
label: '自定义',
|
||||||
|
disabled: srcTableListDisabled,
|
||||||
|
children: [] as any[],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { tabActiveName, form, submitForm } = toRefs(state);
|
||||||
|
|
||||||
|
const { isFetching: saveBtnLoading, execute: saveExec } = dbApi.saveDbTransferTask.useApi(submitForm);
|
||||||
|
|
||||||
|
// 基础字段信息是否填写完整
|
||||||
|
const baseFieldCompleted = computed(() => {
|
||||||
|
return state.form.srcDbId && state.form.targetDbId && state.form.targetDbName;
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(dialogVisible, async (newValue: boolean) => {
|
||||||
|
if (!newValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.tabActiveName = 'basic';
|
||||||
|
const propsData = props.data as any;
|
||||||
|
if (!propsData?.id) {
|
||||||
|
let d = {} as FormData;
|
||||||
|
Object.assign(d, basicFormData);
|
||||||
|
state.form = d;
|
||||||
|
await nextTick(() => {
|
||||||
|
srcTreeRef.value.setCheckedKeys([]);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.form = props.data as FormData;
|
||||||
|
let { srcDbId, targetDbId } = state.form;
|
||||||
|
|
||||||
|
// 初始化src数据源
|
||||||
|
if (srcDbId) {
|
||||||
|
// 通过tagPath查询实例列表
|
||||||
|
const dbInfoRes = await dbApi.dbs.request({ id: srcDbId });
|
||||||
|
const db = dbInfoRes.list[0];
|
||||||
|
// 初始化实例
|
||||||
|
db.databases = db.database?.split(' ').sort() || [];
|
||||||
|
|
||||||
|
if (srcDbId && state.form.srcDbName) {
|
||||||
|
await loadDbTables(srcDbId, state.form.srcDbName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化target数据源
|
||||||
|
if (targetDbId) {
|
||||||
|
// 通过tagPath查询实例列表
|
||||||
|
const dbInfoRes = await dbApi.dbs.request({ id: targetDbId });
|
||||||
|
const db = dbInfoRes.list[0];
|
||||||
|
// 初始化实例
|
||||||
|
db.databases = db.database?.split(' ').sort() || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化勾选迁移表
|
||||||
|
srcTreeRef.value.setCheckedKeys(state.form.checkedKeys.split(','));
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => state.filterSrcTableText,
|
||||||
|
(val) => {
|
||||||
|
srcTreeRef.value!.filter(val);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSelectSrcDb = async (params: any) => {
|
||||||
|
// 初始化数据源
|
||||||
|
params.databases = params.dbs; // 数据源里需要这个值
|
||||||
|
await loadDbTables(params.id, params.db);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSelectTargetDb = async (params: any) => {
|
||||||
|
console.log(params);
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadDbTables = async (dbId: number, db: string) => {
|
||||||
|
// 加载db下的表
|
||||||
|
srcTableList.value = await dbApi.tableInfos.request({ id: dbId, db });
|
||||||
|
handleLoadSrcTableTree();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSrcTableCheckChange = (data: { id: string; name: string }, checked: boolean) => {
|
||||||
|
if (data.id === 'all') {
|
||||||
|
srcTableListDisabled.value = checked;
|
||||||
|
if (checked) {
|
||||||
|
state.form.checkedKeys = 'all';
|
||||||
|
} else {
|
||||||
|
state.form.checkedKeys = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (data.id && (data.id + '').startsWith('list-item')) {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterSrcTableTreeNode = (value: string, data: any) => {
|
||||||
|
if (!value) return true;
|
||||||
|
return data.label.includes(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLoadSrcTableTree = () => {
|
||||||
|
state.srcTableTree[0].children[1].children = srcTableList.value.map((item) => {
|
||||||
|
return {
|
||||||
|
id: item.tableName,
|
||||||
|
label: item.tableName + (item.tableComment && '-' + item.tableComment),
|
||||||
|
disabled: srcTableListDisabled,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getReqForm = async () => {
|
||||||
|
return { ...state.form };
|
||||||
|
};
|
||||||
|
|
||||||
|
const srcTreeRef = ref();
|
||||||
|
|
||||||
|
const getCheckedKeys = () => {
|
||||||
|
let checks = srcTreeRef.value!.getCheckedKeys(false);
|
||||||
|
if (checks.indexOf('all') >= 0) {
|
||||||
|
return ['all'];
|
||||||
|
}
|
||||||
|
return checks.filter((item: any) => !defaultKeys.includes(item));
|
||||||
|
};
|
||||||
|
|
||||||
|
const btnOk = async () => {
|
||||||
|
dbForm.value.validate(async (valid: boolean) => {
|
||||||
|
if (!valid) {
|
||||||
|
ElMessage.error('请正确填写信息');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.submitForm = await getReqForm();
|
||||||
|
|
||||||
|
let checkedKeys = getCheckedKeys();
|
||||||
|
if (checkedKeys.length > 0) {
|
||||||
|
state.submitForm.checkedKeys = checkedKeys.join(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!state.submitForm.checkedKeys) {
|
||||||
|
ElMessage.error('请选择需要迁移的表');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await saveExec();
|
||||||
|
ElMessage.success('保存成功');
|
||||||
|
emit('val-change', state.form);
|
||||||
|
cancel();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
dialogVisible.value = false;
|
||||||
|
emit('cancel');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss">
|
||||||
|
.db-transfer-edit {
|
||||||
|
.el-select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
196
mayfly_go_web/src/views/ops/db/DbTransferList.vue
Normal file
196
mayfly_go_web/src/views/ops/db/DbTransferList.vue
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
<template>
|
||||||
|
<div class="db-list">
|
||||||
|
<page-table
|
||||||
|
ref="pageTableRef"
|
||||||
|
:page-api="dbApi.dbTransferTasks"
|
||||||
|
:searchItems="searchItems"
|
||||||
|
v-model:query-form="query"
|
||||||
|
:show-selection="true"
|
||||||
|
v-model:selection-data="state.selectionData"
|
||||||
|
:columns="columns"
|
||||||
|
>
|
||||||
|
<template #tableHeader>
|
||||||
|
<el-button v-auth="perms.save" type="primary" icon="plus" @click="edit(false)">添加</el-button>
|
||||||
|
<el-button v-auth="perms.del" :disabled="selectionData.length < 1" @click="del()" type="danger" icon="delete">删除</el-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #srcDb="{ data }">
|
||||||
|
<el-tooltip :content="`${data.srcTagPath} > ${data.srcInstName} > ${data.srcDbName}`">
|
||||||
|
<span>
|
||||||
|
<SvgIcon :name="getDbDialect(data.srcDbType).getInfo().icon" :size="18" />
|
||||||
|
{{ data.srcInstName }}
|
||||||
|
</span>
|
||||||
|
</el-tooltip>
|
||||||
|
</template>
|
||||||
|
<template #targetDb="{ data }">
|
||||||
|
<el-tooltip :content="`${data.targetTagPath} > ${data.targetInstName} > ${data.targetDbName}`">
|
||||||
|
<span>
|
||||||
|
<SvgIcon :name="getDbDialect(data.targetDbType).getInfo().icon" :size="18" />
|
||||||
|
{{ data.targetInstName }}
|
||||||
|
</span>
|
||||||
|
</el-tooltip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #action="{ data }">
|
||||||
|
<!-- 删除、启停用、编辑 -->
|
||||||
|
<el-button v-if="actionBtns[perms.save]" @click="edit(data)" type="primary" link>编辑</el-button>
|
||||||
|
<el-button v-if="actionBtns[perms.log]" type="primary" link @click="log(data)">日志</el-button>
|
||||||
|
<el-button v-if="data.runningState === 1" @click="stop(data.id)" type="danger" link>停止</el-button>
|
||||||
|
<el-button v-if="actionBtns[perms.run] && data.runningState !== 1" type="primary" link @click="reRun(data)">运行</el-button>
|
||||||
|
</template>
|
||||||
|
</page-table>
|
||||||
|
|
||||||
|
<db-transfer-edit @val-change="search" :title="editDialog.title" v-model:visible="editDialog.visible" v-model:data="editDialog.data" />
|
||||||
|
|
||||||
|
<TerminalLog v-model:log-id="logsDialog.logId" v-model:visible="logsDialog.visible" :title="logsDialog.title" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
import { dbApi } from './api';
|
||||||
|
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||||
|
import { TableColumn } from '@/components/pagetable';
|
||||||
|
import { hasPerms } from '@/components/auth/auth';
|
||||||
|
import { SearchItem } from '@/components/SearchForm';
|
||||||
|
import { getDbDialect } from '@/views/ops/db/dialect';
|
||||||
|
import { DbTransferRunningStateEnum } from './enums';
|
||||||
|
import TerminalLog from '@/components/terminal/TerminalLog.vue';
|
||||||
|
|
||||||
|
const DbTransferEdit = defineAsyncComponent(() => import('./DbTransferEdit.vue'));
|
||||||
|
|
||||||
|
const perms = {
|
||||||
|
save: 'db:transfer:save',
|
||||||
|
del: 'db:transfer:del',
|
||||||
|
status: 'db:transfer:status',
|
||||||
|
log: 'db:transfer:log',
|
||||||
|
run: 'db:transfer:run',
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchItems = [SearchItem.input('name', '名称')];
|
||||||
|
|
||||||
|
const columns = ref([
|
||||||
|
TableColumn.new('srcDb', '源库').setMinWidth(200).isSlot(),
|
||||||
|
TableColumn.new('targetDb', '目标库').setMinWidth(200).isSlot(),
|
||||||
|
TableColumn.new('runningState', '执行状态').typeTag(DbTransferRunningStateEnum),
|
||||||
|
TableColumn.new('creator', '创建人'),
|
||||||
|
TableColumn.new('createTime', '创建时间').isTime(),
|
||||||
|
TableColumn.new('modifier', '修改人'),
|
||||||
|
TableColumn.new('updateTime', '修改时间').isTime(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 该用户拥有的的操作列按钮权限
|
||||||
|
const actionBtns = hasPerms([perms.save, perms.del, perms.status, perms.log, perms.run]);
|
||||||
|
const actionWidth = ((actionBtns[perms.save] ? 1 : 0) + (actionBtns[perms.log] ? 1 : 0) + (actionBtns[perms.run] ? 1 : 0)) * 55;
|
||||||
|
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(actionWidth).fixedRight().alignCenter();
|
||||||
|
const pageTableRef: Ref<any> = ref(null);
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
row: {},
|
||||||
|
dbId: 0,
|
||||||
|
db: '',
|
||||||
|
/**
|
||||||
|
* 选中的数据
|
||||||
|
*/
|
||||||
|
selectionData: [],
|
||||||
|
/**
|
||||||
|
* 查询条件
|
||||||
|
*/
|
||||||
|
query: {
|
||||||
|
name: null,
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 0,
|
||||||
|
},
|
||||||
|
editDialog: {
|
||||||
|
visible: false,
|
||||||
|
data: null as any,
|
||||||
|
title: '新增数据数据迁移任务',
|
||||||
|
},
|
||||||
|
logsDialog: {
|
||||||
|
logId: 0,
|
||||||
|
title: '数据库迁移日志',
|
||||||
|
visible: false,
|
||||||
|
data: null as any,
|
||||||
|
running: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { selectionData, query, editDialog, logsDialog } = toRefs(state);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (Object.keys(actionBtns).length > 0) {
|
||||||
|
columns.value.push(actionColumn);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const search = () => {
|
||||||
|
pageTableRef.value.search();
|
||||||
|
};
|
||||||
|
|
||||||
|
const edit = async (data: any) => {
|
||||||
|
if (!data) {
|
||||||
|
state.editDialog.data = null;
|
||||||
|
state.editDialog.title = '新增数据库迁移任务';
|
||||||
|
} else {
|
||||||
|
state.editDialog.data = data;
|
||||||
|
state.editDialog.title = '修改数据库迁移任务';
|
||||||
|
}
|
||||||
|
state.editDialog.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const stop = async (id: any) => {
|
||||||
|
await ElMessageBox.confirm(`确定停止?`, '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
});
|
||||||
|
await dbApi.stopDbTransferTask.request({ taskId: id });
|
||||||
|
ElMessage.success(`停止成功`);
|
||||||
|
search();
|
||||||
|
};
|
||||||
|
|
||||||
|
const log = (data: any) => {
|
||||||
|
state.logsDialog.logId = data.logId;
|
||||||
|
state.logsDialog.visible = true;
|
||||||
|
state.logsDialog.title = '数据库迁移日志';
|
||||||
|
state.logsDialog.running = data.state === 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const reRun = async (data: any) => {
|
||||||
|
await ElMessageBox.confirm(`确定运行?`, '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
let res = await dbApi.runDbTransferTask.request({ taskId: data.id });
|
||||||
|
console.log(res);
|
||||||
|
ElMessage.success('运行成功');
|
||||||
|
// 拿到日志id之后,弹出日志弹窗
|
||||||
|
log({ logId: res, state: 1 });
|
||||||
|
} catch (e) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
// 延迟2秒执行,后端异步执行
|
||||||
|
setTimeout(() => {
|
||||||
|
search();
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const del = async () => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确定删除任务?`, '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
});
|
||||||
|
await dbApi.deleteDbTransferTask.request({ taskId: state.selectionData.map((x: any) => x.id).join(',') });
|
||||||
|
ElMessage.success('删除成功');
|
||||||
|
search();
|
||||||
|
} catch (err) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss"></style>
|
||||||
@@ -1,58 +1,111 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" :close-on-click-modal="false" :destroy-on-close="true" width="38%">
|
<el-drawer :title="title" v-model="dialogVisible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
|
||||||
|
<template #header>
|
||||||
|
<DrawerHeader :header="title" :back="cancel" />
|
||||||
|
</template>
|
||||||
|
|
||||||
<el-form :model="form" ref="dbForm" :rules="rules" label-width="auto">
|
<el-form :model="form" ref="dbForm" :rules="rules" label-width="auto">
|
||||||
<el-tabs v-model="tabActiveName">
|
<el-divider content-position="left">基本</el-divider>
|
||||||
<el-tab-pane label="基础信息" name="basic">
|
<el-form-item prop="code" label="编号" required>
|
||||||
<el-form-item prop="name" label="别名" required>
|
<el-input :disabled="form.id" v-model.trim="form.code" placeholder="请输入编号 (数字字母下划线), 不可修改" auto-complete="off"></el-input>
|
||||||
<el-input v-model.trim="form.name" placeholder="请输入数据库别名" auto-complete="off"></el-input>
|
</el-form-item>
|
||||||
</el-form-item>
|
<el-form-item prop="name" label="名称" required>
|
||||||
<el-form-item prop="type" label="类型" required>
|
<el-input v-model.trim="form.name" placeholder="请输入数据库别名" auto-complete="off"></el-input>
|
||||||
<el-select @change="changeDbType" style="width: 100%" v-model="form.type" placeholder="请选择数据库类型">
|
</el-form-item>
|
||||||
<el-option v-for="dt in dbTypes" :key="dt.type" :value="dt.type" :label="dt.label">
|
|
||||||
<SvgIcon :name="getDbDialect(dt.type).getInfo().icon" :size="18" />
|
<el-form-item prop="type" label="类型" required>
|
||||||
{{ dt.label }}
|
<el-select @change="changeDbType" style="width: 100%" v-model="form.type" placeholder="请选择数据库类型">
|
||||||
</el-option>
|
<el-option
|
||||||
</el-select>
|
v-for="(dbTypeAndDialect, key) in getDbDialectMap()"
|
||||||
</el-form-item>
|
:key="key"
|
||||||
<el-form-item prop="host" label="host" required>
|
:value="dbTypeAndDialect[0]"
|
||||||
<el-col :span="18">
|
:label="dbTypeAndDialect[1].getInfo().name"
|
||||||
<el-input :disabled="form.id !== undefined" v-model.trim="form.host" placeholder="请输入主机ip" auto-complete="off"></el-input>
|
>
|
||||||
</el-col>
|
<SvgIcon :name="dbTypeAndDialect[1].getInfo().icon" :size="20" />
|
||||||
<el-col style="text-align: center" :span="1">:</el-col>
|
{{ dbTypeAndDialect[1].getInfo().name }}
|
||||||
<el-col :span="5">
|
</el-option>
|
||||||
<el-input type="number" v-model.number="form.port" placeholder="端口"></el-input>
|
|
||||||
</el-col>
|
<template #prefix>
|
||||||
</el-form-item>
|
<SvgIcon :name="getDbDialect(form.type).getInfo().icon" :size="20" />
|
||||||
<el-form-item v-if="form.type === DbType.oracle" prop="sid" label="SID">
|
</template>
|
||||||
<el-input v-model.trim="form.sid" placeholder="请输入服务id"></el-input>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item prop="username" label="用户名" required>
|
|
||||||
<el-input v-model.trim="form.username" placeholder="请输入用户名"></el-input>
|
<el-form-item v-if="form.type !== DbType.sqlite" prop="host" label="host" required>
|
||||||
</el-form-item>
|
<el-col :span="18">
|
||||||
<el-form-item prop="password" label="密码">
|
<el-input :disabled="form.id !== undefined" v-model.trim="form.host" placeholder="请输入主机ip" auto-complete="off"></el-input>
|
||||||
<el-input type="password" show-password v-model.trim="form.password" placeholder="请输入密码" autocomplete="new-password">
|
</el-col>
|
||||||
<template v-if="form.id && form.id != 0" #suffix>
|
<el-col style="text-align: center" :span="1">:</el-col>
|
||||||
<el-popover @hide="pwd = ''" placement="right" title="原密码" :width="200" trigger="click" :content="pwd">
|
<el-col :span="5">
|
||||||
<template #reference>
|
<el-input type="number" v-model.number="form.port" placeholder="端口"></el-input>
|
||||||
<el-link v-auth="'db:instance:save'" @click="getDbPwd" :underline="false" type="primary" class="mr5"
|
</el-col>
|
||||||
>原密码
|
</el-form-item>
|
||||||
</el-link>
|
|
||||||
</template>
|
<el-form-item v-if="form.type === DbType.sqlite" prop="host" label="sqlite地址">
|
||||||
</el-popover>
|
<el-input v-model.trim="form.host" placeholder="请输入sqlite文件在服务器的绝对地址"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item v-if="form.type === DbType.oracle" label="SID|服务名">
|
||||||
|
<el-col :span="5">
|
||||||
|
<el-select
|
||||||
|
@change="
|
||||||
|
() => {
|
||||||
|
state.extra.serviceName = '';
|
||||||
|
state.extra.sid = '';
|
||||||
|
}
|
||||||
|
"
|
||||||
|
v-model="state.extra.stype"
|
||||||
|
placeholder="请选择"
|
||||||
|
>
|
||||||
|
<el-option label="服务名" :value="1" />
|
||||||
|
<el-option label="SID" :value="2" />
|
||||||
|
</el-select>
|
||||||
|
</el-col>
|
||||||
|
<el-col style="text-align: center" :span="1">:</el-col>
|
||||||
|
<el-col :span="18">
|
||||||
|
<el-input v-if="state.extra.stype == 1" v-model="state.extra.serviceName" placeholder="请输入服务名"> </el-input>
|
||||||
|
<el-input v-else v-model="state.extra.sid" placeholder="请输入SID"> </el-input>
|
||||||
|
</el-col>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item prop="remark" label="备注">
|
||||||
|
<el-input v-model="form.remark" auto-complete="off" type="textarea"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<template v-if="form.type !== DbType.sqlite">
|
||||||
|
<el-divider content-position="left">账号</el-divider>
|
||||||
|
<div>
|
||||||
|
<ResourceAuthCertTableEdit
|
||||||
|
v-model="form.authCerts"
|
||||||
|
:resource-code="form.code"
|
||||||
|
:resource-type="TagResourceTypeEnum.Db.value"
|
||||||
|
:test-conn-btn-loading="testConnBtnLoading"
|
||||||
|
@test-conn="testConn"
|
||||||
|
:disable-ciphertext-type="[AuthCertCiphertextTypeEnum.PrivateKey.value]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<!--
|
||||||
|
<el-form-item v-if="form.type !== DbType.sqlite" prop="username" label="用户名" required>
|
||||||
|
<el-input v-model.trim="form.username" placeholder="请输入用户名"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="form.type !== DbType.sqlite" 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">
|
||||||
|
<template #reference>
|
||||||
|
<el-link v-auth="'db:instance:save'" @click="getDbPwd" :underline="false" type="primary" class="mr5">原密码 </el-link>
|
||||||
</template>
|
</template>
|
||||||
</el-input>
|
</el-popover>
|
||||||
</el-form-item>
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item> -->
|
||||||
|
|
||||||
<el-form-item prop="remark" label="备注">
|
<el-divider content-position="left">其他</el-divider>
|
||||||
<el-input v-model="form.remark" auto-complete="off" type="textarea"></el-input>
|
<el-form-item prop="params" label="连接参数">
|
||||||
</el-form-item>
|
<el-input v-model.trim="form.params" placeholder="其他连接参数,形如: key1=value1&key2=value2">
|
||||||
</el-tab-pane>
|
<!-- <template #suffix>
|
||||||
|
|
||||||
<el-tab-pane label="其他配置" name="other">
|
|
||||||
<el-form-item prop="params" label="连接参数">
|
|
||||||
<el-input v-model.trim="form.params" placeholder="其他连接参数,形如: key1=value1&key2=value2">
|
|
||||||
<!-- <template #suffix>
|
|
||||||
<el-link
|
<el-link
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href="https://github.com/go-sql-driver/mysql#parameters"
|
href="https://github.com/go-sql-driver/mysql#parameters"
|
||||||
@@ -62,24 +115,21 @@
|
|||||||
>参数参考</el-link
|
>参数参考</el-link
|
||||||
>
|
>
|
||||||
</template> -->
|
</template> -->
|
||||||
</el-input>
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item prop="sshTunnelMachineId" label="SSH隧道">
|
<el-form-item prop="sshTunnelMachineId" label="SSH隧道">
|
||||||
<ssh-tunnel-select v-model="form.sshTunnelMachineId" />
|
<ssh-tunnel-select v-model="form.sshTunnelMachineId" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-tab-pane>
|
|
||||||
</el-tabs>
|
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="dialog-footer">
|
<div class="dialog-footer">
|
||||||
<el-button @click="testConn" :loading="testConnBtnLoading" type="success">测试连接</el-button>
|
|
||||||
<el-button @click="cancel()">取 消</el-button>
|
<el-button @click="cancel()">取 消</el-button>
|
||||||
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk">确 定</el-button>
|
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk">确 定</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-drawer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -87,11 +137,14 @@
|
|||||||
import { reactive, ref, toRefs, watch } from 'vue';
|
import { reactive, ref, toRefs, watch } from 'vue';
|
||||||
import { dbApi } from './api';
|
import { dbApi } from './api';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
import { notBlank } from '@/common/assert';
|
|
||||||
import { RsaEncrypt } from '@/common/rsa';
|
|
||||||
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
|
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
|
||||||
import { DbType, getDbDialect } from './dialect';
|
import { DbType, getDbDialect, getDbDialectMap } from './dialect';
|
||||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||||
|
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||||
|
import { ResourceCodePattern } from '@/common/pattern';
|
||||||
|
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||||
|
import ResourceAuthCertTableEdit from '../component/ResourceAuthCertTableEdit.vue';
|
||||||
|
import { AuthCertCiphertextTypeEnum } from '../tag/enums';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
visible: {
|
visible: {
|
||||||
@@ -109,6 +162,18 @@ const props = defineProps({
|
|||||||
const emit = defineEmits(['update:visible', 'cancel', 'val-change']);
|
const emit = defineEmits(['update:visible', 'cancel', 'val-change']);
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
|
code: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请输入编码',
|
||||||
|
trigger: ['change', 'blur'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: ResourceCodePattern.pattern,
|
||||||
|
message: ResourceCodePattern.message,
|
||||||
|
trigger: ['blur'],
|
||||||
|
},
|
||||||
|
],
|
||||||
name: [
|
name: [
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
@@ -130,13 +195,6 @@ const rules = {
|
|||||||
trigger: ['blur'],
|
trigger: ['blur'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
username: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: '请输入用户名',
|
|
||||||
trigger: ['change', 'blur'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
sid: [
|
sid: [
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
@@ -148,69 +206,45 @@ const rules = {
|
|||||||
|
|
||||||
const dbForm: any = ref(null);
|
const dbForm: any = ref(null);
|
||||||
|
|
||||||
const dbTypes = [
|
|
||||||
{
|
|
||||||
type: 'mysql',
|
|
||||||
label: 'mysql',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'mariadb',
|
|
||||||
label: 'mariadb',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'postgres',
|
|
||||||
label: 'postgres',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'dm',
|
|
||||||
label: '达梦',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'oracle',
|
|
||||||
label: 'oracle',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
dialogVisible: false,
|
dialogVisible: false,
|
||||||
tabActiveName: 'basic',
|
extra: {} as any, // 连接需要的额外参数(json)
|
||||||
form: {
|
form: {
|
||||||
id: null,
|
id: null,
|
||||||
type: null,
|
type: '',
|
||||||
|
code: '',
|
||||||
name: null,
|
name: null,
|
||||||
host: '',
|
host: '',
|
||||||
port: null,
|
port: null,
|
||||||
username: null,
|
authCerts: [],
|
||||||
sid: null, // oracle类项目需要服务id
|
extra: '', // 连接需要的额外参数(json字符串)
|
||||||
password: null,
|
|
||||||
params: null,
|
params: null,
|
||||||
remark: '',
|
remark: '',
|
||||||
sshTunnelMachineId: null as any,
|
sshTunnelMachineId: null as any,
|
||||||
},
|
},
|
||||||
subimtForm: {},
|
submitForm: {} as any,
|
||||||
// 原密码
|
|
||||||
pwd: '',
|
|
||||||
// 原用户名
|
|
||||||
oldUserName: null,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { dialogVisible, tabActiveName, form, subimtForm, pwd } = toRefs(state);
|
const { dialogVisible, form, submitForm } = toRefs(state);
|
||||||
|
|
||||||
const { isFetching: saveBtnLoading, execute: saveInstanceExec } = dbApi.saveInstance.useApi(subimtForm);
|
const { isFetching: saveBtnLoading, execute: saveInstanceExec } = dbApi.saveInstance.useApi(submitForm);
|
||||||
const { isFetching: testConnBtnLoading, execute: testConnExec } = dbApi.testConn.useApi(subimtForm);
|
const { isFetching: testConnBtnLoading, execute: testConnExec } = dbApi.testConn.useApi(submitForm);
|
||||||
|
|
||||||
watch(props, (newValue: any) => {
|
watch(props, (newValue: any) => {
|
||||||
state.dialogVisible = newValue.visible;
|
state.dialogVisible = newValue.visible;
|
||||||
if (!state.dialogVisible) {
|
if (!state.dialogVisible) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
state.tabActiveName = 'basic';
|
|
||||||
if (newValue.data) {
|
if (newValue.data) {
|
||||||
state.form = { ...newValue.data };
|
state.form = { ...newValue.data };
|
||||||
state.oldUserName = state.form.username;
|
try {
|
||||||
|
state.extra = JSON.parse(state.form.extra);
|
||||||
|
} catch (e) {
|
||||||
|
state.extra = {};
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
state.form = { port: null } as any;
|
state.form = { port: null, type: DbType.mysql } as any;
|
||||||
state.oldUserName = null;
|
state.form.authCerts = [];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -218,48 +252,42 @@ const changeDbType = (val: string) => {
|
|||||||
if (!state.form.id) {
|
if (!state.form.id) {
|
||||||
state.form.port = getDbDialect(val).getInfo().defaultPort as any;
|
state.form.port = getDbDialect(val).getInfo().defaultPort as any;
|
||||||
}
|
}
|
||||||
};
|
state.extra = {};
|
||||||
|
|
||||||
const getDbPwd = async () => {
|
|
||||||
state.pwd = await dbApi.getInstancePwd.request({ id: state.form.id });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getReqForm = async () => {
|
const getReqForm = async () => {
|
||||||
const reqForm = { ...state.form };
|
const reqForm = { ...state.form };
|
||||||
reqForm.password = await RsaEncrypt(reqForm.password);
|
|
||||||
if (!state.form.sshTunnelMachineId) {
|
if (!state.form.sshTunnelMachineId) {
|
||||||
reqForm.sshTunnelMachineId = -1;
|
reqForm.sshTunnelMachineId = -1;
|
||||||
}
|
}
|
||||||
|
if (Object.keys(state.extra).length > 0) {
|
||||||
|
reqForm.extra = JSON.stringify(state.extra);
|
||||||
|
}
|
||||||
return reqForm;
|
return reqForm;
|
||||||
};
|
};
|
||||||
|
|
||||||
const testConn = async () => {
|
const testConn = async (authCert: any) => {
|
||||||
dbForm.value.validate(async (valid: boolean) => {
|
dbForm.value.validate(async (valid: boolean) => {
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
ElMessage.error('请正确填写信息');
|
ElMessage.error('请正确填写信息');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
state.subimtForm = await getReqForm();
|
state.submitForm = await getReqForm();
|
||||||
|
state.submitForm.authCerts = [authCert];
|
||||||
await testConnExec();
|
await testConnExec();
|
||||||
ElMessage.success('连接成功');
|
ElMessage.success('连接成功');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const btnOk = async () => {
|
const btnOk = async () => {
|
||||||
if (!state.form.id) {
|
|
||||||
notBlank(state.form.password, '新增操作,密码不可为空');
|
|
||||||
} else if (state.form.username != state.oldUserName) {
|
|
||||||
notBlank(state.form.password, '已修改用户名,请输入密码');
|
|
||||||
}
|
|
||||||
|
|
||||||
dbForm.value.validate(async (valid: boolean) => {
|
dbForm.value.validate(async (valid: boolean) => {
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
ElMessage.error('请正确填写信息');
|
ElMessage.error('请正确填写信息');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
state.subimtForm = await getReqForm();
|
state.submitForm = await getReqForm();
|
||||||
await saveInstanceExec();
|
await saveInstanceExec();
|
||||||
ElMessage.success('保存成功');
|
ElMessage.success('保存成功');
|
||||||
emit('val-change', state.form);
|
emit('val-change', state.form);
|
||||||
@@ -270,6 +298,7 @@ const btnOk = async () => {
|
|||||||
const cancel = () => {
|
const cancel = () => {
|
||||||
emit('update:visible', false);
|
emit('update:visible', false);
|
||||||
emit('cancel');
|
emit('cancel');
|
||||||
|
state.extra = {};
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss"></style>
|
<style lang="scss"></style>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
<page-table
|
<page-table
|
||||||
ref="pageTableRef"
|
ref="pageTableRef"
|
||||||
:page-api="dbApi.instances"
|
:page-api="dbApi.instances"
|
||||||
|
:data-handler-fn="handleData"
|
||||||
:searchItems="searchItems"
|
:searchItems="searchItems"
|
||||||
v-model:query-form="query"
|
v-model:query-form="query"
|
||||||
:show-selection="true"
|
:show-selection="true"
|
||||||
@@ -16,8 +17,12 @@
|
|||||||
>
|
>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template #authCert="{ data }">
|
||||||
|
<ResourceAuthCert v-model:select-auth-cert="data.selectAuthCert" :auth-certs="data.authCerts" />
|
||||||
|
</template>
|
||||||
|
|
||||||
<template #type="{ data }">
|
<template #type="{ data }">
|
||||||
<el-tooltip :content="data.type" placement="top">
|
<el-tooltip :content="getDbDialect(data.type).getInfo().name" placement="top">
|
||||||
<SvgIcon :name="getDbDialect(data.type).getInfo().icon" :size="20" />
|
<SvgIcon :name="getDbDialect(data.type).getInfo().icon" :size="20" />
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</template>
|
</template>
|
||||||
@@ -35,7 +40,6 @@
|
|||||||
<el-descriptions-item :span="2" label="主机">{{ infoDialog.data.host }}</el-descriptions-item>
|
<el-descriptions-item :span="2" label="主机">{{ infoDialog.data.host }}</el-descriptions-item>
|
||||||
<el-descriptions-item :span="1" label="端口">{{ infoDialog.data.port }}</el-descriptions-item>
|
<el-descriptions-item :span="1" label="端口">{{ infoDialog.data.port }}</el-descriptions-item>
|
||||||
|
|
||||||
<el-descriptions-item :span="2" label="用户名">{{ infoDialog.data.username }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item :span="1" label="类型">{{ infoDialog.data.type }}</el-descriptions-item>
|
<el-descriptions-item :span="1" label="类型">{{ infoDialog.data.type }}</el-descriptions-item>
|
||||||
|
|
||||||
<el-descriptions-item :span="3" label="连接参数">{{ infoDialog.data.params }}</el-descriptions-item>
|
<el-descriptions-item :span="3" label="连接参数">{{ infoDialog.data.params }}</el-descriptions-item>
|
||||||
@@ -61,7 +65,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, toRefs, reactive, onMounted, defineAsyncComponent, Ref } from 'vue';
|
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
import { dbApi } from './api';
|
import { dbApi } from './api';
|
||||||
import { dateFormat } from '@/common/utils/date';
|
import { dateFormat } from '@/common/utils/date';
|
||||||
@@ -71,6 +75,7 @@ import { hasPerms } from '@/components/auth/auth';
|
|||||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||||
import { getDbDialect } from './dialect';
|
import { getDbDialect } from './dialect';
|
||||||
import { SearchItem } from '@/components/SearchForm';
|
import { SearchItem } from '@/components/SearchForm';
|
||||||
|
import ResourceAuthCert from '../component/ResourceAuthCert.vue';
|
||||||
|
|
||||||
const InstanceEdit = defineAsyncComponent(() => import('./InstanceEdit.vue'));
|
const InstanceEdit = defineAsyncComponent(() => import('./InstanceEdit.vue'));
|
||||||
|
|
||||||
@@ -79,19 +84,20 @@ const perms = {
|
|||||||
delInstance: 'db:instance:del',
|
delInstance: 'db:instance:del',
|
||||||
};
|
};
|
||||||
|
|
||||||
const searchItems = [SearchItem.input('name', '名称')];
|
const searchItems = [SearchItem.input('code', '编号'), SearchItem.input('name', '名称')];
|
||||||
|
|
||||||
const columns = ref([
|
const columns = ref([
|
||||||
|
TableColumn.new('code', '编号'),
|
||||||
TableColumn.new('name', '名称'),
|
TableColumn.new('name', '名称'),
|
||||||
TableColumn.new('type', '类型').isSlot().setAddWidth(-15).alignCenter(),
|
TableColumn.new('type', '类型').isSlot().setAddWidth(-15).alignCenter(),
|
||||||
TableColumn.new('host', 'host:port').setFormatFunc((data: any) => `${data.host}:${data.port}`),
|
TableColumn.new('host', 'host:port').setFormatFunc((data: any) => `${data.host}:${data.port}`),
|
||||||
TableColumn.new('username', '用户名'),
|
TableColumn.new('authCerts[0].username', '授权凭证').isSlot('authCert').setAddWidth(10),
|
||||||
TableColumn.new('params', '连接参数'),
|
TableColumn.new('params', '连接参数'),
|
||||||
TableColumn.new('remark', '备注'),
|
TableColumn.new('remark', '备注'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 该用户拥有的的操作列按钮权限
|
// 该用户拥有的的操作列按钮权限
|
||||||
const actionBtns = hasPerms([perms.saveInstance]);
|
const actionBtns = hasPerms(Object.values(perms));
|
||||||
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(110).fixedRight().alignCenter();
|
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(110).fixedRight().alignCenter();
|
||||||
const pageTableRef: Ref<any> = ref(null);
|
const pageTableRef: Ref<any> = ref(null);
|
||||||
|
|
||||||
@@ -134,6 +140,15 @@ const search = () => {
|
|||||||
pageTableRef.value.search();
|
pageTableRef.value.search();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleData = (res: any) => {
|
||||||
|
const dataList = res.list;
|
||||||
|
// 赋值授权凭证
|
||||||
|
for (let x of dataList) {
|
||||||
|
x.selectAuthCert = x.authCerts[0];
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
const showInfo = (info: any) => {
|
const showInfo = (info: any) => {
|
||||||
state.infoDialog.data = info;
|
state.infoDialog.data = info;
|
||||||
state.infoDialog.visible = true;
|
state.infoDialog.visible = true;
|
||||||
|
|||||||
@@ -71,7 +71,7 @@
|
|||||||
<el-descriptions-item label-align="right">
|
<el-descriptions-item label-align="right">
|
||||||
<template #label>
|
<template #label>
|
||||||
<div>
|
<div>
|
||||||
<SvgIcon :name="getDbDialect(nowDbInst.type).getInfo().icon" :size="18" />
|
<SvgIcon :name="nowDbInst.getDialect().getInfo().icon" :size="18" />
|
||||||
实例
|
实例
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -143,6 +143,7 @@
|
|||||||
:db-id="dt.params.id"
|
:db-id="dt.params.id"
|
||||||
:db="dt.params.db"
|
:db="dt.params.db"
|
||||||
:db-type="dt.params.type"
|
:db-type="dt.params.type"
|
||||||
|
:flow-procdef-key="dt.params.flowProcdefKey"
|
||||||
:height="state.tablesOpHeight"
|
:height="state.tablesOpHeight"
|
||||||
/>
|
/>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
@@ -151,12 +152,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</Pane>
|
</Pane>
|
||||||
</Splitpanes>
|
</Splitpanes>
|
||||||
|
<db-table-op
|
||||||
|
:title="tableCreateDialog.title"
|
||||||
|
:active-name="tableCreateDialog.activeName"
|
||||||
|
:dbId="tableCreateDialog.dbId"
|
||||||
|
:db="tableCreateDialog.db"
|
||||||
|
:dbType="tableCreateDialog.dbType"
|
||||||
|
:flow-procdef-key="tableCreateDialog.flowProcdefKey"
|
||||||
|
:data="tableCreateDialog.data"
|
||||||
|
v-model:visible="tableCreateDialog.visible"
|
||||||
|
@submit-sql="onSubmitEditTableSql"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { defineAsyncComponent, onBeforeUnmount, onMounted, reactive, ref, toRefs } from 'vue';
|
import { defineAsyncComponent, h, onBeforeUnmount, onMounted, reactive, ref, toRefs } from 'vue';
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
import { ElCheckbox, ElMessage, ElMessageBox } from 'element-plus';
|
||||||
import { formatByteSize } from '@/common/utils/format';
|
import { formatByteSize } from '@/common/utils/format';
|
||||||
import { DbInst, registerDbCompletionItemProvider, TabInfo, TabType } from './db';
|
import { DbInst, registerDbCompletionItemProvider, TabInfo, TabType } from './db';
|
||||||
import { NodeType, TagTreeNode } from '../component/tag';
|
import { NodeType, TagTreeNode } from '../component/tag';
|
||||||
@@ -165,12 +177,14 @@ import { dbApi } from './api';
|
|||||||
import { dispposeCompletionItemProvider } from '@/components/monaco/completionItemProvider';
|
import { dispposeCompletionItemProvider } from '@/components/monaco/completionItemProvider';
|
||||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||||
import { ContextmenuItem } from '@/components/contextmenu';
|
import { ContextmenuItem } from '@/components/contextmenu';
|
||||||
import { DbType, getDbDialect } from './dialect/index';
|
import { getDbDialect, schemaDbTypes } from './dialect/index';
|
||||||
import { sleep } from '@/common/utils/loading';
|
import { sleep } from '@/common/utils/loading';
|
||||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||||
import { Pane, Splitpanes } from 'splitpanes';
|
import { Pane, Splitpanes } from 'splitpanes';
|
||||||
import { useEventListener } from '@vueuse/core';
|
import { useEventListener } from '@vueuse/core';
|
||||||
|
import SqlExecBox from '@/views/ops/db/component/sqleditor/SqlExecBox';
|
||||||
|
|
||||||
|
const DbTableOp = defineAsyncComponent(() => import('./component/table/DbTableOp.vue'));
|
||||||
const DbSqlEditor = defineAsyncComponent(() => import('./component/sqleditor/DbSqlEditor.vue'));
|
const DbSqlEditor = defineAsyncComponent(() => import('./component/sqleditor/DbSqlEditor.vue'));
|
||||||
const DbTableDataOp = defineAsyncComponent(() => import('./component/table/DbTableDataOp.vue'));
|
const DbTableDataOp = defineAsyncComponent(() => import('./component/table/DbTableDataOp.vue'));
|
||||||
const DbTablesOp = defineAsyncComponent(() => import('./component/table/DbTablesOp.vue'));
|
const DbTablesOp = defineAsyncComponent(() => import('./component/table/DbTablesOp.vue'));
|
||||||
@@ -214,25 +228,40 @@ const SqlIcon = {
|
|||||||
const nodeClickChangeDb = (nodeData: TagTreeNode) => {
|
const nodeClickChangeDb = (nodeData: TagTreeNode) => {
|
||||||
const params = nodeData.params;
|
const params = nodeData.params;
|
||||||
if (params.db) {
|
if (params.db) {
|
||||||
changeDb({ id: params.id, host: `${params.host}`, name: params.name, type: params.type, tagPath: params.tagPath, databases: params.dbs }, params.db);
|
changeDb(
|
||||||
|
{
|
||||||
|
id: params.id,
|
||||||
|
host: `${params.host}`,
|
||||||
|
name: params.name,
|
||||||
|
type: params.type,
|
||||||
|
tagPath: params.tagPath,
|
||||||
|
databases: params.dbs,
|
||||||
|
flowProcdefKey: params.flowProcdefKey,
|
||||||
|
},
|
||||||
|
params.db
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// tagpath 节点类型
|
const ContextmenuItemRefresh = new ContextmenuItem('refresh', '刷新').withIcon('RefreshRight').withOnClick((data: any) => reloadNode(data.key));
|
||||||
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 [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 防止过快加载会出现一闪而过,对眼睛不好
|
// tagpath 节点类型
|
||||||
await sleep(100);
|
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath)
|
||||||
return dbInfos?.map((x: any) => {
|
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
x.tagPath = parentNode.key;
|
const dbInfoRes = await dbApi.dbs.request({ tagPath: parentNode.key });
|
||||||
return new TagTreeNode(`${parentNode.key}.${x.id}`, x.name, NodeTypeDbInst).withParams(x);
|
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);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.withContextMenuItems([ContextmenuItemRefresh]);
|
||||||
|
|
||||||
// 数据库实例节点类型
|
// 数据库实例节点类型
|
||||||
const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((parentNode: TagTreeNode) => {
|
const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((parentNode: TagTreeNode) => {
|
||||||
@@ -248,6 +277,7 @@ const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((p
|
|||||||
host: `${params.host}:${params.port}`,
|
host: `${params.host}:${params.port}`,
|
||||||
dbs: dbs,
|
dbs: dbs,
|
||||||
db: x,
|
db: x,
|
||||||
|
flowProcdefKey: params.flowProcdefKey,
|
||||||
})
|
})
|
||||||
.withIcon(DbIcon);
|
.withIcon(DbIcon);
|
||||||
});
|
});
|
||||||
@@ -255,12 +285,12 @@ const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((p
|
|||||||
|
|
||||||
// 数据库节点
|
// 数据库节点
|
||||||
const NodeTypeDb = new NodeType(SqlExecNodeType.Db)
|
const NodeTypeDb = new NodeType(SqlExecNodeType.Db)
|
||||||
.withContextMenuItems([new ContextmenuItem('reloadTables', '刷新').withIcon('RefreshRight').withOnClick((data: any) => reloadNode(data.key))])
|
.withContextMenuItems([ContextmenuItemRefresh])
|
||||||
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
const params = parentNode.params;
|
const params = parentNode.params;
|
||||||
|
params.parentKey = parentNode.key;
|
||||||
// pg类数据库会多一层schema
|
// pg类数据库会多一层schema
|
||||||
if (params.type == DbType.postgresql || params.type === DbType.dm || params.type === DbType.oracle) {
|
if (schemaDbTypes.includes(params.type)) {
|
||||||
const params = parentNode.params;
|
|
||||||
const { id, db } = params;
|
const { id, db } = params;
|
||||||
const schemaNames = await dbApi.pgSchemas.request({ id, db });
|
const schemaNames = await dbApi.pgSchemas.request({ id, db });
|
||||||
return schemaNames.map((sn: any) => {
|
return schemaNames.map((sn: any) => {
|
||||||
@@ -269,33 +299,37 @@ const NodeTypeDb = new NodeType(SqlExecNodeType.Db)
|
|||||||
nParams.schema = sn;
|
nParams.schema = sn;
|
||||||
nParams.db = nParams.db + '/' + sn;
|
nParams.db = nParams.db + '/' + sn;
|
||||||
nParams.dbs = schemaNames;
|
nParams.dbs = schemaNames;
|
||||||
return new TagTreeNode(`${params.id}.${params.db}.schema.${sn}`, sn, NodeTypePostgresScheam).withParams(nParams).withIcon(SchemaIcon);
|
return new TagTreeNode(`${params.id}.${params.db}.schema.${sn}`, sn, NodeTypePostgresSchema).withParams(nParams).withIcon(SchemaIcon);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return [
|
return NodeTypeTables(params);
|
||||||
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);
|
.withNodeClickFunc(nodeClickChangeDb);
|
||||||
|
|
||||||
|
const NodeTypeTables = (params: any) => {
|
||||||
|
let tableKey = `${params.id}.${params.db}.table-menu`;
|
||||||
|
let sqlKey = getSqlMenuNodeKey(params.id, params.db);
|
||||||
|
return [
|
||||||
|
new TagTreeNode(`${params.id}.${params.db}.table-menu`, '表', NodeTypeTableMenu).withParams({ ...params, key: tableKey }).withIcon(TableIcon),
|
||||||
|
new TagTreeNode(sqlKey, 'SQL', NodeTypeSqlMenu).withParams({ ...params, key: sqlKey }).withIcon(SqlIcon),
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
// postgres schema模式
|
// postgres schema模式
|
||||||
const NodeTypePostgresScheam = new NodeType(SqlExecNodeType.PgSchema)
|
const NodeTypePostgresSchema = new NodeType(SqlExecNodeType.PgSchema)
|
||||||
.withContextMenuItems([new ContextmenuItem('reloadTables', '刷新').withIcon('RefreshRight').withOnClick((data: any) => reloadNode(data.key))])
|
.withContextMenuItems([ContextmenuItemRefresh])
|
||||||
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
const params = parentNode.params;
|
const params = parentNode.params;
|
||||||
return [
|
params.parentKey = parentNode.key;
|
||||||
new TagTreeNode(`${params.id}.${params.db}.table-menu`, '表', NodeTypeTableMenu).withParams(params).withIcon(TableIcon),
|
return NodeTypeTables(params);
|
||||||
new TagTreeNode(getSqlMenuNodeKey(params.id, params.db), 'SQL', NodeTypeSqlMenu).withParams(params).withIcon(SqlIcon),
|
|
||||||
];
|
|
||||||
})
|
})
|
||||||
.withNodeClickFunc(nodeClickChangeDb);
|
.withNodeClickFunc(nodeClickChangeDb);
|
||||||
|
|
||||||
// 数据库表菜单节点
|
// 数据库表菜单节点
|
||||||
const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu)
|
const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu)
|
||||||
.withContextMenuItems([
|
.withContextMenuItems([
|
||||||
new ContextmenuItem('reloadTables', '刷新').withIcon('RefreshRight').withOnClick((data: any) => reloadNode(data.key)),
|
ContextmenuItemRefresh,
|
||||||
|
new ContextmenuItem('createTable', '创建表').withIcon('Plus').withOnClick((data: any) => onEditTable(data)),
|
||||||
new ContextmenuItem('tablesOp', '表操作').withIcon('Setting').withOnClick((data: any) => {
|
new ContextmenuItem('tablesOp', '表操作').withIcon('Setting').withOnClick((data: any) => {
|
||||||
const params = data.params;
|
const params = data.params;
|
||||||
addTablesOpTab({ id: params.id, db: params.db, type: params.type, nodeKey: data.key });
|
addTablesOpTab({ id: params.id, db: params.db, type: params.type, nodeKey: data.key });
|
||||||
@@ -303,30 +337,40 @@ const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu)
|
|||||||
])
|
])
|
||||||
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
const params = parentNode.params;
|
const params = parentNode.params;
|
||||||
let { id, db } = params;
|
let { id, db, type, flowProcdefKey, schema } = params;
|
||||||
// 获取当前库的所有表信息
|
// 获取当前库的所有表信息
|
||||||
let tables = await DbInst.getInst(id).loadTables(db, state.reloadStatus);
|
let tables = await DbInst.getInst(id).loadTables(db, state.reloadStatus);
|
||||||
state.reloadStatus = false;
|
state.reloadStatus = false;
|
||||||
let dbTableSize = 0;
|
let dbTableSize = 0;
|
||||||
const tablesNode = tables.map((x: any) => {
|
const tablesNode = tables.map((x: any) => {
|
||||||
dbTableSize += x.dataLength + x.indexLength;
|
const tableSize = x.dataLength + x.indexLength;
|
||||||
return new TagTreeNode(`${id}.${db}.${x.tableName}`, x.tableName, NodeTypeTable)
|
dbTableSize += tableSize;
|
||||||
|
const key = `${id}.${db}.${x.tableName}`;
|
||||||
|
return new TagTreeNode(key, x.tableName, NodeTypeTable)
|
||||||
.withIsLeaf(true)
|
.withIsLeaf(true)
|
||||||
.withParams({
|
.withParams({
|
||||||
id,
|
id,
|
||||||
db,
|
db,
|
||||||
|
type,
|
||||||
|
schema,
|
||||||
|
flowProcdefKey: flowProcdefKey,
|
||||||
|
key: key,
|
||||||
|
parentKey: parentNode.key,
|
||||||
tableName: x.tableName,
|
tableName: x.tableName,
|
||||||
tableComment: x.tableComment,
|
tableComment: x.tableComment,
|
||||||
size: formatByteSize(x.dataLength + x.indexLength, 1),
|
size: tableSize == 0 ? '' : formatByteSize(tableSize, 1),
|
||||||
})
|
})
|
||||||
.withIcon(TableIcon)
|
.withIcon(TableIcon)
|
||||||
.withLabelRemark(`${x.tableName} ${x.tableComment ? '| ' + x.tableComment : ''}`);
|
.withLabelRemark(`${x.tableName} ${x.tableComment ? '| ' + x.tableComment : ''}`);
|
||||||
});
|
});
|
||||||
// 设置父节点参数的表大小
|
// 设置父节点参数的表大小
|
||||||
parentNode.params.dbTableSize = formatByteSize(dbTableSize);
|
parentNode.params.dbTableSize = dbTableSize == 0 ? '' : formatByteSize(dbTableSize);
|
||||||
return tablesNode;
|
return tablesNode;
|
||||||
})
|
})
|
||||||
.withNodeClickFunc(nodeClickChangeDb);
|
.withNodeDblclickFunc((node: TagTreeNode) => {
|
||||||
|
const params = node.params;
|
||||||
|
addTablesOpTab({ id: params.id, db: params.db, type: params.type, nodeKey: node.key });
|
||||||
|
});
|
||||||
|
|
||||||
// 数据库sql模板菜单节点
|
// 数据库sql模板菜单节点
|
||||||
const NodeTypeSqlMenu = new NodeType(SqlExecNodeType.SqlMenu)
|
const NodeTypeSqlMenu = new NodeType(SqlExecNodeType.SqlMenu)
|
||||||
@@ -340,22 +384,24 @@ const NodeTypeSqlMenu = new NodeType(SqlExecNodeType.SqlMenu)
|
|||||||
return sqls.map((x: any) => {
|
return sqls.map((x: any) => {
|
||||||
return new TagTreeNode(`${id}.${db}.${x.name}`, x.name, NodeTypeSql)
|
return new TagTreeNode(`${id}.${db}.${x.name}`, x.name, NodeTypeSql)
|
||||||
.withIsLeaf(true)
|
.withIsLeaf(true)
|
||||||
.withParams({
|
.withParams({ id, db, dbs, sqlName: x.name })
|
||||||
id,
|
|
||||||
db,
|
|
||||||
dbs,
|
|
||||||
sqlName: x.name,
|
|
||||||
})
|
|
||||||
.withIcon(SqlIcon);
|
.withIcon(SqlIcon);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.withNodeClickFunc(nodeClickChangeDb);
|
.withNodeClickFunc(nodeClickChangeDb);
|
||||||
|
|
||||||
// 表节点类型
|
// 表节点类型
|
||||||
const NodeTypeTable = new NodeType(SqlExecNodeType.Table).withNodeClickFunc((nodeData: TagTreeNode) => {
|
const NodeTypeTable = new NodeType(SqlExecNodeType.Table)
|
||||||
const params = nodeData.params;
|
.withContextMenuItems([
|
||||||
loadTableData({ id: params.id, nodeKey: nodeData.key }, params.db, params.tableName);
|
new ContextmenuItem('copyTable', '复制表').withIcon('copyDocument').withOnClick((data: any) => onCopyTable(data)),
|
||||||
});
|
new ContextmenuItem('renameTable', '重命名').withIcon('edit').withOnClick((data: any) => onRenameTable(data)),
|
||||||
|
new ContextmenuItem('editTable', '编辑表').withIcon('edit').withOnClick((data: any) => onEditTable(data)),
|
||||||
|
new ContextmenuItem('delTable', '删除表').withIcon('Delete').withOnClick((data: any) => onDeleteTable(data)),
|
||||||
|
])
|
||||||
|
.withNodeClickFunc((nodeData: TagTreeNode) => {
|
||||||
|
const params = nodeData.params;
|
||||||
|
loadTableData({ id: params.id, nodeKey: nodeData.key }, params.db, params.tableName);
|
||||||
|
});
|
||||||
|
|
||||||
// sql模板节点类型
|
// sql模板节点类型
|
||||||
const NodeTypeSql = new NodeType(SqlExecNodeType.Sql)
|
const NodeTypeSql = new NodeType(SqlExecNodeType.Sql)
|
||||||
@@ -385,9 +431,20 @@ const state = reactive({
|
|||||||
loading: true,
|
loading: true,
|
||||||
version: '',
|
version: '',
|
||||||
},
|
},
|
||||||
|
tableCreateDialog: {
|
||||||
|
visible: false,
|
||||||
|
title: '',
|
||||||
|
activeName: '',
|
||||||
|
dbId: 0,
|
||||||
|
db: '',
|
||||||
|
dbType: '',
|
||||||
|
flowProcdefKey: '',
|
||||||
|
data: {},
|
||||||
|
parentKey: '',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { nowDbInst } = toRefs(state);
|
const { nowDbInst, tableCreateDialog } = toRefs(state);
|
||||||
|
|
||||||
const serverInfoReqParam = ref({
|
const serverInfoReqParam = ref({
|
||||||
instanceId: 0,
|
instanceId: 0,
|
||||||
@@ -408,7 +465,7 @@ onBeforeUnmount(() => {
|
|||||||
* 设置editor高度和数据表高度
|
* 设置editor高度和数据表高度
|
||||||
*/
|
*/
|
||||||
const setHeight = () => {
|
const setHeight = () => {
|
||||||
state.dataTabsTableHeight = window.innerHeight - 270 + 'px';
|
state.dataTabsTableHeight = window.innerHeight - 253 + 'px';
|
||||||
state.tablesOpHeight = window.innerHeight - 225 + 'px';
|
state.tablesOpHeight = window.innerHeight - 225 + 'px';
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -603,6 +660,127 @@ const reloadNode = (nodeKey: string) => {
|
|||||||
tagTreeRef.value.reloadNode(nodeKey);
|
tagTreeRef.value.reloadNode(nodeKey);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onEditTable = async (data: any) => {
|
||||||
|
let { db, id, tableName, tableComment, type, parentKey, key, flowProcdefKey } = data.params;
|
||||||
|
// data.label就是表名
|
||||||
|
if (tableName) {
|
||||||
|
state.tableCreateDialog.title = '修改表';
|
||||||
|
let indexs = await dbApi.tableIndex.request({ id, db, tableName });
|
||||||
|
let columns = await dbApi.columnMetadata.request({ id, db, tableName });
|
||||||
|
let row = { tableName, tableComment };
|
||||||
|
state.tableCreateDialog.data = { edit: true, row, indexs, columns };
|
||||||
|
state.tableCreateDialog.parentKey = parentKey;
|
||||||
|
} else {
|
||||||
|
state.tableCreateDialog.title = '创建表';
|
||||||
|
state.tableCreateDialog.data = { edit: false, row: {} };
|
||||||
|
state.tableCreateDialog.parentKey = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.tableCreateDialog.activeName = '1';
|
||||||
|
state.tableCreateDialog.dbId = id;
|
||||||
|
state.tableCreateDialog.db = db;
|
||||||
|
state.tableCreateDialog.dbType = type;
|
||||||
|
state.tableCreateDialog.flowProcdefKey = flowProcdefKey;
|
||||||
|
state.tableCreateDialog.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDeleteTable = async (data: any) => {
|
||||||
|
let { db, id, tableName, parentKey, flowProcdefKey, schema } = data.params;
|
||||||
|
await ElMessageBox.confirm(`此操作是永久性且无法撤销,确定删除【${tableName}】? `, '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 执行sql
|
||||||
|
let dialect = getDbDialect(state.nowDbInst.type);
|
||||||
|
let schemaStr = schema ? `${dialect.quoteIdentifier(schema)}.` : '';
|
||||||
|
|
||||||
|
dbApi.sqlExec.request({ id, db, sql: `drop table ${schemaStr + dialect.quoteIdentifier(tableName)}` }).then(() => {
|
||||||
|
if (flowProcdefKey) {
|
||||||
|
ElMessage.success('工单提交成功');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ElMessage.success('删除成功');
|
||||||
|
setTimeout(() => {
|
||||||
|
parentKey && reloadNode(parentKey);
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRenameTable = async (data: any) => {
|
||||||
|
let { db, id, tableName, parentKey, flowProcdefKey } = data.params;
|
||||||
|
let tableData = { db, oldTableName: tableName, tableName };
|
||||||
|
|
||||||
|
let value = ref(tableName);
|
||||||
|
// 弹出确认框
|
||||||
|
const promptValue = await ElMessageBox.prompt('', `重命名表【${db}.${tableName}】`, {
|
||||||
|
inputValue: value.value,
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
});
|
||||||
|
|
||||||
|
tableData.tableName = promptValue.value;
|
||||||
|
let sql = nowDbInst.value.getDialect().getModifyTableInfoSql(tableData);
|
||||||
|
if (!sql) {
|
||||||
|
ElMessage.warning('无更改');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SqlExecBox({
|
||||||
|
sql: sql,
|
||||||
|
dbId: id as any,
|
||||||
|
db: db as any,
|
||||||
|
dbType: nowDbInst.value.getDialect().getInfo().formatSqlDialect,
|
||||||
|
flowProcdefKey: flowProcdefKey,
|
||||||
|
runSuccessCallback: () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
parentKey && reloadNode(parentKey);
|
||||||
|
}, 1000);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCopyTable = async (data: any) => {
|
||||||
|
let { db, id, tableName, parentKey } = data.params;
|
||||||
|
|
||||||
|
let checked = ref(false);
|
||||||
|
|
||||||
|
// 弹出确认框,并选择是否复制数据
|
||||||
|
await ElMessageBox({
|
||||||
|
title: `复制表【${tableName}】`,
|
||||||
|
type: 'warning',
|
||||||
|
// icon: markRaw(Delete),
|
||||||
|
message: () =>
|
||||||
|
h(ElCheckbox, {
|
||||||
|
label: '是否复制数据?',
|
||||||
|
modelValue: checked.value,
|
||||||
|
'onUpdate:modelValue': (val: boolean | string | number) => {
|
||||||
|
if (typeof val === 'boolean') {
|
||||||
|
checked.value = val;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
callback: (action: string) => {
|
||||||
|
if (action === 'confirm') {
|
||||||
|
// 执行sql
|
||||||
|
dbApi.copyTable.request({ id, db, tableName, copyData: checked.value }).then(() => {
|
||||||
|
ElMessage.success('复制成功');
|
||||||
|
setTimeout(() => {
|
||||||
|
parentKey && reloadNode(parentKey);
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmitEditTableSql = () => {
|
||||||
|
state.tableCreateDialog.visible = false;
|
||||||
|
state.tableCreateDialog.data = { edit: false, row: {} };
|
||||||
|
reloadNode(state.tableCreateDialog.parentKey);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当前操作的数据库信息
|
* 获取当前操作的数据库信息
|
||||||
*/
|
*/
|
||||||
@@ -614,6 +792,7 @@ const getNowDbInfo = () => {
|
|||||||
name: di.name,
|
name: di.name,
|
||||||
type: di.type,
|
type: di.type,
|
||||||
host: di.host,
|
host: di.host,
|
||||||
|
flowProcdefKey: di.flowProcdefKey,
|
||||||
dbName: state.db,
|
dbName: state.db,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<el-row>
|
<el-row>
|
||||||
<el-col :span="11">
|
<el-col :span="11">
|
||||||
<el-form-item prop="taskName" label="任务名" required>
|
<el-form-item prop="taskName" label="任务名" required>
|
||||||
<el-input v-model.trim="form.taskName" placeholder="请输入数据库别名" auto-complete="off" />
|
<el-input v-model.trim="form.taskName" placeholder="请输入同步任务名" auto-complete="off" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
|
||||||
@@ -45,8 +45,10 @@
|
|||||||
<db-select-tree
|
<db-select-tree
|
||||||
placeholder="请选择源数据库"
|
placeholder="请选择源数据库"
|
||||||
v-model:db-id="form.srcDbId"
|
v-model:db-id="form.srcDbId"
|
||||||
|
v-model:inst-name="form.srcInstName"
|
||||||
v-model:db-name="form.srcDbName"
|
v-model:db-name="form.srcDbName"
|
||||||
v-model:tag-path="form.srcTagPath"
|
v-model:tag-path="form.srcTagPath"
|
||||||
|
v-model:db-type="form.srcDbType"
|
||||||
@select-db="onSelectSrcDb"
|
@select-db="onSelectSrcDb"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -55,8 +57,10 @@
|
|||||||
<db-select-tree
|
<db-select-tree
|
||||||
placeholder="请选择目标数据库"
|
placeholder="请选择目标数据库"
|
||||||
v-model:db-id="form.targetDbId"
|
v-model:db-id="form.targetDbId"
|
||||||
|
v-model:inst-name="form.targetInstName"
|
||||||
v-model:db-name="form.targetDbName"
|
v-model:db-name="form.targetDbName"
|
||||||
v-model:tag-path="form.targetTagPath"
|
v-model:tag-path="form.targetTagPath"
|
||||||
|
v-model:db-type="form.targetDbType"
|
||||||
@select-db="onSelectTargetDb"
|
@select-db="onSelectTargetDb"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -85,9 +89,11 @@
|
|||||||
</el-col>
|
</el-col>
|
||||||
|
|
||||||
<el-col :span="8">
|
<el-col :span="8">
|
||||||
<el-form-item prop="updField" label="更新字段" required>
|
<el-tooltip content="查询数据源的时候会带上这个字段当前最大值,支持带别名,如:t.create_time" placement="top">
|
||||||
<el-input v-model.trim="form.updField" placeholder="查询数据源的时候会带上这个字段当前最大值" auto-complete="off" />
|
<el-form-item prop="updField" label="更新字段" required>
|
||||||
</el-form-item>
|
<el-input v-model.trim="form.updField" placeholder="查询数据源的时候会带上这个字段当前最大值" auto-complete="off" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-tooltip>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
|
||||||
<el-col :span="8">
|
<el-col :span="8">
|
||||||
@@ -109,7 +115,7 @@
|
|||||||
<el-option
|
<el-option
|
||||||
v-for="item in state.targetColumnList"
|
v-for="item in state.targetColumnList"
|
||||||
:key="item.columnName"
|
:key="item.columnName"
|
||||||
:label="item.columnName + ` ${item.columnType}` + (item.columnComment && ' - ' + item.columnComment)"
|
:label="item.columnName + ` ${item.showDataType}` + (item.columnComment && ' - ' + item.columnComment)"
|
||||||
:value="item.columnName"
|
:value="item.columnName"
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
@@ -121,10 +127,17 @@
|
|||||||
|
|
||||||
<el-tab-pane label="sql预览" :name="sqlPreviewTab" :disabled="!baseFieldCompleted">
|
<el-tab-pane label="sql预览" :name="sqlPreviewTab" :disabled="!baseFieldCompleted">
|
||||||
<el-form-item prop="fieldMap" label="查询sql">
|
<el-form-item prop="fieldMap" label="查询sql">
|
||||||
<el-input type="textarea" v-model="state.previewDataSql" readonly :input-style="{ height: '190px' }" />
|
<el-input type="textarea" v-model="state.previewDataSql" readonly :input-style="{ height: '170px' }" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item prop="fieldMap" label="插入sql">
|
<el-form-item prop="fieldMap" label="插入sql">
|
||||||
<el-input type="textarea" v-model="state.previewInsertSql" readonly :input-style="{ height: '190px' }" />
|
<el-input type="textarea" v-model="state.previewInsertSql" readonly :input-style="{ height: '170px' }" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="isReplace" v-if="compatibleDuplicateStrategy(form.targetDbType!)" label="键冲突策略">
|
||||||
|
<el-select v-model="form.duplicateStrategy" @change="handleDuplicateStrategy" style="width: 100px">
|
||||||
|
<el-option label="无" :value="DuplicateStrategy.NONE" />
|
||||||
|
<el-option label="忽略" :value="DuplicateStrategy.IGNORE" />
|
||||||
|
<el-option label="替换" :value="DuplicateStrategy.REPLACE" />
|
||||||
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
@@ -181,7 +194,7 @@ import { ElMessage } from 'element-plus';
|
|||||||
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
|
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
|
||||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||||
import { DbInst, registerDbCompletionItemProvider } from '@/views/ops/db/db';
|
import { DbInst, registerDbCompletionItemProvider } from '@/views/ops/db/db';
|
||||||
import { getDbDialect } from '@/views/ops/db/dialect';
|
import { compatibleDuplicateStrategy, DbType, DuplicateStrategy, getDbDialect } from '@/views/ops/db/dialect';
|
||||||
import CrontabInput from '@/components/crontab/CrontabInput.vue';
|
import CrontabInput from '@/components/crontab/CrontabInput.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -226,18 +239,23 @@ type FormData = {
|
|||||||
taskName?: string;
|
taskName?: string;
|
||||||
taskCron: string;
|
taskCron: string;
|
||||||
srcDbId?: number;
|
srcDbId?: number;
|
||||||
|
srcInstName?: string;
|
||||||
srcDbName?: string;
|
srcDbName?: string;
|
||||||
|
srcDbType?: string;
|
||||||
srcTagPath?: string;
|
srcTagPath?: string;
|
||||||
targetDbId?: number;
|
targetDbId?: number;
|
||||||
|
targetInstName?: string;
|
||||||
targetDbName?: string;
|
targetDbName?: string;
|
||||||
targetTagPath?: string;
|
targetTagPath?: string;
|
||||||
targetTableName?: string;
|
targetTableName?: string;
|
||||||
|
targetDbType?: string;
|
||||||
dataSql?: string;
|
dataSql?: string;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
updField?: string;
|
updField?: string;
|
||||||
updFieldVal?: string;
|
updFieldVal?: string;
|
||||||
fieldMap?: { src: string; target: string }[];
|
fieldMap?: { src: string; target: string }[];
|
||||||
status?: 1 | 2;
|
status?: 1 | 2;
|
||||||
|
duplicateStrategy?: -1 | 1 | 2;
|
||||||
};
|
};
|
||||||
|
|
||||||
const basicFormData = {
|
const basicFormData = {
|
||||||
@@ -245,10 +263,11 @@ const basicFormData = {
|
|||||||
targetDbId: -1,
|
targetDbId: -1,
|
||||||
dataSql: 'select * from',
|
dataSql: 'select * from',
|
||||||
pageSize: 1000,
|
pageSize: 1000,
|
||||||
updField: 'id',
|
updField: '',
|
||||||
updFieldVal: '0',
|
updFieldVal: '0',
|
||||||
fieldMap: [{ src: 'a', target: 'b' }],
|
fieldMap: [{ src: 'a', target: 'b' }],
|
||||||
status: 1,
|
status: 1,
|
||||||
|
duplicateStrategy: -1,
|
||||||
} as FormData;
|
} as FormData;
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
@@ -263,6 +282,7 @@ const state = reactive({
|
|||||||
previewRes: {} as any,
|
previewRes: {} as any,
|
||||||
previewDataSql: '',
|
previewDataSql: '',
|
||||||
previewInsertSql: '',
|
previewInsertSql: '',
|
||||||
|
previewFieldArr: [] as string[],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { tabActiveName, form, submitForm } = toRefs(state);
|
const { tabActiveName, form, submitForm } = toRefs(state);
|
||||||
@@ -281,12 +301,17 @@ watch(dialogVisible, async (newValue: boolean) => {
|
|||||||
state.tabActiveName = 'basic';
|
state.tabActiveName = 'basic';
|
||||||
const propsData = props.data as any;
|
const propsData = props.data as any;
|
||||||
if (!propsData?.id) {
|
if (!propsData?.id) {
|
||||||
state.form = basicFormData;
|
let d = {} as FormData;
|
||||||
|
Object.assign(d, basicFormData);
|
||||||
|
state.form = d;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let data = await dbApi.getDatasyncTask.request({ taskId: propsData?.id });
|
let data = await dbApi.getDatasyncTask.request({ taskId: propsData?.id });
|
||||||
state.form = data;
|
state.form = data;
|
||||||
|
if (!state.form.duplicateStrategy) {
|
||||||
|
state.form.duplicateStrategy = -1;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
state.form.fieldMap = JSON.parse(data.fieldMap);
|
state.form.fieldMap = JSON.parse(data.fieldMap);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -302,6 +327,8 @@ watch(dialogVisible, async (newValue: boolean) => {
|
|||||||
// 初始化实例
|
// 初始化实例
|
||||||
db.databases = db.database?.split(' ').sort() || [];
|
db.databases = db.database?.split(' ').sort() || [];
|
||||||
state.srcDbInst = DbInst.getOrNewInst(db);
|
state.srcDbInst = DbInst.getOrNewInst(db);
|
||||||
|
state.form.srcDbType = state.srcDbInst.type;
|
||||||
|
state.form.srcInstName = db.instanceName;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化target数据源
|
// 初始化target数据源
|
||||||
@@ -312,6 +339,8 @@ watch(dialogVisible, async (newValue: boolean) => {
|
|||||||
// 初始化实例
|
// 初始化实例
|
||||||
db.databases = db.database?.split(' ').sort() || [];
|
db.databases = db.database?.split(' ').sort() || [];
|
||||||
state.targetDbInst = DbInst.getOrNewInst(db);
|
state.targetDbInst = DbInst.getOrNewInst(db);
|
||||||
|
state.form.targetDbType = state.targetDbInst.type;
|
||||||
|
state.form.targetInstName = db.instanceName;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetDbId && state.form.targetDbName) {
|
if (targetDbId && state.form.targetDbName) {
|
||||||
@@ -331,13 +360,12 @@ watch(tabActiveName, async (newValue: string) => {
|
|||||||
await handleGetTargetFields();
|
await handleGetTargetFields();
|
||||||
break;
|
break;
|
||||||
case sqlPreviewTab:
|
case sqlPreviewTab:
|
||||||
let srcDbDialect = getDbDialect(state.srcDbInst.type);
|
|
||||||
let targetDbDialect = getDbDialect(state.targetDbInst.type);
|
let targetDbDialect = getDbDialect(state.targetDbInst.type);
|
||||||
|
let updField = state.form.updField!;
|
||||||
|
|
||||||
let updField = srcDbDialect.quoteIdentifier(state.form.updField!);
|
// 判断sql是否以where .*结尾
|
||||||
state.previewDataSql = `SELECT * FROM (\n ${state.form.dataSql?.trim() || '请输入数据sql'} \n ) t \n where ${updField} > '${
|
let hasCondition = /where/i.test(state.form.dataSql!);
|
||||||
state.form.updFieldVal || ''
|
state.previewDataSql = `${state.form.dataSql?.trim() || '请输入数据sql'} \n ${hasCondition ? 'and' : 'where'} ${updField} > '${state.form.updFieldVal || ''}'`;
|
||||||
}'`;
|
|
||||||
|
|
||||||
// 检查字段映射中是否存在重复的目标字段
|
// 检查字段映射中是否存在重复的目标字段
|
||||||
let fields = new Set();
|
let fields = new Set();
|
||||||
@@ -353,17 +381,19 @@ watch(tabActiveName, async (newValue: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let fieldArr = state.form.fieldMap?.map((a: any) => targetDbDialect.quoteIdentifier(a.target)) || [];
|
let fieldArr = state.form.fieldMap?.map((a: any) => targetDbDialect.quoteIdentifier(a.target)) || [];
|
||||||
let placeholder = '?'.repeat(fieldArr.length).split('').join(',');
|
state.previewFieldArr = fieldArr;
|
||||||
|
refreshPreviewInsertSql();
|
||||||
state.previewInsertSql = ` insert into ${targetDbDialect.quoteIdentifier(state.form.targetTableName!)}(${fieldArr.join(
|
|
||||||
','
|
|
||||||
)}) values (${placeholder});`;
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const refreshPreviewInsertSql = () => {
|
||||||
|
let targetDbDialect = getDbDialect(state.targetDbInst.type);
|
||||||
|
state.previewInsertSql = targetDbDialect.getBatchInsertPreviewSql(state.form.targetTableName!, state.previewFieldArr, state.form.duplicateStrategy!);
|
||||||
|
};
|
||||||
|
|
||||||
const onSelectSrcDb = async (params: any) => {
|
const onSelectSrcDb = async (params: any) => {
|
||||||
// 初始化数据源
|
// 初始化数据源
|
||||||
params.databases = params.dbs; // 数据源里需要这个值
|
params.databases = params.dbs; // 数据源里需要这个值
|
||||||
@@ -396,8 +426,8 @@ const handleGetSrcFields = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 判断sql是否是查询语句
|
// 判断sql是否是查询语句
|
||||||
if (!/^select/i.test(state.form.dataSql!)) {
|
if (!/^select/i.test(state.form.dataSql.trim()!)) {
|
||||||
let msg = 'sql语句错误,请输入查询语句';
|
let msg = 'sql语句错误,请输入select语句';
|
||||||
ElMessage.warning(msg);
|
ElMessage.warning(msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -410,10 +440,24 @@ const handleGetSrcFields = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 执行sql
|
// 执行sql
|
||||||
|
let sql: string;
|
||||||
|
|
||||||
|
if (state.form.srcDbType === DbType.mssql) {
|
||||||
|
// mssql的分页语法不一样
|
||||||
|
let top1 = `select top 1`;
|
||||||
|
sql = `${top1} * from (${state.form.dataSql}) a`;
|
||||||
|
} else if (state.form.srcDbType === DbType.oracle) {
|
||||||
|
// oracle的分页关键字不一样
|
||||||
|
let hasCondition = /where/i.test(state.form.dataSql!);
|
||||||
|
sql = `${state.form.dataSql} ${hasCondition ? 'and' : 'where'} rownum <= 1`;
|
||||||
|
} else {
|
||||||
|
sql = `${state.form.dataSql} limit 1`;
|
||||||
|
}
|
||||||
|
|
||||||
const res = await dbApi.sqlExec.request({
|
const res = await dbApi.sqlExec.request({
|
||||||
id: state.form.srcDbId,
|
id: state.form.srcDbId,
|
||||||
db: state.form.srcDbName,
|
db: state.form.srcDbName,
|
||||||
sql: state.form.dataSql.trim() + ' limit 1',
|
sql,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.columns) {
|
if (!res.columns) {
|
||||||
@@ -487,6 +531,11 @@ const btnOk = async () => {
|
|||||||
const cancel = () => {
|
const cancel = () => {
|
||||||
dialogVisible.value = false;
|
dialogVisible.value = false;
|
||||||
emit('cancel');
|
emit('cancel');
|
||||||
|
state.form = basicFormData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDuplicateStrategy = () => {
|
||||||
|
refreshPreviewInsertSql();
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|||||||
@@ -71,11 +71,13 @@ const searchItems = [SearchItem.input('name', '名称')];
|
|||||||
// 任务名、修改人、修改时间、最近一次任务执行状态、状态(停用启用)、操作
|
// 任务名、修改人、修改时间、最近一次任务执行状态、状态(停用启用)、操作
|
||||||
const columns = ref([
|
const columns = ref([
|
||||||
TableColumn.new('taskName', '任务名'),
|
TableColumn.new('taskName', '任务名'),
|
||||||
TableColumn.new('runningState', '运行状态').alignCenter().typeTag(DbDataSyncRunningStateEnum),
|
TableColumn.new('runningState', '运行状态').typeTag(DbDataSyncRunningStateEnum),
|
||||||
TableColumn.new('recentState', '最近任务状态').alignCenter().typeTag(DbDataSyncRecentStateEnum),
|
TableColumn.new('recentState', '最近任务状态').typeTag(DbDataSyncRecentStateEnum),
|
||||||
TableColumn.new('status', '状态').alignCenter().isSlot(),
|
TableColumn.new('status', '状态').isSlot(),
|
||||||
TableColumn.new('modifier', '修改人').alignCenter(),
|
TableColumn.new('creator', '创建人'),
|
||||||
TableColumn.new('updateTime', '修改时间').alignCenter().isTime(),
|
TableColumn.new('createTime', '创建时间').isTime(),
|
||||||
|
TableColumn.new('modifier', '修改人'),
|
||||||
|
TableColumn.new('updateTime', '修改时间').isTime(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 该用户拥有的的操作列按钮权限
|
// 该用户拥有的的操作列按钮权限
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export const dbApi = {
|
|||||||
tableInfos: Api.newGet('/dbs/{id}/t-infos'),
|
tableInfos: Api.newGet('/dbs/{id}/t-infos'),
|
||||||
tableIndex: Api.newGet('/dbs/{id}/t-index'),
|
tableIndex: Api.newGet('/dbs/{id}/t-index'),
|
||||||
tableDdl: Api.newGet('/dbs/{id}/t-create-ddl'),
|
tableDdl: Api.newGet('/dbs/{id}/t-create-ddl'),
|
||||||
|
copyTable: Api.newPost('/dbs/{id}/copy-table'),
|
||||||
columnMetadata: Api.newGet('/dbs/{id}/c-metadata'),
|
columnMetadata: Api.newGet('/dbs/{id}/c-metadata'),
|
||||||
pgSchemas: Api.newGet('/dbs/{id}/pg/schemas'),
|
pgSchemas: Api.newGet('/dbs/{id}/pg/schemas'),
|
||||||
// 获取表即列提示
|
// 获取表即列提示
|
||||||
@@ -34,7 +35,7 @@ export const dbApi = {
|
|||||||
getSqlNames: Api.newGet('/dbs/{id}/sql-names'),
|
getSqlNames: Api.newGet('/dbs/{id}/sql-names'),
|
||||||
deleteDbSql: Api.newDelete('/dbs/{id}/sql'),
|
deleteDbSql: Api.newDelete('/dbs/{id}/sql'),
|
||||||
// 获取数据库sql执行记录
|
// 获取数据库sql执行记录
|
||||||
getSqlExecs: Api.newGet('/dbs/{dbId}/sql-execs'),
|
getSqlExecs: Api.newGet('/dbs/sql-execs'),
|
||||||
|
|
||||||
instances: Api.newGet('/instances'),
|
instances: Api.newGet('/instances'),
|
||||||
getInstance: Api.newGet('/instances/{instanceId}'),
|
getInstance: Api.newGet('/instances/{instanceId}'),
|
||||||
@@ -42,22 +43,25 @@ export const dbApi = {
|
|||||||
getInstanceServerInfo: Api.newGet('/instances/{instanceId}/server-info'),
|
getInstanceServerInfo: Api.newGet('/instances/{instanceId}/server-info'),
|
||||||
testConn: Api.newPost('/instances/test-conn'),
|
testConn: Api.newPost('/instances/test-conn'),
|
||||||
saveInstance: Api.newPost('/instances'),
|
saveInstance: Api.newPost('/instances'),
|
||||||
getInstancePwd: Api.newGet('/instances/{id}/pwd'),
|
|
||||||
deleteInstance: Api.newDelete('/instances/{id}'),
|
deleteInstance: Api.newDelete('/instances/{id}'),
|
||||||
|
|
||||||
// 获取数据库备份列表
|
// 获取数据库备份列表
|
||||||
getDbBackups: Api.newGet('/dbs/{dbId}/backups'),
|
getDbBackups: Api.newGet('/dbs/{dbId}/backups'),
|
||||||
createDbBackup: Api.newPost('/dbs/{dbId}/backups'),
|
createDbBackup: Api.newPost('/dbs/{dbId}/backups'),
|
||||||
|
deleteDbBackup: Api.newDelete('/dbs/{dbId}/backups/{backupId}'),
|
||||||
getDbNamesWithoutBackup: Api.newGet('/dbs/{dbId}/db-names-without-backup'),
|
getDbNamesWithoutBackup: Api.newGet('/dbs/{dbId}/db-names-without-backup'),
|
||||||
enableDbBackup: Api.newPut('/dbs/{dbId}/backups/{backupId}/enable'),
|
enableDbBackup: Api.newPut('/dbs/{dbId}/backups/{backupId}/enable'),
|
||||||
disableDbBackup: Api.newPut('/dbs/{dbId}/backups/{backupId}/disable'),
|
disableDbBackup: Api.newPut('/dbs/{dbId}/backups/{backupId}/disable'),
|
||||||
startDbBackup: Api.newPut('/dbs/{dbId}/backups/{backupId}/start'),
|
startDbBackup: Api.newPut('/dbs/{dbId}/backups/{backupId}/start'),
|
||||||
saveDbBackup: Api.newPut('/dbs/{dbId}/backups/{id}'),
|
saveDbBackup: Api.newPut('/dbs/{dbId}/backups/{id}'),
|
||||||
getDbBackupHistories: Api.newGet('/dbs/{dbId}/backup-histories'),
|
getDbBackupHistories: Api.newGet('/dbs/{dbId}/backup-histories'),
|
||||||
|
restoreDbBackupHistory: Api.newPost('/dbs/{dbId}/backup-histories/{backupHistoryId}/restore'),
|
||||||
|
deleteDbBackupHistory: Api.newDelete('/dbs/{dbId}/backup-histories/{backupHistoryId}'),
|
||||||
|
|
||||||
// 获取数据库备份列表
|
// 获取数据库恢复列表
|
||||||
getDbRestores: Api.newGet('/dbs/{dbId}/restores'),
|
getDbRestores: Api.newGet('/dbs/{dbId}/restores'),
|
||||||
createDbRestore: Api.newPost('/dbs/{dbId}/restores'),
|
createDbRestore: Api.newPost('/dbs/{dbId}/restores'),
|
||||||
|
deleteDbRestore: Api.newDelete('/dbs/{dbId}/restores/{restoreId}'),
|
||||||
getDbNamesWithoutRestore: Api.newGet('/dbs/{dbId}/db-names-without-restore'),
|
getDbNamesWithoutRestore: Api.newGet('/dbs/{dbId}/db-names-without-restore'),
|
||||||
enableDbRestore: Api.newPut('/dbs/{dbId}/restores/{restoreId}/enable'),
|
enableDbRestore: Api.newPut('/dbs/{dbId}/restores/{restoreId}/enable'),
|
||||||
disableDbRestore: Api.newPut('/dbs/{dbId}/restores/{restoreId}/disable'),
|
disableDbRestore: Api.newPut('/dbs/{dbId}/restores/{restoreId}/disable'),
|
||||||
@@ -78,4 +82,17 @@ export const dbApi = {
|
|||||||
runDatasyncTask: Api.newPost('/datasync/tasks/{taskId}/run'),
|
runDatasyncTask: Api.newPost('/datasync/tasks/{taskId}/run'),
|
||||||
stopDatasyncTask: Api.newPost('/datasync/tasks/{taskId}/stop'),
|
stopDatasyncTask: Api.newPost('/datasync/tasks/{taskId}/stop'),
|
||||||
datasyncLogs: Api.newGet('/datasync/tasks/{taskId}/logs'),
|
datasyncLogs: Api.newGet('/datasync/tasks/{taskId}/logs'),
|
||||||
|
|
||||||
|
// 数据库迁移相关
|
||||||
|
dbTransferTasks: Api.newGet('/dbTransfer'),
|
||||||
|
saveDbTransferTask: Api.newPost('/dbTransfer/save'),
|
||||||
|
deleteDbTransferTask: Api.newDelete('/dbTransfer/{taskId}/del'),
|
||||||
|
runDbTransferTask: Api.newPost('/dbTransfer/{taskId}/run'),
|
||||||
|
stopDbTransferTask: Api.newPost('/dbTransfer/{taskId}/stop'),
|
||||||
|
dbTransferTaskLogs: Api.newGet('/dbTransfer/{taskId}/logs'),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dbSqlExecApi = {
|
||||||
|
// 根据业务key获取sql执行信息
|
||||||
|
getSqlExecByBizKey: Api.newGet('/dbs/sql-execs'),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
:resource-type="TagResourceTypeEnum.Db.value"
|
:resource-type="TagResourceTypeEnum.Db.value"
|
||||||
:tag-path-node-type="NodeTypeTagPath"
|
:tag-path-node-type="NodeTypeTagPath"
|
||||||
>
|
>
|
||||||
|
<template #iconPrefix>
|
||||||
|
<SvgIcon v-if="dbType && getDbDialect(dbType)" :name="getDbDialect(dbType).getInfo().icon" :size="18" />
|
||||||
|
</template>
|
||||||
<template #prefix="{ data }">
|
<template #prefix="{ data }">
|
||||||
<SvgIcon v-if="data.type.value == SqlExecNodeType.DbInst" :name="getDbDialect(data.params.type).getInfo().icon" :size="18" />
|
<SvgIcon v-if="data.type.value == SqlExecNodeType.DbInst" :name="getDbDialect(data.params.type).getInfo().icon" :size="18" />
|
||||||
<SvgIcon v-if="data.icon" :name="data.icon.name" :color="data.icon.color" />
|
<SvgIcon v-if="data.icon" :name="data.icon.name" :color="data.icon.color" />
|
||||||
@@ -19,7 +22,7 @@ import { NodeType, TagTreeNode } from '@/views/ops/component/tag';
|
|||||||
import { dbApi } from '@/views/ops/db/api';
|
import { dbApi } from '@/views/ops/db/api';
|
||||||
import { sleep } from '@/common/utils/loading';
|
import { sleep } from '@/common/utils/loading';
|
||||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||||
import { DbType, getDbDialect } from '@/views/ops/db/dialect';
|
import { getDbDialect, noSchemaTypes } from '@/views/ops/db/dialect';
|
||||||
import TagTreeResourceSelect from '../../component/TagTreeResourceSelect.vue';
|
import TagTreeResourceSelect from '../../component/TagTreeResourceSelect.vue';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
@@ -27,15 +30,21 @@ const props = defineProps({
|
|||||||
dbId: {
|
dbId: {
|
||||||
type: Number,
|
type: Number,
|
||||||
},
|
},
|
||||||
|
instName: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
dbName: {
|
dbName: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
tagPath: {
|
tagPath: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
dbType: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emits = defineEmits(['update:dbName', 'update:tagPath', 'update:dbId', 'selectDb']);
|
const emits = defineEmits(['update:dbName', 'update:tagPath', 'update:instName', 'update:dbId', 'update:dbType', 'selectDb']);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 树节点类型
|
* 树节点类型
|
||||||
@@ -53,7 +62,7 @@ class SqlExecNodeType {
|
|||||||
|
|
||||||
const selectNode = computed({
|
const selectNode = computed({
|
||||||
get: () => {
|
get: () => {
|
||||||
return props.dbName ? `${props.tagPath} - ${props.dbId} - ${props.dbName}` : '';
|
return props.dbName ? `${props.tagPath} > ${props.instName} > ${props.dbName}` : '';
|
||||||
},
|
},
|
||||||
set: () => {
|
set: () => {
|
||||||
//
|
//
|
||||||
@@ -87,8 +96,8 @@ const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(asyn
|
|||||||
});
|
});
|
||||||
|
|
||||||
/** mysql类型的数据库,没有schema层 */
|
/** mysql类型的数据库,没有schema层 */
|
||||||
const mysqlType = (type: string) => {
|
const noSchemaType = (type: string) => {
|
||||||
return type === DbType.mysql;
|
return noSchemaTypes.includes(type);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 数据库实例节点类型
|
// 数据库实例节点类型
|
||||||
@@ -96,7 +105,7 @@ const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((p
|
|||||||
const params = parentNode.params;
|
const params = parentNode.params;
|
||||||
const dbs = params.database.split(' ')?.sort();
|
const dbs = params.database.split(' ')?.sort();
|
||||||
let fn: NodeType;
|
let fn: NodeType;
|
||||||
if (mysqlType(params.type)) {
|
if (noSchemaType(params.type)) {
|
||||||
fn = MysqlNodeTypes;
|
fn = MysqlNodeTypes;
|
||||||
} else {
|
} else {
|
||||||
fn = PgNodeTypes;
|
fn = PgNodeTypes;
|
||||||
@@ -114,7 +123,7 @@ const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((p
|
|||||||
db: x,
|
db: x,
|
||||||
})
|
})
|
||||||
.withIcon(DbIcon);
|
.withIcon(DbIcon);
|
||||||
if (mysqlType(params.type)) {
|
if (noSchemaType(params.type)) {
|
||||||
tagTreeNode.isLeaf = true;
|
tagTreeNode.isLeaf = true;
|
||||||
}
|
}
|
||||||
return tagTreeNode;
|
return tagTreeNode;
|
||||||
@@ -148,8 +157,10 @@ const changeNode = (nodeData: TagTreeNode) => {
|
|||||||
const params = nodeData.params;
|
const params = nodeData.params;
|
||||||
// postgres
|
// postgres
|
||||||
emits('update:dbName', params.db);
|
emits('update:dbName', params.db);
|
||||||
|
emits('update:instName', params.name);
|
||||||
emits('update:dbId', params.id);
|
emits('update:dbId', params.id);
|
||||||
emits('update:tagPath', params.tagPath);
|
emits('update:tagPath', params.tagPath);
|
||||||
|
emits('update:dbType', params.type);
|
||||||
emits('selectDb', params);
|
emits('selectDb', params);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -128,12 +128,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { h, nextTick, onMounted, reactive, toRefs, ref, unref } from 'vue';
|
import { h, nextTick, onMounted, reactive, ref, toRefs, unref } from 'vue';
|
||||||
import { getToken } from '@/common/utils/storage';
|
import { getToken } from '@/common/utils/storage';
|
||||||
import { notBlank } from '@/common/assert';
|
import { notBlank } from '@/common/assert';
|
||||||
import { format as sqlFormatter } from 'sql-formatter';
|
import { format as sqlFormatter } from 'sql-formatter';
|
||||||
import config from '@/common/config';
|
import config from '@/common/config';
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
import { ElMessage, ElMessageBox, ElNotification } from 'element-plus';
|
||||||
|
|
||||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||||
import { editor } from 'monaco-editor';
|
import { editor } from 'monaco-editor';
|
||||||
@@ -146,11 +146,9 @@ import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
|||||||
import { joinClientParams } from '@/common/request';
|
import { joinClientParams } from '@/common/request';
|
||||||
import { buildProgressProps } from '@/components/progress-notify/progress-notify';
|
import { buildProgressProps } from '@/components/progress-notify/progress-notify';
|
||||||
import ProgressNotify from '@/components/progress-notify/progress-notify.vue';
|
import ProgressNotify from '@/components/progress-notify/progress-notify.vue';
|
||||||
import { ElNotification } from 'element-plus';
|
|
||||||
import syssocket from '@/common/syssocket';
|
import syssocket from '@/common/syssocket';
|
||||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||||
import { getDbDialect } from '../../dialect';
|
import { Pane, Splitpanes } from 'splitpanes';
|
||||||
import { Splitpanes, Pane } from 'splitpanes';
|
|
||||||
|
|
||||||
const emits = defineEmits(['saveSqlSuccess']);
|
const emits = defineEmits(['saveSqlSuccess']);
|
||||||
|
|
||||||
@@ -299,13 +297,16 @@ const onRunSql = async (newTab = false) => {
|
|||||||
sql = sql.replace(/(^\s*)/g, '');
|
sql = sql.replace(/(^\s*)/g, '');
|
||||||
let execRemark = '';
|
let execRemark = '';
|
||||||
let canRun = true;
|
let canRun = true;
|
||||||
|
|
||||||
|
// 简单截取前十个字符
|
||||||
|
const sqlPrefix = sql.slice(0, 10).toLowerCase();
|
||||||
if (
|
if (
|
||||||
sql.startsWith('update') ||
|
sqlPrefix.startsWith('update') ||
|
||||||
sql.startsWith('UPDATE') ||
|
sqlPrefix.startsWith('insert') ||
|
||||||
sql.startsWith('INSERT') ||
|
sqlPrefix.startsWith('delete') ||
|
||||||
sql.startsWith('insert') ||
|
sqlPrefix.startsWith('alert') ||
|
||||||
sql.startsWith('DELETE') ||
|
sqlPrefix.startsWith('drop') ||
|
||||||
sql.startsWith('delete')
|
sqlPrefix.startsWith('create')
|
||||||
) {
|
) {
|
||||||
const res: any = await ElMessageBox.prompt('请输入备注', 'Tip', {
|
const res: any = await ElMessageBox.prompt('请输入备注', 'Tip', {
|
||||||
confirmButtonText: '确定',
|
confirmButtonText: '确定',
|
||||||
@@ -322,6 +323,18 @@ const onRunSql = async (newTab = false) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 启用工单审批
|
||||||
|
if (execRemark && getNowDbInst().flowProcdefKey) {
|
||||||
|
try {
|
||||||
|
await getNowDbInst().runSql(props.dbName, sql, execRemark);
|
||||||
|
ElMessage.success('工单提交成功');
|
||||||
|
return;
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.success('工单提交失败');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let execRes: ExecResTab;
|
let execRes: ExecResTab;
|
||||||
let i = 0;
|
let i = 0;
|
||||||
let id;
|
let id;
|
||||||
@@ -357,6 +370,7 @@ const onRunSql = async (newTab = false) => {
|
|||||||
const colAndData: any = data.value;
|
const colAndData: any = data.value;
|
||||||
if (!colAndData.res || colAndData.res.length === 0) {
|
if (!colAndData.res || colAndData.res.length === 0) {
|
||||||
ElMessage.warning('未查询到结果集');
|
ElMessage.warning('未查询到结果集');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 要实时响应,故需要用索引改变数据才生效
|
// 要实时响应,故需要用索引改变数据才生效
|
||||||
@@ -453,7 +467,7 @@ const formatSql = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatDialect = getDbDialect(getNowDbInst().type).getInfo().formatSqlDialect;
|
const formatDialect = getNowDbInst().getDialect().getInfo().formatSqlDialect;
|
||||||
|
|
||||||
let sql = monacoEditor.getModel()?.getValueInRange(selection);
|
let sql = monacoEditor.getModel()?.getValueInRange(selection);
|
||||||
// 有选中sql则格式化并替换选中sql, 否则格式化编辑器所有内容
|
// 有选中sql则格式化并替换选中sql, 否则格式化编辑器所有内容
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { h, render, VNode } from 'vue';
|
import { h, render } from 'vue';
|
||||||
import SqlExecDialog from './SqlExecDialog.vue';
|
import SqlExecDialog from './SqlExecDialog.vue';
|
||||||
|
|
||||||
export type SqlExecProps = {
|
export type SqlExecProps = {
|
||||||
@@ -6,31 +6,26 @@ export type SqlExecProps = {
|
|||||||
dbId: number;
|
dbId: number;
|
||||||
db: string;
|
db: string;
|
||||||
dbType?: string;
|
dbType?: string;
|
||||||
|
flowProcdefKey?: string;
|
||||||
runSuccessCallback?: Function;
|
runSuccessCallback?: Function;
|
||||||
cancelCallback?: Function;
|
cancelCallback?: Function;
|
||||||
};
|
};
|
||||||
|
|
||||||
const boxId = 'sql-exec-dialog-id';
|
|
||||||
|
|
||||||
let boxInstance: VNode;
|
|
||||||
|
|
||||||
const SqlExecBox = (props: SqlExecProps): void => {
|
const SqlExecBox = (props: SqlExecProps): void => {
|
||||||
if (!boxInstance) {
|
const propsCancelFn = props.cancelCallback;
|
||||||
const container = document.createElement('div');
|
// 包装取消回调函数,新增销毁组件代码
|
||||||
container.id = boxId;
|
props.cancelCallback = () => {
|
||||||
// 创建 虚拟dom
|
propsCancelFn && propsCancelFn();
|
||||||
boxInstance = h(SqlExecDialog);
|
setTimeout(() => {
|
||||||
// 将虚拟dom渲染到 container dom 上
|
// 销毁组件
|
||||||
render(boxInstance, container);
|
render(null, document.body);
|
||||||
// 最后将 container 追加到 body 上
|
}, 500);
|
||||||
document.body.appendChild(container);
|
};
|
||||||
}
|
const vnode = h(SqlExecDialog, {
|
||||||
|
...props,
|
||||||
const boxVue = boxInstance.component;
|
visible: true,
|
||||||
if (boxVue) {
|
});
|
||||||
// 调用open方法显示弹框,注意不能使用boxVue.ctx来调用组件函数(build打包后ctx会获取不到)
|
render(vnode, document.body);
|
||||||
boxVue.exposed?.open(props);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SqlExecBox;
|
export default SqlExecBox;
|
||||||
|
|||||||
@@ -1,8 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<el-dialog title="待执行SQL" v-model="dialogVisible" :show-close="false" width="600px" @close="cancel">
|
<el-dialog title="待执行SQL" v-model="dialogVisible" :show-close="false" width="600px">
|
||||||
<monaco-editor height="300px" class="codesql" language="sql" v-model="sqlValue" />
|
<monaco-editor height="300px" class="codesql" language="sql" v-model="sqlValue" />
|
||||||
<el-input @keyup.enter="runSql" ref="remarkInputRef" v-model="remark" placeholder="请输入执行备注" class="mt5" />
|
<el-input
|
||||||
|
@keyup.enter="runSql"
|
||||||
|
ref="remarkInputRef"
|
||||||
|
v-model="remark"
|
||||||
|
:placeholder="props.flowProcdefKey ? '执行备注(必填)' : '执行备注(选填)'"
|
||||||
|
class="mt5"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div v-if="props.flowProcdefKey">
|
||||||
|
<el-divider content-position="left">审批节点</el-divider>
|
||||||
|
<procdef-tasks :procdef-key="props.flowProcdefKey" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<span class="dialog-footer">
|
<span class="dialog-footer">
|
||||||
<el-button @click="cancel">取 消</el-button>
|
<el-button @click="cancel">取 消</el-button>
|
||||||
@@ -14,52 +26,40 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { toRefs, ref, nextTick, reactive } from 'vue';
|
import { toRefs, ref, reactive, onMounted } from 'vue';
|
||||||
import { dbApi } from '@/views/ops/db/api';
|
import { dbApi } from '@/views/ops/db/api';
|
||||||
import { ElDialog, ElButton, ElInput, ElMessage, InputInstance } from 'element-plus';
|
import { ElDialog, ElButton, ElInput, ElMessage, InputInstance, ElDivider } from 'element-plus';
|
||||||
// import base style
|
// import base style
|
||||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||||
import { format as sqlFormatter } from 'sql-formatter';
|
import { format as sqlFormatter } from 'sql-formatter';
|
||||||
|
|
||||||
import { SqlExecProps } from './SqlExecBox';
|
import { SqlExecProps } from './SqlExecBox';
|
||||||
|
import ProcdefTasks from '@/views/flow/components/ProcdefTasks.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = withDefaults(defineProps<SqlExecProps>(), {});
|
||||||
visible: {
|
|
||||||
type: Boolean,
|
|
||||||
},
|
|
||||||
dbId: {
|
|
||||||
type: [Number],
|
|
||||||
},
|
|
||||||
db: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
sql: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const remarkInputRef = ref<InputInstance>();
|
const remarkInputRef = ref<InputInstance>();
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
dialogVisible: false,
|
dialogVisible: false,
|
||||||
sqlValue: '',
|
sqlValue: '',
|
||||||
dbId: 0,
|
|
||||||
db: '',
|
|
||||||
remark: '',
|
remark: '',
|
||||||
btnLoading: false,
|
btnLoading: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { dialogVisible, sqlValue, remark, btnLoading } = toRefs(state);
|
const { dialogVisible, sqlValue, remark, btnLoading } = toRefs(state);
|
||||||
|
|
||||||
state.sqlValue = props.sql as any;
|
|
||||||
let runSuccessCallback: any;
|
|
||||||
let cancelCallback: any;
|
|
||||||
let runSuccess: boolean = false;
|
let runSuccess: boolean = false;
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
open();
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行sql
|
* 执行sql
|
||||||
*/
|
*/
|
||||||
const runSql = async () => {
|
const runSql = async () => {
|
||||||
if (!state.remark) {
|
// 存在流程审批,则备注为必填
|
||||||
|
if (!state.remark && props.flowProcdefKey) {
|
||||||
ElMessage.error('请输入执行的备注信息');
|
ElMessage.error('请输入执行的备注信息');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -67,12 +67,19 @@ const runSql = async () => {
|
|||||||
try {
|
try {
|
||||||
state.btnLoading = true;
|
state.btnLoading = true;
|
||||||
const res = await dbApi.sqlExec.request({
|
const res = await dbApi.sqlExec.request({
|
||||||
id: state.dbId,
|
id: props.dbId,
|
||||||
db: state.db,
|
db: props.db,
|
||||||
remark: state.remark,
|
remark: state.remark,
|
||||||
sql: state.sqlValue.trim(),
|
sql: state.sqlValue.trim(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 存在流程审批
|
||||||
|
if (props.flowProcdefKey) {
|
||||||
|
runSuccess = false;
|
||||||
|
ElMessage.success('工单提交成功');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (let re of res.res) {
|
for (let re of res.res) {
|
||||||
if (re.result !== 'success') {
|
if (re.result !== 'success') {
|
||||||
ElMessage.error(`${re.sql} \n执行失败: ${re.result}`);
|
ElMessage.error(`${re.sql} \n执行失败: ${re.result}`);
|
||||||
@@ -84,45 +91,33 @@ const runSql = async () => {
|
|||||||
ElMessage.success('执行成功');
|
ElMessage.success('执行成功');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
runSuccess = false;
|
runSuccess = false;
|
||||||
}
|
} finally {
|
||||||
if (runSuccess) {
|
if (runSuccess) {
|
||||||
if (runSuccessCallback) {
|
if (props.runSuccessCallback) {
|
||||||
runSuccessCallback();
|
props.runSuccessCallback();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
state.btnLoading = false;
|
||||||
cancel();
|
cancel();
|
||||||
}
|
}
|
||||||
state.btnLoading = false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const cancel = () => {
|
const cancel = () => {
|
||||||
state.dialogVisible = false;
|
state.dialogVisible = false;
|
||||||
// 没有执行成功,并且取消回调函数存在,则执行
|
props.cancelCallback && props.cancelCallback();
|
||||||
if (!runSuccess && cancelCallback) {
|
|
||||||
cancelCallback();
|
|
||||||
}
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
state.dbId = 0;
|
|
||||||
state.sqlValue = '';
|
state.sqlValue = '';
|
||||||
state.remark = '';
|
state.remark = '';
|
||||||
runSuccessCallback = null;
|
|
||||||
cancelCallback = null;
|
|
||||||
runSuccess = false;
|
runSuccess = false;
|
||||||
}, 200);
|
}, 200);
|
||||||
};
|
};
|
||||||
|
|
||||||
const open = (props: SqlExecProps) => {
|
const open = () => {
|
||||||
runSuccessCallback = props.runSuccessCallback;
|
state.sqlValue = sqlFormatter(props.sql, { language: props.dbType || 'mysql' });
|
||||||
cancelCallback = props.cancelCallback;
|
|
||||||
props.dbType = props.dbType || 'mysql';
|
|
||||||
state.sqlValue = sqlFormatter(props.sql, { language: props.dbType });
|
|
||||||
state.dbId = props.dbId;
|
|
||||||
state.db = props.db;
|
|
||||||
state.dialogVisible = true;
|
state.dialogVisible = true;
|
||||||
nextTick(() => {
|
setTimeout(() => {
|
||||||
setTimeout(() => {
|
remarkInputRef.value?.focus();
|
||||||
remarkInputRef.value?.focus();
|
}, 200);
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
defineExpose({ open });
|
defineExpose({ open });
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
<el-input
|
<el-input
|
||||||
v-if="dataType == DataType.String"
|
v-if="dataType == DataType.String"
|
||||||
:ref="(el: any) => focus && el?.focus()"
|
:ref="(el: any) => focus && el?.focus()"
|
||||||
|
:disabled="disabled"
|
||||||
@blur="handleBlur"
|
@blur="handleBlur"
|
||||||
:class="`w100 mb4 ${showEditorIcon ? 'string-input-container-show-icon' : ''}`"
|
:class="`w100 mb4 ${showEditorIcon ? 'string-input-container-show-icon' : ''}`"
|
||||||
input-style="text-align: center; height: 26px;"
|
|
||||||
size="small"
|
size="small"
|
||||||
v-model="itemValue"
|
v-model="itemValue"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
@@ -16,9 +16,9 @@
|
|||||||
<el-input
|
<el-input
|
||||||
v-else-if="dataType == DataType.Number"
|
v-else-if="dataType == DataType.Number"
|
||||||
:ref="(el: any) => focus && el?.focus()"
|
:ref="(el: any) => focus && el?.focus()"
|
||||||
|
:disabled="disabled"
|
||||||
@blur="handleBlur"
|
@blur="handleBlur"
|
||||||
class="w100 mb4"
|
class="w100 mb4"
|
||||||
input-style="text-align: center; height: 26px;"
|
|
||||||
size="small"
|
size="small"
|
||||||
v-model.number="itemValue"
|
v-model.number="itemValue"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
@@ -28,6 +28,7 @@
|
|||||||
<el-date-picker
|
<el-date-picker
|
||||||
v-else-if="dataType == DataType.Date"
|
v-else-if="dataType == DataType.Date"
|
||||||
:ref="(el: any) => focus && el?.focus()"
|
:ref="(el: any) => focus && el?.focus()"
|
||||||
|
:disabled="disabled"
|
||||||
@change="emit('blur')"
|
@change="emit('blur')"
|
||||||
@blur="handleBlur"
|
@blur="handleBlur"
|
||||||
class="edit-time-picker mb4"
|
class="edit-time-picker mb4"
|
||||||
@@ -43,6 +44,7 @@
|
|||||||
<el-date-picker
|
<el-date-picker
|
||||||
v-else-if="dataType == DataType.DateTime"
|
v-else-if="dataType == DataType.DateTime"
|
||||||
:ref="(el: any) => focus && el?.focus()"
|
:ref="(el: any) => focus && el?.focus()"
|
||||||
|
:disabled="disabled"
|
||||||
@change="handleBlur"
|
@change="handleBlur"
|
||||||
@blur="handleBlur"
|
@blur="handleBlur"
|
||||||
class="edit-time-picker mb4"
|
class="edit-time-picker mb4"
|
||||||
@@ -58,6 +60,7 @@
|
|||||||
<el-time-picker
|
<el-time-picker
|
||||||
v-else-if="dataType == DataType.Time"
|
v-else-if="dataType == DataType.Time"
|
||||||
:ref="(el: any) => focus && el?.focus()"
|
:ref="(el: any) => focus && el?.focus()"
|
||||||
|
:disabled="disabled"
|
||||||
@change="handleBlur"
|
@change="handleBlur"
|
||||||
@blur="handleBlur"
|
@blur="handleBlur"
|
||||||
class="edit-time-picker mb4"
|
class="edit-time-picker mb4"
|
||||||
@@ -71,7 +74,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { Ref, ref, computed } from 'vue';
|
import { computed, ref, Ref } from 'vue';
|
||||||
import { ElInput } from 'element-plus';
|
import { ElInput } from 'element-plus';
|
||||||
import { DataType } from '../../dialect/index';
|
import { DataType } from '../../dialect/index';
|
||||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||||
@@ -83,11 +86,13 @@ export interface ColumnFormItemProps {
|
|||||||
focus?: boolean; // 是否获取焦点
|
focus?: boolean; // 是否获取焦点
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
columnName?: string;
|
columnName?: string;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<ColumnFormItemProps>(), {
|
const props = withDefaults(defineProps<ColumnFormItemProps>(), {
|
||||||
focus: false,
|
focus: false,
|
||||||
dataType: DataType.String,
|
dataType: DataType.String,
|
||||||
|
disabled: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue', 'blur']);
|
const emit = defineEmits(['update:modelValue', 'blur']);
|
||||||
@@ -178,9 +183,6 @@ const getEditorLangByValue = (value: any) => {
|
|||||||
.el-input__prefix {
|
.el-input__prefix {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.el-input__inner {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-time-picker-popper {
|
.edit-time-picker-popper {
|
||||||
|
|||||||
@@ -46,14 +46,6 @@
|
|||||||
<b :title="column.remark" class="el-text" style="cursor: pointer">
|
<b :title="column.remark" class="el-text" style="cursor: pointer">
|
||||||
{{ column.title }}
|
{{ column.title }}
|
||||||
</b>
|
</b>
|
||||||
|
|
||||||
<span>
|
|
||||||
<SvgIcon
|
|
||||||
color="var(--el-color-primary)"
|
|
||||||
v-if="column.title == nowSortColumn?.columnName"
|
|
||||||
:name="nowSortColumn?.order == 'asc' ? 'top' : 'bottom'"
|
|
||||||
></SvgIcon>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 字段备注信息 -->
|
<!-- 字段备注信息 -->
|
||||||
@@ -71,6 +63,13 @@
|
|||||||
{{ column.title }}
|
{{ column.title }}
|
||||||
</b>
|
</b>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 字段列右部分内容 -->
|
||||||
|
<div class="column-right">
|
||||||
|
<span v-if="column.title == nowSortColumn?.columnName">
|
||||||
|
<SvgIcon color="var(--el-color-primary)" :name="nowSortColumn?.order == 'asc' ? 'top' : 'bottom'"></SvgIcon>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -138,13 +137,25 @@
|
|||||||
<el-input v-model="state.genTxtDialog.txt" type="textarea" rows="20" />
|
<el-input v-model="state.genTxtDialog.txt" type="textarea" rows="20" />
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<DbTableDataForm
|
||||||
|
v-if="state.tableDataFormDialog.visible"
|
||||||
|
:db-inst="getNowDbInst()"
|
||||||
|
:db-name="db"
|
||||||
|
:columns="columns!"
|
||||||
|
:title="state.tableDataFormDialog.title"
|
||||||
|
:table-name="table"
|
||||||
|
v-model:visible="state.tableDataFormDialog.visible"
|
||||||
|
v-model="state.tableDataFormDialog.data"
|
||||||
|
@submit-success="emits('changeUpdatedField')"
|
||||||
|
/>
|
||||||
|
|
||||||
<contextmenu :dropdown="state.contextmenu.dropdown" :items="state.contextmenu.items" ref="contextmenuRef" />
|
<contextmenu :dropdown="state.contextmenu.dropdown" :items="state.contextmenu.items" ref="contextmenuRef" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onBeforeUnmount, onMounted, reactive, ref, toRefs, watch } from 'vue';
|
import { onBeforeUnmount, onMounted, reactive, ref, toRefs, watch } from 'vue';
|
||||||
import { ElInput } from 'element-plus';
|
import { ElInput, ElMessage } from 'element-plus';
|
||||||
import { copyToClipboard } from '@/common/utils/string';
|
import { copyToClipboard } from '@/common/utils/string';
|
||||||
import { DbInst } from '@/views/ops/db/db';
|
import { DbInst } from '@/views/ops/db/db';
|
||||||
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
|
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
|
||||||
@@ -154,6 +165,7 @@ import { dateStrFormat } from '@/common/utils/date';
|
|||||||
import { useIntervalFn, useStorage } from '@vueuse/core';
|
import { useIntervalFn, useStorage } from '@vueuse/core';
|
||||||
import { ColumnTypeSubscript, compatibleMysql, DataType, DbDialect, getDbDialect } from '../../dialect/index';
|
import { ColumnTypeSubscript, compatibleMysql, DataType, DbDialect, getDbDialect } from '../../dialect/index';
|
||||||
import ColumnFormItem from './ColumnFormItem.vue';
|
import ColumnFormItem from './ColumnFormItem.vue';
|
||||||
|
import DbTableDataForm from './DbTableDataForm.vue';
|
||||||
|
|
||||||
const emits = defineEmits(['dataDelete', 'sortChange', 'deleteData', 'selectionChange', 'changeUpdatedField']);
|
const emits = defineEmits(['dataDelete', 'sortChange', 'deleteData', 'selectionChange', 'changeUpdatedField']);
|
||||||
|
|
||||||
@@ -247,6 +259,13 @@ const cmDataDel = new ContextmenuItem('deleteData', '删除')
|
|||||||
return state.table == '';
|
return state.table == '';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const cmDataEdit = new ContextmenuItem('editData', '编辑行')
|
||||||
|
.withIcon('edit')
|
||||||
|
.withOnClick(() => onEditRowData())
|
||||||
|
.withHideFunc(() => {
|
||||||
|
return state.table == '';
|
||||||
|
});
|
||||||
|
|
||||||
const cmDataGenInsertSql = new ContextmenuItem('genInsertSql', 'Insert SQL')
|
const cmDataGenInsertSql = new ContextmenuItem('genInsertSql', 'Insert SQL')
|
||||||
.withIcon('tickets')
|
.withIcon('tickets')
|
||||||
.withOnClick(() => onGenerateInsertSql())
|
.withOnClick(() => onGenerateInsertSql())
|
||||||
@@ -333,7 +352,11 @@ const state = reactive({
|
|||||||
},
|
},
|
||||||
items: [] as ContextmenuItem[],
|
items: [] as ContextmenuItem[],
|
||||||
},
|
},
|
||||||
|
tableDataFormDialog: {
|
||||||
|
data: {},
|
||||||
|
title: '',
|
||||||
|
visible: false,
|
||||||
|
},
|
||||||
genTxtDialog: {
|
genTxtDialog: {
|
||||||
title: 'SQL',
|
title: 'SQL',
|
||||||
visible: false,
|
visible: false,
|
||||||
@@ -438,13 +461,13 @@ const formatDataValues = (datas: any) => {
|
|||||||
|
|
||||||
for (let data of datas) {
|
for (let data of datas) {
|
||||||
for (let column of props.columns!) {
|
for (let column of props.columns!) {
|
||||||
data[column.columnName] = getFormatTimeValue(dbDialect.getDataType(column.columnType), data[column.columnName]);
|
data[column.columnName] = getFormatTimeValue(dbDialect.getDataType(column.dataType), data[column.columnName]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const setTableData = (datas: any) => {
|
const setTableData = (datas: any) => {
|
||||||
tableRef.value.scrollTo({ scrollLeft: 0, scrollTop: 0 });
|
tableRef.value?.scrollTo({ scrollLeft: 0, scrollTop: 0 });
|
||||||
selectionRowsMap.clear();
|
selectionRowsMap.clear();
|
||||||
cellUpdateMap.clear();
|
cellUpdateMap.clear();
|
||||||
formatDataValues(datas);
|
formatDataValues(datas);
|
||||||
@@ -456,9 +479,9 @@ const setTableColumns = (columns: any) => {
|
|||||||
state.columns = columns.map((x: any) => {
|
state.columns = columns.map((x: any) => {
|
||||||
const columnName = x.columnName;
|
const columnName = x.columnName;
|
||||||
// 数据类型
|
// 数据类型
|
||||||
x.dataType = dbDialect.getDataType(x.columnType);
|
x.dataType = dbDialect.getDataType(x.dataType);
|
||||||
x.dataTypeSubscript = ColumnTypeSubscript[x.dataType];
|
x.dataTypeSubscript = ColumnTypeSubscript[x.dataType];
|
||||||
x.remark = `${x.columnType} ${x.columnComment ? ' | ' + x.columnComment : ''}`;
|
x.remark = `${x.showDataType} ${x.columnComment ? ' | ' + x.columnComment : ''}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...x,
|
...x,
|
||||||
@@ -576,7 +599,7 @@ const dataContextmenuClick = (event: any, rowIndex: number, column: any, data: a
|
|||||||
const { clientX, clientY } = event;
|
const { clientX, clientY } = event;
|
||||||
state.contextmenu.dropdown.x = clientX;
|
state.contextmenu.dropdown.x = clientX;
|
||||||
state.contextmenu.dropdown.y = clientY;
|
state.contextmenu.dropdown.y = clientY;
|
||||||
state.contextmenu.items = [cmDataCopyCell, cmDataDel, cmDataGenInsertSql, cmDataGenJson, cmDataExportCsv, cmDataExportSql];
|
state.contextmenu.items = [cmDataCopyCell, cmDataDel, cmDataEdit, cmDataGenInsertSql, cmDataGenJson, cmDataExportCsv, cmDataExportSql];
|
||||||
contextmenuRef.value.openContextmenu({ column, rowData: data });
|
contextmenuRef.value.openContextmenu({ column, rowData: data });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -601,6 +624,18 @@ const onDeleteData = async () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onEditRowData = () => {
|
||||||
|
const selectionDatas = Array.from(selectionRowsMap.values());
|
||||||
|
if (selectionDatas.length > 1) {
|
||||||
|
ElMessage.warning('只能编辑一行数据');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = selectionDatas[0];
|
||||||
|
state.tableDataFormDialog.data = data;
|
||||||
|
state.tableDataFormDialog.title = `编辑表'${props.table}'数据`;
|
||||||
|
state.tableDataFormDialog.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
const onGenerateInsertSql = async () => {
|
const onGenerateInsertSql = async () => {
|
||||||
const selectionDatas = Array.from(selectionRowsMap.values());
|
const selectionDatas = Array.from(selectionRowsMap.values());
|
||||||
state.genTxtDialog.txt = await getNowDbInst().genInsertSql(state.db, state.table, selectionDatas);
|
state.genTxtDialog.txt = await getNowDbInst().genInsertSql(state.db, state.table, selectionDatas);
|
||||||
@@ -714,48 +749,28 @@ const submitUpdateFields = async () => {
|
|||||||
|
|
||||||
const db = state.db;
|
const db = state.db;
|
||||||
let res = '';
|
let res = '';
|
||||||
const dbDialect = getDbDialect(dbInst.type);
|
|
||||||
|
|
||||||
for (let updateRow of cellUpdateMap.values()) {
|
for (let updateRow of cellUpdateMap.values()) {
|
||||||
let sql = `UPDATE ${dbInst.wrapName(state.table)} SET `;
|
const rowData = { ...updateRow.rowData };
|
||||||
const rowData = updateRow.rowData;
|
let updateColumnValue = {};
|
||||||
// 主键列信息
|
|
||||||
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()) {
|
for (let k of updateRow.columnsMap.keys()) {
|
||||||
const v = updateRow.columnsMap.get(k);
|
const v = updateRow.columnsMap.get(k);
|
||||||
if (!v) {
|
if (!v) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// 更新字段列信息
|
updateColumnValue[k] = rowData[k];
|
||||||
const updateColumn = await dbInst.loadTableColumn(db, state.table, k);
|
// 将更新的字段对应的原始数据还原(主要应对可能更新修改了主键等)
|
||||||
|
rowData[k] = v.oldValue;
|
||||||
sql += ` ${dbInst.wrapName(k)} = ${DbInst.wrapColumnValue(updateColumn.columnType, rowData[k], dbDialect)},`;
|
|
||||||
|
|
||||||
// 如果修改的字段是主键
|
|
||||||
if (k === primaryKeyName) {
|
|
||||||
primaryKeyValue = v.oldValue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
res += await dbInst.genUpdateSql(db, state.table, updateColumnValue, rowData);
|
||||||
sql = sql.substring(0, sql.length - 1);
|
|
||||||
sql += ` WHERE ${dbInst.wrapName(primaryKeyName)} = ${DbInst.wrapColumnValue(primaryKeyType, primaryKeyValue)} ;`;
|
|
||||||
res += sql;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dbInst.promptExeSql(
|
dbInst.promptExeSql(db, res, cancelUpdateFields, () => {
|
||||||
db,
|
triggerRefresh();
|
||||||
res,
|
cellUpdateMap.clear();
|
||||||
() => {},
|
changeUpdatedField();
|
||||||
() => {
|
});
|
||||||
triggerRefresh();
|
|
||||||
cellUpdateMap.clear();
|
|
||||||
changeUpdatedField();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const cancelUpdateFields = () => {
|
const cancelUpdateFields = () => {
|
||||||
@@ -868,9 +883,15 @@ defineExpose({
|
|||||||
color: var(--el-color-info-light-3);
|
color: var(--el-color-info-light-3);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -7px;
|
top: -5px;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-right {
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
right: 0;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
height: 12px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog v-model="visible" :title="title" :destroy-on-close="true" width="600px">
|
||||||
|
<el-form ref="dataForm" :model="modelValue" :show-message="false" label-width="auto" size="small">
|
||||||
|
<el-form-item
|
||||||
|
v-for="column in columns"
|
||||||
|
:key="column.columnName"
|
||||||
|
class="w100 mb5"
|
||||||
|
:prop="column.columnName"
|
||||||
|
:required="column.nullable != 'YES' && !column.isPrimaryKey && !column.isIdentity"
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
<span class="pointer" :title="`${column.showDataType} | ${column.columnComment}`">
|
||||||
|
{{ column.columnName }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<ColumnFormItem
|
||||||
|
v-model="modelValue[`${column.columnName}`]"
|
||||||
|
:data-type="dbInst.getDialect().getDataType(column.dataType)"
|
||||||
|
:placeholder="`${column.showDataType} ${column.columnComment}`"
|
||||||
|
:column-name="column.columnName"
|
||||||
|
:disabled="column.isIdentity"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="closeDialog">取消</el-button>
|
||||||
|
<el-button type="primary" @click="confirm">确定</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, watch, onMounted } from 'vue';
|
||||||
|
import ColumnFormItem from './ColumnFormItem.vue';
|
||||||
|
import { DbInst } from '../../db';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { getDbDialect } from '@/views/ops/db/dialect';
|
||||||
|
|
||||||
|
export interface ColumnFormItemProps {
|
||||||
|
dbInst: DbInst;
|
||||||
|
dbName: string;
|
||||||
|
tableName: string;
|
||||||
|
columns: any[];
|
||||||
|
title?: string; // dialog title
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<ColumnFormItemProps>(), {
|
||||||
|
title: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const modelValue = defineModel<any>('modelValue');
|
||||||
|
|
||||||
|
const visible = defineModel<boolean>('visible', {
|
||||||
|
default: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['submitSuccess']);
|
||||||
|
|
||||||
|
const dataForm: any = ref(null);
|
||||||
|
|
||||||
|
let oldValue = null as any;
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
setOldValue();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(visible, (newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
setOldValue();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const setOldValue = () => {
|
||||||
|
// 空对象则为insert操作,否则为update
|
||||||
|
if (Object.keys(modelValue.value).length > 0) {
|
||||||
|
oldValue = Object.assign({}, modelValue.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeDialog = () => {
|
||||||
|
visible.value = false;
|
||||||
|
modelValue.value = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirm = async () => {
|
||||||
|
dataForm.value.validate(async (valid: boolean) => {
|
||||||
|
if (!valid) {
|
||||||
|
ElMessage.error('请正确填写数据信息');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dbInst = props.dbInst;
|
||||||
|
const data = modelValue.value;
|
||||||
|
const db = props.dbName;
|
||||||
|
const tableName = props.tableName;
|
||||||
|
|
||||||
|
let sql = '';
|
||||||
|
if (oldValue) {
|
||||||
|
const updateColumnValue = {};
|
||||||
|
Object.keys(oldValue).forEach((key) => {
|
||||||
|
// 如果新旧值不相等,则为需要更新的字段
|
||||||
|
if (oldValue[key] !== modelValue.value[key]) {
|
||||||
|
updateColumnValue[key] = modelValue.value[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
sql = await dbInst.genUpdateSql(db, tableName, updateColumnValue, oldValue);
|
||||||
|
} else {
|
||||||
|
sql = await dbInst.genInsertSql(db, tableName, [data], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
dbInst.promptExeSql(db, sql, null, () => {
|
||||||
|
closeDialog();
|
||||||
|
emit('submitSuccess');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss"></style>
|
||||||
@@ -17,8 +17,8 @@
|
|||||||
<el-checkbox
|
<el-checkbox
|
||||||
v-model="item.show"
|
v-model="item.show"
|
||||||
:label="`${!item.columnComment ? item.columnName : item.columnName + ' [' + item.columnComment + ']'}`"
|
:label="`${!item.columnComment ? item.columnName : item.columnName + ' [' + item.columnComment + ']'}`"
|
||||||
:true-label="true"
|
:true-value="true"
|
||||||
:false-label="false"
|
:false-value="false"
|
||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
title="展示配置"
|
title="展示配置"
|
||||||
trigger="click"
|
trigger="click"
|
||||||
>
|
>
|
||||||
<el-checkbox v-model="dbConfig.showColumnComment" label="显示字段备注" :true-label="true" :false-label="false" size="small" />
|
<el-checkbox v-model="dbConfig.showColumnComment" label="显示字段备注" :true-value="true" :false-value="false" size="small" />
|
||||||
<template #reference>
|
<template #reference>
|
||||||
<el-link type="primary" icon="setting" :underline="false"></el-link>
|
<el-link type="primary" icon="setting" :underline="false"></el-link>
|
||||||
</template>
|
</template>
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
<el-divider direction="vertical" />
|
<el-divider direction="vertical" />
|
||||||
|
|
||||||
<span style="color: var(--el-color-info-light-3)">
|
<span style="color: var(--el-color-info-light-3)">
|
||||||
{{ item.columnType }}
|
{{ item.showDataType }}
|
||||||
|
|
||||||
<template v-if="item.columnComment">
|
<template v-if="item.columnComment">
|
||||||
<el-divider direction="vertical" />
|
<el-divider direction="vertical" />
|
||||||
@@ -158,24 +158,55 @@
|
|||||||
@data-delete="onRefresh"
|
@data-delete="onRefresh"
|
||||||
></db-table-data>
|
></db-table-data>
|
||||||
|
|
||||||
<el-row type="flex" class="mt5" justify="center">
|
<el-row type="flex" class="mt5" :gutter="10" justify="space-between" style="user-select: none">
|
||||||
<el-pagination
|
<el-col :span="12">
|
||||||
small
|
<el-text
|
||||||
:total="count"
|
id="copyValue"
|
||||||
@size-change="handleSizeChange"
|
style="color: var(--el-color-info-light-3)"
|
||||||
@current-change="pageChange()"
|
class="is-truncated font12 mt5"
|
||||||
layout="prev, pager, next, total, sizes, jumper"
|
@click="copyToClipboard(sql)"
|
||||||
v-model:current-page="pageNum"
|
:title="sql"
|
||||||
v-model:page-size="pageSize"
|
>{{ sql }}</el-text
|
||||||
:page-sizes="pageSizes"
|
>
|
||||||
></el-pagination>
|
</el-col>
|
||||||
</el-row>
|
<el-col :span="12">
|
||||||
<div style="font-size: 12px; padding: 0 10px; color: #606266">
|
<el-row :gutter="10" justify="left">
|
||||||
<span>{{ state.sql }}</span>
|
<el-link class="op-page" :underline="false" @click="pageNum = 1" :disabled="pageNum == 1" icon="DArrowLeft" title="首页" />
|
||||||
</div>
|
<el-link class="op-page" :underline="false" @click="pageNum = --pageNum || 1" :disabled="pageNum == 1" icon="Back" title="上一页" />
|
||||||
|
<div class="op-page">
|
||||||
|
<el-input-number
|
||||||
|
style="width: 50px"
|
||||||
|
:controls="false"
|
||||||
|
:min="1"
|
||||||
|
v-model="state.setPageNum"
|
||||||
|
size="small"
|
||||||
|
@blur="handleSetPageNum"
|
||||||
|
@keydown.enter="handleSetPageNum"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<el-link class="op-page" :underline="false" @click="++pageNum" :disabled="datas.length < pageSize" icon="Right" />
|
||||||
|
<el-link class="op-page" :underline="false" @click="handleEndPage" :disabled="datas.length < pageSize" icon="DArrowRight" />
|
||||||
|
<div style="width: 90px" class="op-page ml10">
|
||||||
|
<el-select size="small" :default-first-option="true" v-model="pageSize" @change="handleSizeChange">
|
||||||
|
<el-option
|
||||||
|
style="font-size: 12px; height: 24px; line-height: 24px"
|
||||||
|
v-for="(op, i) in pageSizes"
|
||||||
|
:key="i"
|
||||||
|
:label="op + '条/页'"
|
||||||
|
:value="op"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-dialog v-model="conditionDialog.visible" :title="conditionDialog.title" width="420px">
|
<el-button @click="handleCount" :loading="state.counting" class="ml10" text bg size="small">
|
||||||
<el-row>
|
{{ state.showTotal ? `${state.total} 条` : 'count' }}
|
||||||
|
</el-button>
|
||||||
|
</el-row>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-dialog v-model="conditionDialog.visible" :title="conditionDialog.title" width="460px">
|
||||||
|
<el-row gutter="5">
|
||||||
<el-col :span="5">
|
<el-col :span="5">
|
||||||
<el-select v-model="conditionDialog.condition">
|
<el-select v-model="conditionDialog.condition">
|
||||||
<el-option label="=" value="="> </el-option>
|
<el-option label="=" value="="> </el-option>
|
||||||
@@ -203,31 +234,16 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog v-model="addDataDialog.visible" :title="addDataDialog.title" :destroy-on-close="true" width="600px">
|
<DbTableDataForm
|
||||||
<el-form ref="dataForm" :model="addDataDialog.data" :show-message="false" label-width="auto" size="small">
|
:db-inst="getNowDbInst()"
|
||||||
<el-form-item
|
:db-name="dbName"
|
||||||
v-for="column in columns"
|
:columns="columns"
|
||||||
:key="column.columnName"
|
:title="addDataDialog.title"
|
||||||
class="w100 mb5"
|
:table-name="tableName"
|
||||||
:prop="column.columnName"
|
v-model:visible="addDataDialog.visible"
|
||||||
:label="column.columnName"
|
v-model="addDataDialog.data"
|
||||||
:required="column.nullable != 'YES' && column.columnKey != 'PRI'"
|
@submit-success="onRefresh"
|
||||||
>
|
/>
|
||||||
<ColumnFormItem
|
|
||||||
v-model="addDataDialog.data[`${column.columnName}`]"
|
|
||||||
:data-type="dbDialect.getDataType(column.columnType)"
|
|
||||||
:placeholder="`${column.columnType} ${column.columnComment}`"
|
|
||||||
:column-name="column.columnName"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<span class="dialog-footer">
|
|
||||||
<el-button @click="closeAddDataDialog">取消</el-button>
|
|
||||||
<el-button type="primary" @click="addRow">确定</el-button>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -237,10 +253,11 @@ import { ElMessage } from 'element-plus';
|
|||||||
|
|
||||||
import { DbInst } from '@/views/ops/db/db';
|
import { DbInst } from '@/views/ops/db/db';
|
||||||
import DbTableData from './DbTableData.vue';
|
import DbTableData from './DbTableData.vue';
|
||||||
import { DbDialect, getDbDialect } from '@/views/ops/db/dialect';
|
import { DbDialect } from '@/views/ops/db/dialect';
|
||||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||||
import ColumnFormItem from './ColumnFormItem.vue';
|
|
||||||
import { useEventListener, useStorage } from '@vueuse/core';
|
import { useEventListener, useStorage } from '@vueuse/core';
|
||||||
|
import { copyToClipboard } from '@/common/utils/string';
|
||||||
|
import DbTableDataForm from './DbTableDataForm.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
dbId: {
|
dbId: {
|
||||||
@@ -261,7 +278,6 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const dataForm: any = ref(null);
|
|
||||||
const dbTableRef: Ref = ref(null);
|
const dbTableRef: Ref = ref(null);
|
||||||
const condInputRef: Ref = ref(null);
|
const condInputRef: Ref = ref(null);
|
||||||
const columnNameSearchInputRef: Ref = ref(null);
|
const columnNameSearchInputRef: Ref = ref(null);
|
||||||
@@ -289,7 +305,10 @@ const state = reactive({
|
|||||||
defaultPageSize * 40,
|
defaultPageSize * 40,
|
||||||
defaultPageSize * 80,
|
defaultPageSize * 80,
|
||||||
],
|
],
|
||||||
count: 0,
|
setPageNum: 0,
|
||||||
|
total: 0,
|
||||||
|
showTotal: false,
|
||||||
|
counting: false,
|
||||||
selectionDatas: [] as any,
|
selectionDatas: [] as any,
|
||||||
condPopVisible: false,
|
condPopVisible: false,
|
||||||
columnNameSearch: '',
|
columnNameSearch: '',
|
||||||
@@ -305,7 +324,6 @@ const state = reactive({
|
|||||||
addDataDialog: {
|
addDataDialog: {
|
||||||
data: {},
|
data: {},
|
||||||
title: '',
|
title: '',
|
||||||
placeholder: '',
|
|
||||||
visible: false,
|
visible: false,
|
||||||
},
|
},
|
||||||
tableHeight: '600px',
|
tableHeight: '600px',
|
||||||
@@ -313,7 +331,7 @@ const state = reactive({
|
|||||||
dbDialect: {} as DbDialect,
|
dbDialect: {} as DbDialect,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { datas, condition, loading, columns, pageNum, pageSize, pageSizes, count, hasUpdatedFileds, conditionDialog, addDataDialog, dbDialect } = toRefs(state);
|
const { datas, condition, loading, columns, pageNum, pageSize, pageSizes, sql, hasUpdatedFileds, conditionDialog, addDataDialog } = toRefs(state);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.tableHeight,
|
() => props.tableHeight,
|
||||||
@@ -331,7 +349,7 @@ onMounted(async () => {
|
|||||||
state.tableHeight = props.tableHeight;
|
state.tableHeight = props.tableHeight;
|
||||||
await onRefresh();
|
await onRefresh();
|
||||||
|
|
||||||
state.dbDialect = getDbDialect(getNowDbInst().type);
|
state.dbDialect = getNowDbInst().getDialect();
|
||||||
useEventListener('click', handlerWindowClick);
|
useEventListener('click', handlerWindowClick);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -346,18 +364,19 @@ const onRefresh = async () => {
|
|||||||
await selectData();
|
await selectData();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
watch(
|
||||||
* 数据tab修改页数
|
() => state.pageNum,
|
||||||
*/
|
async () => {
|
||||||
const pageChange = async () => {
|
await selectData();
|
||||||
await selectData();
|
}
|
||||||
};
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 单表数据信息查询数据
|
* 单表数据信息查询数据
|
||||||
*/
|
*/
|
||||||
const selectData = async () => {
|
const selectData = async () => {
|
||||||
state.loading = true;
|
state.loading = true;
|
||||||
|
state.setPageNum = state.pageNum;
|
||||||
const dbInst = getNowDbInst();
|
const dbInst = getNowDbInst();
|
||||||
const db = props.dbName;
|
const db = props.dbName;
|
||||||
const table = props.tableName;
|
const table = props.tableName;
|
||||||
@@ -370,16 +389,10 @@ const selectData = async () => {
|
|||||||
state.columns = columns;
|
state.columns = columns;
|
||||||
}
|
}
|
||||||
|
|
||||||
const countRes = await dbInst.runSql(db, dbInst.getDefaultCountSql(table, state.condition));
|
let sql = dbInst.getDefaultSelectSql(db, table, state.condition, state.orderBy, state.pageNum, state.pageSize);
|
||||||
state.count = countRes.res[0].count || countRes.res[0].COUNT || 0;
|
|
||||||
let sql = dbInst.getDefaultSelectSql(table, state.condition, state.orderBy, state.pageNum, state.pageSize);
|
|
||||||
state.sql = sql;
|
state.sql = sql;
|
||||||
if (state.count > 0) {
|
const colAndData: any = await dbInst.runSql(db, sql);
|
||||||
const colAndData: any = await dbInst.runSql(db, sql);
|
state.datas = colAndData.res;
|
||||||
state.datas = colAndData.res;
|
|
||||||
} else {
|
|
||||||
state.datas = [];
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
}
|
}
|
||||||
@@ -391,6 +404,33 @@ const handleSizeChange = async (size: any) => {
|
|||||||
await selectData();
|
await selectData();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleEndPage = async () => {
|
||||||
|
await handleCount();
|
||||||
|
state.pageNum = Math.ceil(state.total / state.pageSize);
|
||||||
|
await selectData();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSetPageNum = async () => {
|
||||||
|
state.pageNum = state.setPageNum;
|
||||||
|
await selectData();
|
||||||
|
};
|
||||||
|
const handleCount = async () => {
|
||||||
|
state.counting = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const db = props.dbName;
|
||||||
|
const table = props.tableName;
|
||||||
|
const dbInst = getNowDbInst();
|
||||||
|
const countRes = await dbInst.runSql(db, dbInst.getDefaultCountSql(table, state.condition));
|
||||||
|
state.total = parseInt(countRes.res[0].count || countRes.res[0].COUNT || 0);
|
||||||
|
state.showTotal = true;
|
||||||
|
} catch (e) {
|
||||||
|
/* empty */
|
||||||
|
}
|
||||||
|
|
||||||
|
state.counting = false;
|
||||||
|
};
|
||||||
|
|
||||||
// 完整的条件,每次选中后会重置条件框内容,故需要这个变量在获取建议时将文本框内容保存
|
// 完整的条件,每次选中后会重置条件框内容,故需要这个变量在获取建议时将文本框内容保存
|
||||||
let completeCond = '';
|
let completeCond = '';
|
||||||
// 是否存在列建议
|
// 是否存在列建议
|
||||||
@@ -423,7 +463,7 @@ const handlerColumnSelect = (column: any) => {
|
|||||||
// 默认拼接上 columnName =
|
// 默认拼接上 columnName =
|
||||||
let value = column.columnName + ' = ';
|
let value = column.columnName + ' = ';
|
||||||
// 不是数字类型默认拼接上''
|
// 不是数字类型默认拼接上''
|
||||||
if (!DbInst.isNumber(column.columnType)) {
|
if (!DbInst.isNumber(column.dataType)) {
|
||||||
value = `${value} ''`;
|
value = `${value} ''`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -467,7 +507,7 @@ const filterCondColumns = computed(() => {
|
|||||||
const onConditionRowClick = (event: any) => {
|
const onConditionRowClick = (event: any) => {
|
||||||
const row = event[0];
|
const row = event[0];
|
||||||
state.conditionDialog.title = `请输入 [${row.columnName}] 的值`;
|
state.conditionDialog.title = `请输入 [${row.columnName}] 的值`;
|
||||||
state.conditionDialog.placeholder = `${row.columnType} ${row.columnComment}`;
|
state.conditionDialog.placeholder = `${row.showDataType} ${row.columnComment}`;
|
||||||
state.conditionDialog.columnRow = row;
|
state.conditionDialog.columnRow = row;
|
||||||
state.conditionDialog.visible = true;
|
state.conditionDialog.visible = true;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -484,7 +524,7 @@ const onConfirmCondition = () => {
|
|||||||
}
|
}
|
||||||
const row = conditionDialog.columnRow as any;
|
const row = conditionDialog.columnRow as any;
|
||||||
condition += `${row.columnName} ${conditionDialog.condition} `;
|
condition += `${row.columnName} ${conditionDialog.condition} `;
|
||||||
state.condition = condition + DbInst.wrapColumnValue(row.columnType, conditionDialog.value);
|
state.condition = condition + state.dbDialect.wrapValue(row.dataType, conditionDialog.value!);
|
||||||
onCancelCondition();
|
onCancelCondition();
|
||||||
condInputRef.value.focus();
|
condInputRef.value.focus();
|
||||||
};
|
};
|
||||||
@@ -543,40 +583,10 @@ const onShowAddDataDialog = async () => {
|
|||||||
state.addDataDialog.title = `添加'${props.tableName}'表数据`;
|
state.addDataDialog.title = `添加'${props.tableName}'表数据`;
|
||||||
state.addDataDialog.visible = true;
|
state.addDataDialog.visible = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeAddDataDialog = () => {
|
|
||||||
state.addDataDialog.visible = false;
|
|
||||||
state.addDataDialog.data = {};
|
|
||||||
};
|
|
||||||
|
|
||||||
// 添加新数据行
|
|
||||||
const addRow = async () => {
|
|
||||||
dataForm.value.validate(async (valid: boolean) => {
|
|
||||||
if (valid) {
|
|
||||||
const dbInst = getNowDbInst();
|
|
||||||
const data = state.addDataDialog.data;
|
|
||||||
// key: 字段名,value: 字段名提示
|
|
||||||
let obj: any = {};
|
|
||||||
for (let item of state.columns) {
|
|
||||||
const value = data[item.columnName];
|
|
||||||
if (!value) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
obj[`${dbInst.wrapName(item.columnName)}`] = DbInst.wrapValueByType(value);
|
|
||||||
}
|
|
||||||
let columnNames = Object.keys(obj).join(',');
|
|
||||||
let values = Object.values(obj).join(',');
|
|
||||||
let sql = `INSERT INTO ${dbInst.wrapName(props.tableName)} (${columnNames}) VALUES (${values});`;
|
|
||||||
dbInst.promptExeSql(props.dbName, sql, null, () => {
|
|
||||||
closeAddDataDialog();
|
|
||||||
onRefresh();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
ElMessage.error('请正确填写数据信息');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss"></style>
|
<style lang="scss">
|
||||||
|
.op-page {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" width="90%" :close-on-press-escape="false" :close-on-click-modal="false">
|
<el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" width="70%" :close-on-press-escape="false" :close-on-click-modal="false">
|
||||||
<el-form label-position="left" ref="formRef" :model="tableData" label-width="80px">
|
<el-form label-position="left" ref="formRef" :model="tableData" label-width="80px">
|
||||||
<el-row>
|
<el-row>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
@@ -26,11 +26,11 @@
|
|||||||
:width="item.width"
|
:width="item.width"
|
||||||
>
|
>
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-input v-if="item.prop === 'name'" size="small" v-model="scope.row.name"> </el-input>
|
<el-input v-if="item.prop === 'name'" size="small" v-model="scope.row.name" />
|
||||||
|
|
||||||
<el-select v-else-if="item.prop === 'type'" filterable size="small" v-model="scope.row.type">
|
<el-select v-else-if="item.prop === 'type'" filterable size="small" v-model="scope.row.type">
|
||||||
<el-option
|
<el-option
|
||||||
v-for="pgsqlType in state.columnTypeList"
|
v-for="pgsqlType in getDbDialect(dbType).getInfo().columnTypes"
|
||||||
:key="pgsqlType.dataType"
|
:key="pgsqlType.dataType"
|
||||||
:value="pgsqlType.udtName"
|
:value="pgsqlType.udtName"
|
||||||
:label="pgsqlType.dataType"
|
:label="pgsqlType.dataType"
|
||||||
@@ -42,35 +42,30 @@
|
|||||||
</el-option>
|
</el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
|
|
||||||
<el-input v-else-if="item.prop === 'value'" size="small" v-model="scope.row.value"> </el-input>
|
<el-input v-else-if="item.prop === 'value'" size="small" v-model="scope.row.value" />
|
||||||
|
|
||||||
<el-input v-else-if="item.prop === 'length'" size="small" v-model="scope.row.length"> </el-input>
|
<el-input v-else-if="item.prop === 'length'" type="number" size="small" v-model.number="scope.row.length" />
|
||||||
|
|
||||||
<el-input v-else-if="item.prop === 'numScale'" size="small" v-model="scope.row.numScale"> </el-input>
|
<el-input v-else-if="item.prop === 'numScale'" type="number" size="small" v-model.number="scope.row.numScale" />
|
||||||
|
|
||||||
<el-checkbox v-else-if="item.prop === 'notNull'" size="small" v-model="scope.row.notNull"> </el-checkbox>
|
<el-checkbox v-else-if="item.prop === 'notNull'" size="small" v-model="scope.row.notNull" />
|
||||||
|
|
||||||
<el-checkbox v-else-if="item.prop === 'pri'" size="small" v-model="scope.row.pri"> </el-checkbox>
|
<el-checkbox v-else-if="item.prop === 'pri'" size="small" v-model="scope.row.pri" />
|
||||||
|
|
||||||
<el-checkbox
|
<el-checkbox
|
||||||
v-else-if="item.prop === 'auto_increment'"
|
v-else-if="item.prop === 'auto_increment'"
|
||||||
size="small"
|
size="small"
|
||||||
v-model="scope.row.auto_increment"
|
v-model="scope.row.auto_increment"
|
||||||
:disabled="dbType === DbType.postgresql"
|
:disabled="disableEditIncr()"
|
||||||
>
|
/>
|
||||||
</el-checkbox>
|
|
||||||
|
|
||||||
<el-input v-else-if="item.prop === 'remark'" size="small" v-model="scope.row.remark"> </el-input>
|
<el-input v-else-if="item.prop === 'remark'" size="small" v-model="scope.row.remark" />
|
||||||
|
|
||||||
<el-link
|
<el-popconfirm v-else-if="item.prop === 'action'" title="确定删除?" @confirm="deleteRow(scope.$index)">
|
||||||
v-else-if="item.prop === 'action'"
|
<template #reference>
|
||||||
type="danger"
|
<el-link type="danger" plain size="small" :underline="false">删除</el-link>
|
||||||
plain
|
</template>
|
||||||
size="small"
|
</el-popconfirm>
|
||||||
:underline="false"
|
|
||||||
@click.prevent="deleteRow(scope.$index)"
|
|
||||||
>删除</el-link
|
|
||||||
>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -104,21 +99,15 @@
|
|||||||
<el-checkbox v-if="item.prop === 'unique'" size="small" v-model="scope.row.unique" @change="indexChanges(scope.row)">
|
<el-checkbox v-if="item.prop === 'unique'" size="small" v-model="scope.row.unique" @change="indexChanges(scope.row)">
|
||||||
</el-checkbox>
|
</el-checkbox>
|
||||||
|
|
||||||
<el-select v-if="item.prop === 'indexType'" disabled size="small" v-model="scope.row.indexType">
|
<el-input v-if="item.prop === 'indexType'" disabled size="small" v-model="scope.row.indexType" />
|
||||||
<el-option v-for="typeValue in indexTypeList" :key="typeValue" :value="typeValue">{{ typeValue }}</el-option>
|
|
||||||
</el-select>
|
|
||||||
|
|
||||||
<el-input v-if="item.prop === 'indexComment'" size="small" v-model="scope.row.indexComment"> </el-input>
|
<el-input v-if="item.prop === 'indexComment'" size="small" v-model="scope.row.indexComment"> </el-input>
|
||||||
|
|
||||||
<el-link
|
<el-popconfirm v-else-if="item.prop === 'action'" title="确定删除?" @confirm="deleteIndex(scope.$index)">
|
||||||
v-if="item.prop === 'action'"
|
<template #reference>
|
||||||
type="danger"
|
<el-link type="danger" plain size="small" :underline="false">删除</el-link>
|
||||||
plain
|
</template>
|
||||||
size="small"
|
</el-popconfirm>
|
||||||
:underline="false"
|
|
||||||
@click.prevent="deleteIndex(scope.$index)"
|
|
||||||
>删除</el-link
|
|
||||||
>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -130,6 +119,7 @@
|
|||||||
</el-tabs>
|
</el-tabs>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
|
<el-button @click="cancel()">取消</el-button>
|
||||||
<el-button :loading="btnloading" @click="submit()" type="primary">保存</el-button>
|
<el-button :loading="btnloading" @click="submit()" type="primary">保存</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
@@ -141,6 +131,7 @@ import { reactive, ref, toRefs, watch } from 'vue';
|
|||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
import SqlExecBox from '../sqleditor/SqlExecBox';
|
import SqlExecBox from '../sqleditor/SqlExecBox';
|
||||||
import { DbType, getDbDialect, IndexDefinition, RowDefinition } from '../../dialect/index';
|
import { DbType, getDbDialect, IndexDefinition, RowDefinition } from '../../dialect/index';
|
||||||
|
import { DbInst } from '../../db';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
visible: {
|
visible: {
|
||||||
@@ -161,12 +152,15 @@ const props = defineProps({
|
|||||||
dbType: {
|
dbType: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
flowProcdefKey: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
//定义事件
|
//定义事件
|
||||||
const emit = defineEmits(['update:visible', 'cancel', 'val-change', 'submit-sql']);
|
const emit = defineEmits(['update:visible', 'cancel', 'val-change', 'submit-sql']);
|
||||||
|
|
||||||
const dbDialect = getDbDialect(props.dbType);
|
let dbDialect = getDbDialect(props.dbType);
|
||||||
|
|
||||||
type ColName = {
|
type ColName = {
|
||||||
prop: string;
|
prop: string;
|
||||||
@@ -179,30 +173,33 @@ const state = reactive({
|
|||||||
dialogVisible: false,
|
dialogVisible: false,
|
||||||
btnloading: false,
|
btnloading: false,
|
||||||
activeName: '1',
|
activeName: '1',
|
||||||
columnTypeList: dbDialect.getInfo().columnTypes,
|
|
||||||
indexTypeList: ['BTREE', 'NORMAL'], // mysql索引类型详解 http://c.biancheng.net/view/7897.html
|
|
||||||
tableData: {
|
tableData: {
|
||||||
fields: {
|
fields: {
|
||||||
colNames: [
|
colNames: [
|
||||||
{
|
{
|
||||||
prop: 'name',
|
prop: 'name',
|
||||||
label: '字段名称',
|
label: '字段名称',
|
||||||
|
width: 200,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'type',
|
prop: 'type',
|
||||||
label: '字段类型',
|
label: '字段类型',
|
||||||
|
width: 120,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'length',
|
prop: 'length',
|
||||||
label: '长度',
|
label: '长度',
|
||||||
|
width: 120,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'numScale',
|
prop: 'numScale',
|
||||||
label: '小数点',
|
label: '小数精度',
|
||||||
|
width: 120,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'value',
|
prop: 'value',
|
||||||
label: '默认值',
|
label: '默认值',
|
||||||
|
width: 120,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -231,6 +228,7 @@ const state = reactive({
|
|||||||
},
|
},
|
||||||
] as ColName[],
|
] as ColName[],
|
||||||
res: [] as RowDefinition[],
|
res: [] as RowDefinition[],
|
||||||
|
oldFields: [] as RowDefinition[],
|
||||||
},
|
},
|
||||||
indexs: {
|
indexs: {
|
||||||
colNames: [
|
colNames: [
|
||||||
@@ -261,19 +259,36 @@ const state = reactive({
|
|||||||
],
|
],
|
||||||
columns: [{ name: '', remark: '' }],
|
columns: [{ name: '', remark: '' }],
|
||||||
res: [] as IndexDefinition[],
|
res: [] as IndexDefinition[],
|
||||||
|
oldIndexs: [] as IndexDefinition[],
|
||||||
},
|
},
|
||||||
tableName: '',
|
tableName: '',
|
||||||
tableComment: '',
|
tableComment: '',
|
||||||
|
oldTableName: '',
|
||||||
|
oldTableComment: '',
|
||||||
height: 450,
|
height: 450,
|
||||||
|
db: '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { dialogVisible, btnloading, activeName, indexTypeList, tableData } = toRefs(state);
|
const { dialogVisible, btnloading, activeName, tableData } = toRefs(state);
|
||||||
|
|
||||||
watch(props, async (newValue) => {
|
watch(props, async (newValue) => {
|
||||||
state.dialogVisible = newValue.visible;
|
state.dialogVisible = newValue.visible;
|
||||||
|
dbDialect = getDbDialect(newValue.dbType);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 切换到索引tab时,刷新索引字段下拉选项
|
||||||
|
watch(
|
||||||
|
() => state.activeName,
|
||||||
|
(newValue) => {
|
||||||
|
if (newValue === '2') {
|
||||||
|
state.tableData.indexs.columns = state.tableData.fields.res.map((a) => {
|
||||||
|
return { name: a.name, remark: a.remark };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const cancel = () => {
|
const cancel = () => {
|
||||||
emit('update:visible', false);
|
emit('update:visible', false);
|
||||||
reset();
|
reset();
|
||||||
@@ -320,6 +335,7 @@ const submit = async () => {
|
|||||||
dbId: props.dbId as any,
|
dbId: props.dbId as any,
|
||||||
db: props.db as any,
|
db: props.db as any,
|
||||||
dbType: dbDialect.getInfo().formatSqlDialect,
|
dbType: dbDialect.getInfo().formatSqlDialect,
|
||||||
|
flowProcdefKey: props.flowProcdefKey,
|
||||||
runSuccessCallback: () => {
|
runSuccessCallback: () => {
|
||||||
emit('submit-sql', { tableName: state.tableData.tableName });
|
emit('submit-sql', { tableName: state.tableData.tableName });
|
||||||
// cancel();
|
// cancel();
|
||||||
@@ -333,22 +349,25 @@ const submit = async () => {
|
|||||||
* @param nowArr 修改后的对象数组
|
* @param nowArr 修改后的对象数组
|
||||||
* @param key 标志对象唯一属性
|
* @param key 标志对象唯一属性
|
||||||
*/
|
*/
|
||||||
const filterChangedData = (oldArr: object[], nowArr: object[], key: string): { del: any[]; add: any[]; upd: any[] } => {
|
const filterChangedData = (oldArr: object[], nowArr: object[], key: string): { del: any[]; add: any[]; upd: any[]; changed: boolean } => {
|
||||||
let data = {
|
let data = {
|
||||||
del: [] as object[], // 删除的数据
|
del: [] as object[], // 删除的数据
|
||||||
add: [] as object[], // 新增的数据
|
add: [] as object[], // 新增的数据
|
||||||
upd: [] as object[], // 修改的数据
|
upd: [] as object[], // 修改的数据
|
||||||
|
changed: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 旧数据为空
|
// 旧数据为空
|
||||||
if (oldArr && Array.isArray(oldArr) && oldArr.length === 0 && nowArr && Array.isArray(nowArr) && nowArr.length > 0) {
|
if (oldArr && Array.isArray(oldArr) && oldArr.length === 0 && nowArr && Array.isArray(nowArr) && nowArr.length > 0) {
|
||||||
data.add = nowArr;
|
data.add = nowArr;
|
||||||
|
data.changed = true;
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新数据为空
|
// 新数据为空
|
||||||
if (nowArr && Array.isArray(nowArr) && nowArr.length === 0 && oldArr && Array.isArray(oldArr) && oldArr.length > 0) {
|
if (nowArr && Array.isArray(nowArr) && nowArr.length === 0 && oldArr && Array.isArray(oldArr) && oldArr.length > 0) {
|
||||||
data.del = oldArr;
|
data.del = oldArr;
|
||||||
|
data.changed = true;
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,8 +378,12 @@ const filterChangedData = (oldArr: object[], nowArr: object[], key: string): { d
|
|||||||
nowArr.forEach((a) => {
|
nowArr.forEach((a) => {
|
||||||
let k = a[key];
|
let k = a[key];
|
||||||
newMap[k] = a;
|
newMap[k] = a;
|
||||||
if (!oldMap.hasOwnProperty(k)) {
|
// 取oldName,因为修改了name,但是oldName不会变
|
||||||
|
let oldName = a['oldName'];
|
||||||
|
oldName && (newMap[oldName] = a);
|
||||||
|
if (!oldMap.hasOwnProperty(k) && (!oldName || (oldName && !oldMap.hasOwnProperty(oldName)))) {
|
||||||
// 新增
|
// 新增
|
||||||
|
data.changed = true;
|
||||||
data.add.push(a);
|
data.add.push(a);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -370,13 +393,15 @@ const filterChangedData = (oldArr: object[], nowArr: object[], key: string): { d
|
|||||||
let newData = newMap[k];
|
let newData = newMap[k];
|
||||||
if (!newData) {
|
if (!newData) {
|
||||||
// 删除
|
// 删除
|
||||||
|
data.changed = true;
|
||||||
data.del.push(a);
|
data.del.push(a);
|
||||||
} else {
|
} else {
|
||||||
// 判断每个字段是否相等,否则为修改
|
// 判断每个字段是否相等,否则为修改
|
||||||
for (let f in a) {
|
for (let f in a) {
|
||||||
let oldV = a[f];
|
let oldV = a[f];
|
||||||
let newV = newData[f];
|
let newV = newData[f];
|
||||||
if (oldV.toString() !== newV.toString()) {
|
if (oldV?.toString() !== newV?.toString()) {
|
||||||
|
data.changed = true;
|
||||||
data.upd.push(newData);
|
data.upd.push(newData);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -390,22 +415,28 @@ const genSql = () => {
|
|||||||
let data = state.tableData;
|
let data = state.tableData;
|
||||||
// 创建表
|
// 创建表
|
||||||
if (!props.data?.edit) {
|
if (!props.data?.edit) {
|
||||||
if (state.activeName === '1') {
|
let createTable = dbDialect.getCreateTableSql(data);
|
||||||
return dbDialect.getCreateTableSql(data);
|
let createIndex = '';
|
||||||
} else if (state.activeName === '2' && data.indexs.res.length > 0) {
|
if (data.indexs.res.length > 0) {
|
||||||
return dbDialect.getCreateIndexSql(data);
|
createIndex = dbDialect.getCreateIndexSql(data);
|
||||||
}
|
}
|
||||||
|
return createTable + ';' + createIndex;
|
||||||
} else {
|
} else {
|
||||||
// 修改
|
// 修改列
|
||||||
if (state.activeName === '1') {
|
let changeColData = filterChangedData(state.tableData.fields.oldFields, state.tableData.fields.res, 'name');
|
||||||
// 修改列
|
let colSql = changeColData.changed ? dbDialect.getModifyColumnSql(data, data.tableName, changeColData) : '';
|
||||||
let changeData = filterChangedData(oldData.fields, state.tableData.fields.res, 'name');
|
// 修改索引
|
||||||
return dbDialect.getModifyColumnSql(data.tableName, changeData);
|
let changeIdxData = filterChangedData(state.tableData.indexs.oldIndexs, state.tableData.indexs.res, 'indexName');
|
||||||
} else if (state.activeName === '2') {
|
let idxSql = changeIdxData.changed ? dbDialect.getModifyIndexSql(data, data.tableName, changeIdxData) : '';
|
||||||
// 修改索引
|
// 修改表名,表注释
|
||||||
let changeData = filterChangedData(oldData.indexs, state.tableData.indexs.res, 'indexName');
|
let tableInfoSql = data.tableName !== data.oldTableName || data.tableComment !== data.oldTableComment ? dbDialect.getModifyTableInfoSql(data) : '';
|
||||||
return dbDialect.getModifyIndexSql(data.tableName, changeData);
|
|
||||||
}
|
let sqlArr = [];
|
||||||
|
colSql && sqlArr.push(colSql);
|
||||||
|
idxSql && sqlArr.push(idxSql);
|
||||||
|
tableInfoSql && sqlArr.push(tableInfoSql);
|
||||||
|
|
||||||
|
return sqlArr.join(';');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -414,28 +445,10 @@ const reset = () => {
|
|||||||
formRef.value.resetFields();
|
formRef.value.resetFields();
|
||||||
state.tableData.tableName = '';
|
state.tableData.tableName = '';
|
||||||
state.tableData.tableComment = '';
|
state.tableData.tableComment = '';
|
||||||
state.tableData.fields.res = [
|
state.tableData.fields.res = [];
|
||||||
{
|
state.tableData.fields.oldFields = [];
|
||||||
name: '',
|
state.tableData.indexs.res = [];
|
||||||
type: '',
|
state.tableData.indexs.oldIndexs = [];
|
||||||
value: '',
|
|
||||||
length: '',
|
|
||||||
numScale: '',
|
|
||||||
notNull: false,
|
|
||||||
pri: false,
|
|
||||||
auto_increment: false,
|
|
||||||
remark: '',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
state.tableData.indexs.res = [
|
|
||||||
{
|
|
||||||
indexName: '',
|
|
||||||
columnNames: [],
|
|
||||||
unique: false,
|
|
||||||
indexType: 'BTREE',
|
|
||||||
indexComment: '',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const indexChanges = (row: any) => {
|
const indexChanges = (row: any) => {
|
||||||
@@ -456,7 +469,21 @@ const indexChanges = (row: any) => {
|
|||||||
row.indexComment = `${tableData.value.tableName}表(${name.replaceAll('_', ',')})${commentSuffix}`;
|
row.indexComment = `${tableData.value.tableName}表(${name.replaceAll('_', ',')})${commentSuffix}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const oldData = { indexs: [] as any[], fields: [] as RowDefinition[] };
|
const disableEditIncr = () => {
|
||||||
|
if (DbType.postgresql === props.dbType) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是mssql则不能修改自增
|
||||||
|
if (props.data?.edit) {
|
||||||
|
if (DbType.mssql === props.dbType) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.data,
|
() => props.data,
|
||||||
(newValue: any) => {
|
(newValue: any) => {
|
||||||
@@ -464,37 +491,47 @@ watch(
|
|||||||
// 回显表名表注释
|
// 回显表名表注释
|
||||||
state.tableData.tableName = row.tableName;
|
state.tableData.tableName = row.tableName;
|
||||||
state.tableData.tableComment = row.tableComment;
|
state.tableData.tableComment = row.tableComment;
|
||||||
|
state.tableData.oldTableName = row.tableName;
|
||||||
|
state.tableData.oldTableComment = row.tableComment;
|
||||||
|
state.tableData.db = props.db!;
|
||||||
|
|
||||||
|
state.tableData.fields.oldFields = [];
|
||||||
|
state.tableData.fields.res = [];
|
||||||
|
state.tableData.indexs.oldIndexs = [];
|
||||||
|
state.tableData.indexs.res = [];
|
||||||
|
// 索引列下拉选
|
||||||
|
state.tableData.indexs.columns = [];
|
||||||
|
DbInst.initColumns(columns);
|
||||||
// 回显列
|
// 回显列
|
||||||
if (columns && Array.isArray(columns) && columns.length > 0) {
|
if (columns && Array.isArray(columns) && columns.length > 0) {
|
||||||
oldData.fields = [];
|
|
||||||
state.tableData.fields.res = [];
|
|
||||||
// 索引列下拉选
|
|
||||||
state.tableData.indexs.columns = [];
|
|
||||||
columns.forEach((a) => {
|
columns.forEach((a) => {
|
||||||
let typeObj = a.columnType.replace(')', '').split('(');
|
let defaultValue = '';
|
||||||
let type = typeObj[0];
|
if (a.columnDefault) {
|
||||||
let length = (typeObj.length > 1 && typeObj[1]) || '';
|
defaultValue = a.columnDefault.trim().replace(/^'|'$/g, '');
|
||||||
|
// 解决高斯的默认值问题
|
||||||
|
defaultValue = defaultValue.replace("'::character varying", '');
|
||||||
|
}
|
||||||
let data = {
|
let data = {
|
||||||
name: a.columnName,
|
name: a.columnName,
|
||||||
type,
|
oldName: a.columnName,
|
||||||
value: a.columnDefault || '',
|
type: a.dataType,
|
||||||
length,
|
value: defaultValue,
|
||||||
numScale: a.numScale,
|
length: a.showLength,
|
||||||
notNull: a.nullable !== 'YES',
|
numScale: a.showScale,
|
||||||
pri: a.columnKey === 'PRI',
|
notNull: !a.nullable,
|
||||||
auto_increment: a.columnKey === 'PRI' /*a.extra?.indexOf('auto_increment') > -1*/,
|
pri: a.isPrimaryKey,
|
||||||
|
auto_increment: a.isIdentity /*a.extra?.indexOf('auto_increment') > -1*/,
|
||||||
remark: a.columnComment,
|
remark: a.columnComment,
|
||||||
};
|
};
|
||||||
state.tableData.fields.res.push(data);
|
state.tableData.fields.res.push(data);
|
||||||
oldData.fields.push(JSON.parse(JSON.stringify(data)));
|
state.tableData.fields.oldFields.push(JSON.parse(JSON.stringify(data)));
|
||||||
// 索引字段下拉选项
|
// 索引字段下拉选项
|
||||||
state.tableData.indexs.columns.push({ name: a.columnName, remark: a.columnComment });
|
state.tableData.indexs.columns.push({ name: a.columnName, remark: a.columnComment });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 回显索引
|
// 回显索引
|
||||||
if (indexs && Array.isArray(indexs) && indexs.length > 0) {
|
if (indexs && Array.isArray(indexs) && indexs.length > 0) {
|
||||||
oldData.indexs = [];
|
|
||||||
state.tableData.indexs.res = [];
|
|
||||||
// 索引过滤掉主键
|
// 索引过滤掉主键
|
||||||
indexs
|
indexs
|
||||||
.filter((a) => a.indexName !== 'PRIMARY')
|
.filter((a) => a.indexName !== 'PRIMARY')
|
||||||
@@ -502,12 +539,12 @@ watch(
|
|||||||
let data = {
|
let data = {
|
||||||
indexName: a.indexName,
|
indexName: a.indexName,
|
||||||
columnNames: a.columnName?.split(','),
|
columnNames: a.columnName?.split(','),
|
||||||
unique: a.nonUnique === 0 || false,
|
unique: a.isUnique || false,
|
||||||
indexType: a.indexType,
|
indexType: a.indexType,
|
||||||
indexComment: a.indexComment,
|
indexComment: a.indexComment,
|
||||||
};
|
};
|
||||||
state.tableData.indexs.res.push(data);
|
state.tableData.indexs.res.push(data);
|
||||||
oldData.indexs.push(JSON.parse(JSON.stringify(data)));
|
state.tableData.indexs.oldIndexs.push(JSON.parse(JSON.stringify(data)));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,9 @@
|
|||||||
</template>
|
</template>
|
||||||
<el-form-item label="导出内容: ">
|
<el-form-item label="导出内容: ">
|
||||||
<el-radio-group v-model="dumpInfo.type">
|
<el-radio-group v-model="dumpInfo.type">
|
||||||
<el-radio :label="1" size="small">结构</el-radio>
|
<el-radio :value="1" size="small">结构</el-radio>
|
||||||
<el-radio :label="2" size="small">数据</el-radio>
|
<el-radio :value="2" size="small">数据</el-radio>
|
||||||
<el-radio :label="3" size="small">结构+数据</el-radio>
|
<el-radio :value="3" size="small">结构+数据</el-radio>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
@@ -68,9 +68,7 @@
|
|||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-link @click.prevent="showColumns(scope.row)" type="primary">字段</el-link>
|
<el-link @click.prevent="showColumns(scope.row)" type="primary">字段</el-link>
|
||||||
<el-link class="ml5" @click.prevent="showTableIndex(scope.row)" type="success">索引</el-link>
|
<el-link class="ml5" @click.prevent="showTableIndex(scope.row)" type="success">索引</el-link>
|
||||||
<el-link class="ml5" v-if="tableCreateDialog.enableEditTypes.indexOf(dbType) > -1" @click.prevent="openEditTable(scope.row)" type="warning"
|
<el-link class="ml5" v-if="editDbTypes.indexOf(dbType) > -1" @click.prevent="openEditTable(scope.row)" type="warning">编辑表</el-link>
|
||||||
>编辑表</el-link
|
|
||||||
>
|
|
||||||
<el-link class="ml5" @click.prevent="showCreateDdl(scope.row)" type="info">DDL</el-link>
|
<el-link class="ml5" @click.prevent="showCreateDdl(scope.row)" type="info">DDL</el-link>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -84,7 +82,7 @@
|
|||||||
<el-dialog width="40%" :title="`${chooseTableName} 字段信息`" v-model="columnDialog.visible">
|
<el-dialog width="40%" :title="`${chooseTableName} 字段信息`" v-model="columnDialog.visible">
|
||||||
<el-table border stripe :data="columnDialog.columns" size="small">
|
<el-table border stripe :data="columnDialog.columns" size="small">
|
||||||
<el-table-column prop="columnName" label="名称" show-overflow-tooltip> </el-table-column>
|
<el-table-column prop="columnName" label="名称" show-overflow-tooltip> </el-table-column>
|
||||||
<el-table-column width="120" prop="columnType" label="类型" show-overflow-tooltip> </el-table-column>
|
<el-table-column width="120" prop="showDataType" label="类型" show-overflow-tooltip> </el-table-column>
|
||||||
<el-table-column width="80" prop="nullable" label="是否可为空" show-overflow-tooltip> </el-table-column>
|
<el-table-column width="80" prop="nullable" label="是否可为空" show-overflow-tooltip> </el-table-column>
|
||||||
<el-table-column prop="columnComment" label="备注" show-overflow-tooltip> </el-table-column>
|
<el-table-column prop="columnComment" label="备注" show-overflow-tooltip> </el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -100,8 +98,8 @@
|
|||||||
</el-table>
|
</el-table>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog width="55%" :title="`${chooseTableName} Create-DDL`" v-model="ddlDialog.visible">
|
<el-dialog width="55%" :title="`'${chooseTableName}' DDL`" v-model="ddlDialog.visible">
|
||||||
<el-input disabled type="textarea" :autosize="{ minRows: 15, maxRows: 30 }" v-model="ddlDialog.ddl" size="small"> </el-input>
|
<monaco-editor height="400px" language="sql" v-model="ddlDialog.ddl" :options="{ readOnly: true }" />
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<db-table-op
|
<db-table-op
|
||||||
@@ -110,6 +108,7 @@
|
|||||||
:dbId="dbId"
|
:dbId="dbId"
|
||||||
:db="db"
|
:db="db"
|
||||||
:dbType="dbType"
|
:dbType="dbType"
|
||||||
|
:flow-procdef-key="props.flowProcdefKey"
|
||||||
:data="tableCreateDialog.data"
|
:data="tableCreateDialog.data"
|
||||||
v-model:visible="tableCreateDialog.visible"
|
v-model:visible="tableCreateDialog.visible"
|
||||||
@submit-sql="onSubmitSql"
|
@submit-sql="onSubmitSql"
|
||||||
@@ -127,7 +126,10 @@ import SqlExecBox from '../sqleditor/SqlExecBox';
|
|||||||
import config from '@/common/config';
|
import config from '@/common/config';
|
||||||
import { joinClientParams } from '@/common/request';
|
import { joinClientParams } from '@/common/request';
|
||||||
import { isTrue } from '@/common/assert';
|
import { isTrue } from '@/common/assert';
|
||||||
import { compatibleMysql, DbType } from '../../dialect/index';
|
import { compatibleMysql, editDbTypes, getDbDialect } from '../../dialect/index';
|
||||||
|
import { DbInst } from '../../db';
|
||||||
|
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||||
|
import { format as sqlFormatter } from 'sql-formatter';
|
||||||
|
|
||||||
const DbTableOp = defineAsyncComponent(() => import('./DbTableOp.vue'));
|
const DbTableOp = defineAsyncComponent(() => import('./DbTableOp.vue'));
|
||||||
|
|
||||||
@@ -148,6 +150,9 @@ const props = defineProps({
|
|||||||
type: [String],
|
type: [String],
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
flowProcdefKey: {
|
||||||
|
type: [String],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
@@ -181,7 +186,6 @@ const state = reactive({
|
|||||||
visible: false,
|
visible: false,
|
||||||
activeName: '1',
|
activeName: '1',
|
||||||
type: '',
|
type: '',
|
||||||
enableEditTypes: [DbType.mysql, DbType.mariadb, DbType.postgresql, DbType.dm, DbType.oracle], // 支持"编辑表"的数据库类型
|
|
||||||
data: {
|
data: {
|
||||||
// 修改表时,传递修改数据
|
// 修改表时,传递修改数据
|
||||||
edit: false,
|
edit: false,
|
||||||
@@ -274,11 +278,13 @@ const dump = (db: string) => {
|
|||||||
|
|
||||||
const showColumns = async (row: any) => {
|
const showColumns = async (row: any) => {
|
||||||
state.chooseTableName = row.tableName;
|
state.chooseTableName = row.tableName;
|
||||||
state.columnDialog.columns = await dbApi.columnMetadata.request({
|
const columns = await dbApi.columnMetadata.request({
|
||||||
id: props.dbId,
|
id: props.dbId,
|
||||||
db: props.db,
|
db: props.db,
|
||||||
tableName: row.tableName,
|
tableName: row.tableName,
|
||||||
});
|
});
|
||||||
|
DbInst.initColumns(columns);
|
||||||
|
state.columnDialog.columns = columns;
|
||||||
|
|
||||||
state.columnDialog.visible = true;
|
state.columnDialog.visible = true;
|
||||||
};
|
};
|
||||||
@@ -301,7 +307,8 @@ const showCreateDdl = async (row: any) => {
|
|||||||
db: props.db,
|
db: props.db,
|
||||||
tableName: row.tableName,
|
tableName: row.tableName,
|
||||||
});
|
});
|
||||||
state.ddlDialog.ddl = res;
|
|
||||||
|
state.ddlDialog.ddl = sqlFormatter(res, { language: getDbDialect(props.dbType).getInfo().formatSqlDialect });
|
||||||
state.ddlDialog.visible = true;
|
state.ddlDialog.visible = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -320,6 +327,7 @@ const dropTable = async (row: any) => {
|
|||||||
sql: `DROP TABLE ${tableName}`,
|
sql: `DROP TABLE ${tableName}`,
|
||||||
dbId: props.dbId as any,
|
dbId: props.dbId as any,
|
||||||
db: props.db as any,
|
db: props.db as any,
|
||||||
|
flowProcdefKey: props.flowProcdefKey,
|
||||||
runSuccessCallback: async () => {
|
runSuccessCallback: async () => {
|
||||||
await getTables();
|
await getTables();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ import { editor, languages, Position } from 'monaco-editor';
|
|||||||
|
|
||||||
import { registerCompletionItemProvider } from '@/components/monaco/completionItemProvider';
|
import { registerCompletionItemProvider } from '@/components/monaco/completionItemProvider';
|
||||||
import { DbDialect, EditorCompletionItem, getDbDialect } from './dialect';
|
import { DbDialect, EditorCompletionItem, getDbDialect } from './dialect';
|
||||||
|
import { type RemovableRef, useLocalStorage } from '@vueuse/core';
|
||||||
|
|
||||||
|
const hintsStorage: RemovableRef<Map<string, any>> = useLocalStorage('db-table-hints', new Map());
|
||||||
|
const tableStorage: RemovableRef<Map<string, any>> = useLocalStorage('db-tables', new Map());
|
||||||
|
|
||||||
const dbInstCache: Map<number, DbInst> = new Map();
|
const dbInstCache: Map<number, DbInst> = new Map();
|
||||||
|
|
||||||
@@ -36,6 +40,11 @@ export class DbInst {
|
|||||||
*/
|
*/
|
||||||
type: string;
|
type: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流程定义key,若存在则需要审批执行
|
||||||
|
*/
|
||||||
|
flowProcdefKey: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* dbName -> db
|
* dbName -> db
|
||||||
*/
|
*/
|
||||||
@@ -58,17 +67,23 @@ export class DbInst {
|
|||||||
if (!dbName) {
|
if (!dbName) {
|
||||||
throw new Error('dbName不能为空');
|
throw new Error('dbName不能为空');
|
||||||
}
|
}
|
||||||
let db = this.dbs.get(dbName);
|
let key = `${this.id}_${dbName}`;
|
||||||
|
let db = this.dbs.get(key);
|
||||||
if (db) {
|
if (db) {
|
||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
console.info(`new db -> dbId: ${this.id}, dbName: ${dbName}`);
|
console.info(`new db -> dbId: ${this.id}, dbName: ${dbName}`);
|
||||||
db = new Db();
|
db = new Db();
|
||||||
db.name = dbName;
|
db.name = dbName;
|
||||||
this.dbs.set(dbName, db);
|
this.dbs.set(key, db);
|
||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取数据库实例方言
|
||||||
|
getDialect(): DbDialect {
|
||||||
|
return getDbDialect(this.type);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 加载数据库表信息
|
* 加载数据库表信息
|
||||||
* @param dbName 数据库名
|
* @param dbName 数据库名
|
||||||
@@ -77,17 +92,22 @@ export class DbInst {
|
|||||||
*/
|
*/
|
||||||
async loadTables(dbName: string, reload?: boolean) {
|
async loadTables(dbName: string, reload?: boolean) {
|
||||||
const db = this.getDb(dbName);
|
const db = this.getDb(dbName);
|
||||||
// 优先从 table map中获取
|
let key = this.dbTablesKey(dbName);
|
||||||
let tables = db.tables;
|
let tables = tableStorage.value.get(key);
|
||||||
|
// 优先从 table 缓存中获取
|
||||||
if (!reload && tables) {
|
if (!reload && tables) {
|
||||||
|
db.tables = tables;
|
||||||
return tables;
|
return tables;
|
||||||
}
|
}
|
||||||
// 重置列信息缓存与表提示信息
|
// 重置列信息缓存与表提示信息
|
||||||
db.columnsMap?.clear();
|
db.columnsMap?.clear();
|
||||||
db.tableHints = null;
|
|
||||||
console.log(`load tables -> dbName: ${dbName}`);
|
console.log(`load tables -> dbName: ${dbName}`);
|
||||||
tables = await dbApi.tableInfos.request({ id: this.id, db: dbName });
|
tables = await dbApi.tableInfos.request({ id: this.id, db: dbName });
|
||||||
|
tableStorage.value.set(key, tables);
|
||||||
db.tables = tables;
|
db.tables = tables;
|
||||||
|
|
||||||
|
// 异步加载表提示信息
|
||||||
|
this.loadDbHints(dbName, true).then(() => {});
|
||||||
return tables;
|
return tables;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,7 +166,7 @@ export class DbInst {
|
|||||||
const db = this.getDb(dbName);
|
const db = this.getDb(dbName);
|
||||||
// 优先从 table map中获取
|
// 优先从 table map中获取
|
||||||
let columns = db.getColumns(table);
|
let columns = db.getColumns(table);
|
||||||
if (columns) {
|
if (columns && columns.length > 0) {
|
||||||
return columns;
|
return columns;
|
||||||
}
|
}
|
||||||
console.log(`load columns -> dbName: ${dbName}, table: ${table}`);
|
console.log(`load columns -> dbName: ${dbName}, table: ${table}`);
|
||||||
@@ -155,6 +175,9 @@ export class DbInst {
|
|||||||
db: dbName,
|
db: dbName,
|
||||||
tableName: table,
|
tableName: table,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
DbInst.initColumns(columns);
|
||||||
|
|
||||||
db.columnsMap.set(table, columns);
|
db.columnsMap.set(table, columns);
|
||||||
return columns;
|
return columns;
|
||||||
}
|
}
|
||||||
@@ -169,18 +192,30 @@ export class DbInst {
|
|||||||
return this.getDb(dbName).getColumn(table, columnName);
|
return this.getDb(dbName).getColumn(table, columnName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dbTableHintsKey(dbName: string) {
|
||||||
|
return `db-table-hints_${this.id}_${dbName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
dbTablesKey(dbName: string) {
|
||||||
|
return `db-tables_${this.id}_${dbName}`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取库信息提示
|
* 获取库信息提示
|
||||||
*/
|
*/
|
||||||
async loadDbHints(dbName: string) {
|
async loadDbHints(dbName: string, reload?: boolean) {
|
||||||
const db = this.getDb(dbName);
|
const db = this.getDb(dbName);
|
||||||
if (db.tableHints) {
|
let key = this.dbTableHintsKey(dbName);
|
||||||
return db.tableHints;
|
let hints = hintsStorage.value.get(key);
|
||||||
|
if (!reload && hints) {
|
||||||
|
db.tableHints = hints;
|
||||||
|
return hints;
|
||||||
}
|
}
|
||||||
console.log(`load db-hits -> dbName: ${dbName}`);
|
console.log(`load db-hits -> dbName: ${dbName}`);
|
||||||
const hits = await dbApi.hintTables.request({ id: this.id, db: db.name });
|
hints = await dbApi.hintTables.request({ id: this.id, db: db.name });
|
||||||
db.tableHints = hits;
|
db.tableHints = hints;
|
||||||
return hits;
|
hintsStorage.value.set(key, hints);
|
||||||
|
return hints;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -225,8 +260,8 @@ export class DbInst {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 获取指定表的默认查询sql
|
// 获取指定表的默认查询sql
|
||||||
getDefaultSelectSql(table: string, condition: string, orderBy: string, pageNum: number, limit: number = DbInst.DefaultLimit) {
|
getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number = DbInst.DefaultLimit) {
|
||||||
return getDbDialect(this.type).getDefaultSelectSql(table, condition, orderBy, pageNum, limit);
|
return this.getDialect().getDefaultSelectSql(db, table, condition, orderBy, pageNum, limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -234,12 +269,22 @@ export class DbInst {
|
|||||||
* @param dbName 数据库名
|
* @param dbName 数据库名
|
||||||
* @param table 表名
|
* @param table 表名
|
||||||
* @param datas 要生成的数据
|
* @param datas 要生成的数据
|
||||||
|
* @param dbDialect db方言
|
||||||
|
* @param skipNull 是否跳过空字段
|
||||||
*/
|
*/
|
||||||
async genInsertSql(dbName: string, table: string, datas: any[]) {
|
async genInsertSql(dbName: string, table: string, datas: any[], skipNull = false) {
|
||||||
if (!datas) {
|
if (!datas) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
let schema = '';
|
||||||
|
let arr = dbName.split('/');
|
||||||
|
if (arr.length == 1) {
|
||||||
|
schema = this.wrapName(dbName) + '.';
|
||||||
|
} else if (arr.length == 2) {
|
||||||
|
schema = this.wrapName(arr[1]) + '.';
|
||||||
|
}
|
||||||
|
|
||||||
|
let dbDialect = this.getDialect();
|
||||||
const columns = await this.loadColumns(dbName, table);
|
const columns = await this.loadColumns(dbName, table);
|
||||||
const sqls = [];
|
const sqls = [];
|
||||||
for (let data of datas) {
|
for (let data of datas) {
|
||||||
@@ -247,23 +292,59 @@ export class DbInst {
|
|||||||
let values = [];
|
let values = [];
|
||||||
for (let column of columns) {
|
for (let column of columns) {
|
||||||
const colName = column.columnName;
|
const colName = column.columnName;
|
||||||
|
if (skipNull && data[colName] == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
colNames.push(this.wrapName(colName));
|
colNames.push(this.wrapName(colName));
|
||||||
values.push(DbInst.wrapValueByType(data[colName]));
|
values.push(dbDialect.wrapValue(column.dataType, data[colName]));
|
||||||
}
|
}
|
||||||
sqls.push(`INSERT INTO ${this.wrapName(table)} (${colNames.join(', ')}) VALUES(${values.join(', ')})`);
|
sqls.push(`INSERT INTO ${schema}${this.wrapName(table)} (${colNames.join(', ')}) VALUES(${values.join(', ')})`);
|
||||||
}
|
}
|
||||||
return sqls.join(';\n') + ';';
|
return sqls.join(';\n') + ';';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成根据主键更新语句
|
||||||
|
* @param dbName 数据库名
|
||||||
|
* @param table 表名
|
||||||
|
* @param columnValue 要更新的列以及对应的值 field->columnName; value->columnValue
|
||||||
|
* @param rowData 表的一行完整数据(需要获取主键信息)
|
||||||
|
*/
|
||||||
|
async genUpdateSql(dbName: string, table: string, columnValue: {}, rowData: {}) {
|
||||||
|
let schema = '';
|
||||||
|
let dbArr = dbName.split('/');
|
||||||
|
if (dbArr.length == 2) {
|
||||||
|
schema = this.wrapName(dbArr[1]) + '.';
|
||||||
|
}
|
||||||
|
|
||||||
|
let sql = `UPDATE ${schema}${this.wrapName(table)} SET `;
|
||||||
|
// 主键列信息
|
||||||
|
const primaryKey = await this.loadTableColumn(dbName, table);
|
||||||
|
let primaryKeyType = primaryKey.dataType;
|
||||||
|
let primaryKeyName = primaryKey.columnName;
|
||||||
|
let primaryKeyValue = rowData[primaryKeyName];
|
||||||
|
const dialect = this.getDialect();
|
||||||
|
for (let k of Object.keys(columnValue)) {
|
||||||
|
const v = columnValue[k];
|
||||||
|
// 更新字段列信息
|
||||||
|
const updateColumn = await this.loadTableColumn(dbName, table, k);
|
||||||
|
sql += ` ${this.wrapName(k)} = ${dialect.wrapValue(updateColumn.dataType, v)},`;
|
||||||
|
}
|
||||||
|
sql = sql.substring(0, sql.length - 1);
|
||||||
|
|
||||||
|
return sql + ` WHERE ${this.wrapName(primaryKeyName)} = ${this.getDialect().wrapValue(primaryKeyType, primaryKeyValue)} ;`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成根据主键删除的sql语句
|
* 生成根据主键删除的sql语句
|
||||||
|
* @param db 数据库名
|
||||||
* @param table 表名
|
* @param table 表名
|
||||||
* @param datas 要删除的记录
|
* @param datas 要删除的记录
|
||||||
*/
|
*/
|
||||||
async genDeleteByPrimaryKeysSql(db: string, table: string, datas: any[]) {
|
async genDeleteByPrimaryKeysSql(db: string, table: string, datas: any[]) {
|
||||||
const primaryKey = await this.loadTableColumn(db, table);
|
const primaryKey = await this.loadTableColumn(db, table);
|
||||||
const primaryKeyColumnName = primaryKey.columnName;
|
const primaryKeyColumnName = primaryKey.columnName;
|
||||||
const ids = datas.map((d: any) => `${DbInst.wrapColumnValue(primaryKey.columnType, d[primaryKeyColumnName])}`).join(',');
|
const ids = datas.map((d: any) => `${this.getDialect().wrapValue(primaryKey.dataType, d[primaryKeyColumnName])}`).join(',');
|
||||||
return `DELETE FROM ${this.wrapName(table)} WHERE ${this.wrapName(primaryKeyColumnName)} IN (${ids})`;
|
return `DELETE FROM ${this.wrapName(table)} WHERE ${this.wrapName(primaryKeyColumnName)} IN (${ids})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,19 +356,20 @@ export class DbInst {
|
|||||||
sql,
|
sql,
|
||||||
dbId: this.id,
|
dbId: this.id,
|
||||||
db,
|
db,
|
||||||
|
dbType: this.getDialect().getInfo().formatSqlDialect,
|
||||||
runSuccessCallback: successFunc,
|
runSuccessCallback: successFunc,
|
||||||
cancelCallback: cancelFunc,
|
cancelCallback: cancelFunc,
|
||||||
|
flowProcdefKey: this.flowProcdefKey,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 包裹数据库表名、字段名等,避免使用关键字为字段名或表名时报错
|
* 包裹数据库表名、字段名等,避免使用关键字为字段名或表名时报错
|
||||||
* @param table
|
* @param name 表名、字段名、schema名
|
||||||
* @param condition
|
* @returns 包裹后的字符串
|
||||||
* @returns
|
|
||||||
*/
|
*/
|
||||||
wrapName = (name: string) => {
|
wrapName = (name: string) => {
|
||||||
return getDbDialect(this.type).quoteIdentifier(name);
|
return this.getDialect().quoteIdentifier(name);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -311,6 +393,7 @@ export class DbInst {
|
|||||||
dbInst.name = inst.name;
|
dbInst.name = inst.name;
|
||||||
dbInst.type = inst.type;
|
dbInst.type = inst.type;
|
||||||
dbInst.databases = inst.databases;
|
dbInst.databases = inst.databases;
|
||||||
|
dbInst.flowProcdefKey = inst.flowProcdefKey;
|
||||||
|
|
||||||
dbInstCache.set(dbInst.id, dbInst);
|
dbInstCache.set(dbInst.id, dbInst);
|
||||||
return dbInst;
|
return dbInst;
|
||||||
@@ -340,41 +423,13 @@ export class DbInst {
|
|||||||
dbInstCache.clear();
|
dbInstCache.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据返回值包装值,若值为字符串类型则添加''
|
|
||||||
* @param val 值
|
|
||||||
* @returns 包装后的值
|
|
||||||
*/
|
|
||||||
static wrapValueByType = (val: any) => {
|
|
||||||
if (val == null) {
|
|
||||||
return 'NULL';
|
|
||||||
}
|
|
||||||
if (typeof val == 'number') {
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
return `'${val}'`;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据字段类型包装字段值,如为字符串等则添加‘’,数字类型则直接返回即可
|
|
||||||
*/
|
|
||||||
static wrapColumnValue(columnType: string, value: any, dbDialect?: DbDialect) {
|
|
||||||
if (this.isNumber(columnType)) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
if (!dbDialect) {
|
|
||||||
return `${value}`;
|
|
||||||
}
|
|
||||||
return dbDialect.wrapStrValue(columnType, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 判断字段类型是否为数字类型
|
* 判断字段类型是否为数字类型
|
||||||
* @param columnType 字段类型
|
* @param columnType 字段类型
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
static isNumber(columnType: string) {
|
static isNumber(columnType: string) {
|
||||||
return columnType.match(/int|double|float|number|decimal|byte|bit/gi);
|
return columnType && columnType.match(/int|double|float|number|decimal|byte|bit/gi);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -414,6 +469,35 @@ export class DbInst {
|
|||||||
const flexWidth: number = contentWidth > columnWidth ? contentWidth : columnWidth;
|
const flexWidth: number = contentWidth > columnWidth ? contentWidth : columnWidth;
|
||||||
return flexWidth > 500 ? 500 : flexWidth;
|
return flexWidth > 500 ? 500 : flexWidth;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 初始化所有列信息,完善需要显示的列类型,包含长度等,如varchar(20)
|
||||||
|
static initColumns(columns: any[]) {
|
||||||
|
if (!columns) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (let col of columns) {
|
||||||
|
if (col.charMaxLength > 0) {
|
||||||
|
col.showDataType = `${col.dataType}(${col.charMaxLength})`;
|
||||||
|
col.showLength = col.charMaxLength;
|
||||||
|
col.showScale = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (col.numPrecision > 0) {
|
||||||
|
if (col.numScale > 0) {
|
||||||
|
col.showDataType = `${col.dataType}(${col.numPrecision},${col.numScale})`;
|
||||||
|
col.showScale = col.numScale;
|
||||||
|
} else {
|
||||||
|
col.showDataType = `${col.dataType}(${col.numPrecision})`;
|
||||||
|
col.showScale = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
col.showLength = col.numPrecision;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
col.showDataType = col.dataType;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -441,7 +525,7 @@ class Db {
|
|||||||
getColumn(table: string, columnName: string = '') {
|
getColumn(table: string, columnName: string = '') {
|
||||||
const cols = this.getColumns(table);
|
const cols = this.getColumns(table);
|
||||||
if (!columnName) {
|
if (!columnName) {
|
||||||
const col = cols.find((c: any) => c.columnKey == 'PRI');
|
const col = cols.find((c: any) => c.isPrimaryKey);
|
||||||
return col || cols[0];
|
return col || cols[0];
|
||||||
}
|
}
|
||||||
return cols.find((c: any) => c.columnName == columnName);
|
return cols.find((c: any) => c.columnName == columnName);
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import {
|
|||||||
DataType,
|
DataType,
|
||||||
DbDialect,
|
DbDialect,
|
||||||
DialectInfo,
|
DialectInfo,
|
||||||
|
DuplicateStrategy,
|
||||||
EditorCompletion,
|
EditorCompletion,
|
||||||
EditorCompletionItem,
|
EditorCompletionItem,
|
||||||
|
QuoteEscape,
|
||||||
IndexDefinition,
|
IndexDefinition,
|
||||||
RowDefinition,
|
RowDefinition,
|
||||||
sqlColumnType,
|
sqlColumnType,
|
||||||
@@ -34,7 +36,6 @@ const DM_TYPE_LIST: sqlColumnType[] = [
|
|||||||
// 位串数据类型 BIT 用于存储整数数据 1、0 或 NULL,只有 0 才转换为假,其他非空、非 0 值都会自动转换为真
|
// 位串数据类型 BIT 用于存储整数数据 1、0 或 NULL,只有 0 才转换为假,其他非空、非 0 值都会自动转换为真
|
||||||
{ udtName: 'BIT', dataType: 'BIT', desc: '用于存储整数数据 1、0 或 NULL', space: '1', range: '1' },
|
{ udtName: 'BIT', dataType: 'BIT', desc: '用于存储整数数据 1、0 或 NULL', space: '1', range: '1' },
|
||||||
// 一般日期时间数据类型 DATE TIME TIMESTAMP 默认精度 6
|
// 一般日期时间数据类型 DATE TIME TIMESTAMP 默认精度 6
|
||||||
// 多媒体数据类型 TEXT/LONG/LONGVARCHAR 类型:变长字符串类型 IMAGE/LONGVARBINARY 类型 BLOB CLOB BFILE 100G-1
|
|
||||||
{ udtName: 'DATE', dataType: 'DATE', desc: '年、月、日', space: '', range: '' },
|
{ udtName: 'DATE', dataType: 'DATE', desc: '年、月、日', space: '', range: '' },
|
||||||
{ udtName: 'TIME', dataType: 'TIME', desc: '时、分、秒', space: '', range: '' },
|
{ udtName: 'TIME', dataType: 'TIME', desc: '时、分、秒', space: '', range: '' },
|
||||||
{
|
{
|
||||||
@@ -44,6 +45,7 @@ const DM_TYPE_LIST: sqlColumnType[] = [
|
|||||||
space: '',
|
space: '',
|
||||||
range: '-4712-01-01 00:00:00.000000000 ~ 9999-12-31 23:59:59.999999999',
|
range: '-4712-01-01 00:00:00.000000000 ~ 9999-12-31 23:59:59.999999999',
|
||||||
},
|
},
|
||||||
|
// 多媒体数据类型 TEXT/LONG/LONGVARCHAR 类型:变长字符串类型 IMAGE/LONGVARBINARY 类型 BLOB CLOB BFILE 100G-1
|
||||||
{ udtName: 'TEXT', dataType: 'TEXT', desc: '变长字符串', space: '', range: '100G-1' },
|
{ udtName: 'TEXT', dataType: 'TEXT', desc: '变长字符串', space: '', range: '100G-1' },
|
||||||
{ udtName: 'LONG', dataType: 'LONG', desc: '同TEXT', space: '', range: '100G-1' },
|
{ udtName: 'LONG', dataType: 'LONG', desc: '同TEXT', space: '', range: '100G-1' },
|
||||||
{ udtName: 'LONGVARCHAR', dataType: 'LONGVARCHAR', desc: '同TEXT', space: '', range: '100G-1' },
|
{ udtName: 'LONGVARCHAR', dataType: 'LONGVARCHAR', desc: '同TEXT', space: '', range: '100G-1' },
|
||||||
@@ -54,6 +56,7 @@ const DM_TYPE_LIST: sqlColumnType[] = [
|
|||||||
{ udtName: 'BFILE', dataType: 'BFILE', desc: '二进制文件', space: '', range: '100G-1' },
|
{ udtName: 'BFILE', dataType: 'BFILE', desc: '二进制文件', space: '', range: '100G-1' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 参考官方文档:https://eco.dameng.com/document/dm/zh-cn/pm/function.html
|
||||||
const replaceFunctions: EditorCompletionItem[] = [
|
const replaceFunctions: EditorCompletionItem[] = [
|
||||||
// 数值函数
|
// 数值函数
|
||||||
{ label: 'ABS', insertText: 'ABS(n)', description: '求数值 n 的绝对值' },
|
{ label: 'ABS', insertText: 'ABS(n)', description: '求数值 n 的绝对值' },
|
||||||
@@ -365,21 +368,22 @@ class DMDialect implements DbDialect {
|
|||||||
};
|
};
|
||||||
|
|
||||||
dmDialectInfo = {
|
dmDialectInfo = {
|
||||||
|
name: 'DM',
|
||||||
icon: 'iconfont icon-db-dm',
|
icon: 'iconfont icon-db-dm',
|
||||||
defaultPort: 5236,
|
defaultPort: 5236,
|
||||||
formatSqlDialect: 'postgresql',
|
formatSqlDialect: 'plsql',
|
||||||
columnTypes: DM_TYPE_LIST.sort((a, b) => a.udtName.localeCompare(b.udtName)),
|
columnTypes: DM_TYPE_LIST.sort((a, b) => a.udtName.localeCompare(b.udtName)),
|
||||||
editorCompletions,
|
editorCompletions,
|
||||||
};
|
};
|
||||||
return dmDialectInfo;
|
return dmDialectInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
getDefaultSelectSql(table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
|
getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
|
||||||
return `SELECT * FROM "${table}" ${condition ? 'WHERE ' + condition : ''} ${orderBy ? orderBy : ''} ${this.getPageSql(pageNum, limit)};`;
|
return `SELECT * FROM "${table}" ${condition ? 'WHERE ' + condition : ''} ${orderBy ? orderBy : ''} ${this.getPageSql(pageNum, limit)};`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getPageSql(pageNum: number, limit: number) {
|
getPageSql(pageNum: number, limit: number) {
|
||||||
return ` OFFSET ${(pageNum - 1) * limit} LIMIT ${limit};`;
|
return ` OFFSET ${(pageNum - 1) * limit} LIMIT ${limit}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getDefaultRows(): RowDefinition[] {
|
getDefaultRows(): RowDefinition[] {
|
||||||
@@ -500,7 +504,9 @@ class DMDialect implements DbDialect {
|
|||||||
// 默认值
|
// 默认值
|
||||||
let defVal = this.getDefaultValueSql(cl);
|
let defVal = this.getDefaultValueSql(cl);
|
||||||
let incr = cl.auto_increment ? 'IDENTITY' : '';
|
let incr = cl.auto_increment ? 'IDENTITY' : '';
|
||||||
return ` "${cl.name}" ${cl.type}${length} ${incr} ${cl.notNull ? 'NOT NULL' : ''} ${defVal} `;
|
// 如果有原名以原名为准
|
||||||
|
let name = cl.oldName && cl.name !== cl.oldName ? cl.oldName : cl.name;
|
||||||
|
return ` ${this.quoteIdentifier(name)} ${cl.type}${length} ${incr} ${cl.notNull ? 'NOT NULL' : ''} ${defVal} `;
|
||||||
}
|
}
|
||||||
|
|
||||||
getCreateTableSql(data: any): string {
|
getCreateTableSql(data: any): string {
|
||||||
@@ -518,7 +524,7 @@ class DMDialect implements DbDialect {
|
|||||||
}
|
}
|
||||||
// 列注释
|
// 列注释
|
||||||
if (item.remark) {
|
if (item.remark) {
|
||||||
columCommentSql += ` comment on column "${data.tableName}"."${item.name}" is '${item.remark}'; `;
|
columCommentSql += ` comment on column "${data.tableName}"."${item.name}" is '${QuoteEscape(item.remark)}'; `;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// 建表
|
// 建表
|
||||||
@@ -529,7 +535,7 @@ class DMDialect implements DbDialect {
|
|||||||
);`;
|
);`;
|
||||||
// 表注释
|
// 表注释
|
||||||
if (data.tableComment) {
|
if (data.tableComment) {
|
||||||
tableCommentSql = ` comment on table "${data.tableName}" is '${data.tableComment}'; `;
|
tableCommentSql = ` comment on table "${data.tableName}" is '${QuoteEscape(data.tableComment)}'; `;
|
||||||
}
|
}
|
||||||
|
|
||||||
return createSql + tableCommentSql + columCommentSql;
|
return createSql + tableCommentSql + columCommentSql;
|
||||||
@@ -546,35 +552,78 @@ class DMDialect implements DbDialect {
|
|||||||
return sql.join(';');
|
return sql.join(';');
|
||||||
}
|
}
|
||||||
|
|
||||||
getModifyColumnSql(tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
|
getModifyColumnSql(tableData: any, tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
|
||||||
let sql: string[] = [];
|
let schemaArr = tableData.db.split('/');
|
||||||
|
let schema = schemaArr.length > 1 ? schemaArr[schemaArr.length - 1] : schemaArr[0];
|
||||||
|
|
||||||
|
let dbTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableName)}`;
|
||||||
|
|
||||||
|
let modifySql = '';
|
||||||
|
let dropSql = '';
|
||||||
|
let renameSql = '';
|
||||||
|
let commentSql = '';
|
||||||
|
|
||||||
|
// 主键字段
|
||||||
|
let priArr = new Set();
|
||||||
|
|
||||||
if (changeData.add.length > 0) {
|
if (changeData.add.length > 0) {
|
||||||
changeData.add.forEach((a) => {
|
changeData.add.forEach((a) => {
|
||||||
sql.push(`ALTER TABLE "${tableName}" add COLUMN ${this.genColumnBasicSql(a)}`);
|
modifySql += `ALTER TABLE ${dbTable} add COLUMN ${this.genColumnBasicSql(a)};`;
|
||||||
if (a.remark) {
|
if (a.remark) {
|
||||||
sql.push(`comment on COLUMN "${tableName}"."${a.name}" is '${a.remark}'`);
|
commentSql += `COMMENT ON COLUMN ${dbTable}.${this.quoteIdentifier(a.name)} IS '${QuoteEscape(a.remark)}';`;
|
||||||
|
}
|
||||||
|
if (a.pri) {
|
||||||
|
priArr.add(`"${a.name}"`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changeData.upd.length > 0) {
|
if (changeData.upd.length > 0) {
|
||||||
changeData.upd.forEach((a) => {
|
changeData.upd.forEach((a) => {
|
||||||
sql.push(`ALTER TABLE "${tableName}" MODIFY ${this.genColumnBasicSql(a)}`);
|
let cmtSql = `COMMENT ON COLUMN ${dbTable}.${this.quoteIdentifier(a.name)} IS '${QuoteEscape(a.remark)}';`;
|
||||||
if (a.remark) {
|
if (a.remark && a.oldName === a.name) {
|
||||||
sql.push(`comment on COLUMN "${tableName}"."${a.name}" is '${a.remark}'`);
|
commentSql += cmtSql;
|
||||||
|
}
|
||||||
|
// 修改了字段名
|
||||||
|
if (a.oldName !== a.name) {
|
||||||
|
renameSql += `ALTER TABLE ${dbTable} RENAME COLUMN ${this.quoteIdentifier(a.oldName!)} TO ${this.quoteIdentifier(a.name)};`;
|
||||||
|
if (a.remark) {
|
||||||
|
commentSql += cmtSql;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modifySql += `ALTER TABLE ${dbTable} MODIFY ${this.genColumnBasicSql(a)};`;
|
||||||
|
if (a.pri) {
|
||||||
|
priArr.add(`${this.quoteIdentifier(a.name)}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changeData.del.length > 0) {
|
if (changeData.del.length > 0) {
|
||||||
changeData.del.forEach((a) => {
|
changeData.del.forEach((a) => {
|
||||||
sql.push(`ALTER TABLE "${tableName}" DROP COLUMN ${a.name}`);
|
dropSql += `ALTER TABLE ${dbTable} DROP COLUMN ${a.name};`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return sql.join(';');
|
|
||||||
|
// 编辑主键
|
||||||
|
let dropPkSql = '';
|
||||||
|
if (priArr.size > 0) {
|
||||||
|
let resPri = tableData.fields.res.filter((a: RowDefinition) => a.pri);
|
||||||
|
if (resPri) {
|
||||||
|
priArr.add(`${this.quoteIdentifier(resPri.name)}`);
|
||||||
|
}
|
||||||
|
// 如果有编辑主键字段,则删除主键,再添加主键
|
||||||
|
// 解析表字段中是否含有主键,有的话就删除主键
|
||||||
|
if (tableData.fields.oldFields.find((a: RowDefinition) => a.pri)) {
|
||||||
|
dropPkSql = `ALTER TABLE ${dbTable} DROP PRIMARY KEY;`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let addPkSql = priArr.size > 0 ? `ALTER TABLE ${dbTable} ADD PRIMARY KEY (${Array.from(priArr).join(',')});` : '';
|
||||||
|
|
||||||
|
return dropPkSql + modifySql + dropSql + renameSql + addPkSql + commentSql;
|
||||||
}
|
}
|
||||||
|
|
||||||
getModifyIndexSql(tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
|
getModifyIndexSql(tableData: any, tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
|
||||||
// 不能直接修改索引名或字段、需要先删后加
|
// 不能直接修改索引名或字段、需要先删后加
|
||||||
let dropIndexNames: string[] = [];
|
let dropIndexNames: string[] = [];
|
||||||
let addIndexs: any[] = [];
|
let addIndexs: any[] = [];
|
||||||
@@ -615,6 +664,22 @@ class DMDialect implements DbDialect {
|
|||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
getModifyTableInfoSql(tableData: any): string {
|
||||||
|
let schemaArr = tableData.db.split('/');
|
||||||
|
let schema = schemaArr.length > 1 ? schemaArr[schemaArr.length - 1] : schemaArr[0];
|
||||||
|
|
||||||
|
let sql = '';
|
||||||
|
|
||||||
|
if (tableData.oldTableName !== tableData.tableName) {
|
||||||
|
let baseTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableData.oldTableName)}`;
|
||||||
|
sql += `ALTER TABLE ${baseTable} RENAME TO ${this.quoteIdentifier(tableData.tableName)};`;
|
||||||
|
}
|
||||||
|
if (tableData.oldTableComment !== tableData.tableComment) {
|
||||||
|
let baseTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableData.tableName)}`;
|
||||||
|
sql += `COMMENT ON TABLE ${baseTable} IS '${QuoteEscape(tableData.tableComment)}';`;
|
||||||
|
}
|
||||||
|
return sql;
|
||||||
|
}
|
||||||
|
|
||||||
getDataType(columnType: string): DataType {
|
getDataType(columnType: string): DataType {
|
||||||
if (DbInst.isNumber(columnType)) {
|
if (DbInst.isNumber(columnType)) {
|
||||||
@@ -635,8 +700,54 @@ class DMDialect implements DbDialect {
|
|||||||
return DataType.String;
|
return DataType.String;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars
|
wrapValue(columnType: string, value: any): any {
|
||||||
wrapStrValue(columnType: string, value: string): string {
|
if (value == null) {
|
||||||
|
return 'NULL';
|
||||||
|
}
|
||||||
|
if (DbInst.isNumber(columnType)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
return `'${value}'`;
|
return `'${value}'`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getBatchInsertPreviewSql(tableName: string, fieldArr: string[], duplicateStrategy: DuplicateStrategy): string {
|
||||||
|
// 替换
|
||||||
|
// MERGE INTO t_person T1
|
||||||
|
// USING (
|
||||||
|
// <foreach collection="list" item="item" index="index" separator="UNION ALL">
|
||||||
|
// SELECT
|
||||||
|
// #{item.id} id,
|
||||||
|
// #{item.mc} mc,
|
||||||
|
// #{item.sex} sex,
|
||||||
|
// #{item.age} age
|
||||||
|
// FROM dual
|
||||||
|
// </foreach>
|
||||||
|
// ) T2 ON (T1.id = T2.id )
|
||||||
|
// WHEN NOT MATCHED THEN INSERT(id, mc, sex,
|
||||||
|
// age) VALUES
|
||||||
|
// (T2.id, T2.mc, T2.sex, T2.age)
|
||||||
|
// WHEN MATCHED THEN UPDATE
|
||||||
|
// SET T1.mc = T2.mc,T1.sex = T2.sex,T1.age = T2.age
|
||||||
|
|
||||||
|
if (duplicateStrategy == DuplicateStrategy.REPLACE || duplicateStrategy == DuplicateStrategy.IGNORE) {
|
||||||
|
// 字段数组生成占位符sql
|
||||||
|
let phs = [];
|
||||||
|
let values = [];
|
||||||
|
for (let i = 0; i < fieldArr.length; i++) {
|
||||||
|
phs.push(`? ${fieldArr[i]}`);
|
||||||
|
values.push(`T2.${fieldArr[i]}`);
|
||||||
|
}
|
||||||
|
let placeholder = phs.join(',');
|
||||||
|
let sql = `MERGE INTO ${tableName} T1 USING
|
||||||
|
(
|
||||||
|
SELECT ${placeholder} FROM dual
|
||||||
|
) T2 ON (T1.id = T2.id)
|
||||||
|
WHEN NOT MATCHED THEN INSERT(${fieldArr.join(',')}) VALUES (${values.join(',')})
|
||||||
|
WHEN MATCHED THEN UPDATE SET ${fieldArr.map((a) => `T1.${a} = T2.${a}`).join(',')}`;
|
||||||
|
return sql;
|
||||||
|
} else {
|
||||||
|
let placeholder = '?'.repeat(fieldArr.length).split('').join(',');
|
||||||
|
return `INSERT INTO ${tableName} (${fieldArr.join(',')}) VALUES (${placeholder}), (${placeholder});`;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user