mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-02 23:40:24 +08:00
Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4cb4234fd | ||
|
|
89e12678eb | ||
|
|
137ebb8e9e | ||
|
|
05625bd8c1 | ||
|
|
4afeac5fdd | ||
|
|
1d0e91f1af | ||
|
|
cf5111a325 | ||
|
|
78957a8ebd | ||
|
|
4ed892a656 | ||
|
|
3486b07003 | ||
|
|
a5cd7caf19 | ||
|
|
f2c7ef78c0 | ||
|
|
653953ee76 | ||
|
|
a831614d5a | ||
|
|
ebe73e2f19 | ||
|
|
29fd5a25d2 | ||
|
|
44805ce580 | ||
|
|
2a6d620830 | ||
|
|
01d3e1ad28 | ||
|
|
f4162c38db | ||
|
|
1a4626c24d | ||
|
|
d6eb9683d1 | ||
|
|
e2b524dadb | ||
|
|
8998a21626 | ||
|
|
abc015aec0 | ||
|
|
4ef8d27b1e | ||
|
|
40b6e603fc | ||
|
|
21498584b1 | ||
|
|
408bac09a1 | ||
|
|
582d879a77 | ||
|
|
38ff5152e0 | ||
|
|
d1d372e1bf | ||
|
|
5e4793433b | ||
|
|
54ad19f97e | ||
|
|
fc166650b3 | ||
|
|
2acc295259 | ||
|
|
4b3ed1310d | ||
|
|
b2cfd1517c | ||
|
|
b13d27ccd6 | ||
|
|
68e0088016 | ||
|
|
bd1e83989d | ||
|
|
263dfa6be7 | ||
|
|
eb55f93864 | ||
|
|
8589105e44 |
37
README.md
37
README.md
@@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
### 介绍
|
### 介绍
|
||||||
|
|
||||||
web 版 **linux(终端[终端回放] 文件 脚本 进程 计划任务)、数据库(mysql postgres oracle sqlserver 达梦 高斯 sqlite)、redis(单机 哨兵 集群)、mongo 等集工单流程审批于一体的统一管理操作平台**
|
web 版 **linux(终端[终端回放、命令过滤] 文件 脚本 进程 计划任务)、数据库(mysql postgres oracle sqlserver 达梦 高斯 sqlite)数据同步 数据迁移、redis(单机 哨兵 集群)、mongo 等集工单流程审批于一体的统一管理操作平台**
|
||||||
|
|
||||||
### 开发语言与主要框架
|
### 开发语言与主要框架
|
||||||
|
|
||||||
@@ -45,56 +45,61 @@ http://go.mayfly.run
|
|||||||
|
|
||||||
### 系统核心功能截图
|
### 系统核心功能截图
|
||||||
|
|
||||||
##### 记录操作记录
|
#### 首页
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
#### 机器操作
|
#### 机器操作
|
||||||
|
|
||||||
##### 状态查看
|
##### 状态查看
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
##### ssh 终端
|
##### ssh 终端
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
##### 文件操作
|
##### 文件操作
|
||||||
|
|
||||||

|

|
||||||

|
|
||||||
|

|
||||||
|
|
||||||
#### 数据库操作
|
#### 数据库操作
|
||||||
|
|
||||||
##### sql 编辑器
|
##### sql 编辑器
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
##### 在线增删改查数据
|
##### 在线增删改查数据
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
#### Redis 操作
|
#### Redis 操作
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
#### Mongo 操作
|
#### Mongo 操作
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
##### 系统管理
|
#### 工单流程审批
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### 系统管理
|
||||||
|
|
||||||
##### 账号管理
|
##### 账号管理
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
##### 角色管理
|
##### 角色管理
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
##### 资源管理
|
##### 菜单资源管理
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
**其他更多功能&操作指南可查看在线文档**: https://www.yuque.com/may-fly/mayfly-go
|
**其他更多功能&操作指南可查看在线文档**: https://www.yuque.com/may-fly/mayfly-go
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
server:
|
server:
|
||||||
image: mayfly-go:v1.3.1
|
image: ccr.ccs.tencentyun.com/mayfly/mayfly-go:v1.8.5
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
|||||||
@@ -10,19 +10,19 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "^2.3.1",
|
"@element-plus/icons-vue": "^2.3.1",
|
||||||
"@vueuse/core": "^10.8.0",
|
"@vueuse/core": "^10.9.0",
|
||||||
"asciinema-player": "^3.7.0",
|
"asciinema-player": "^3.7.1",
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
"clipboard": "^2.0.11",
|
"clipboard": "^2.0.11",
|
||||||
"countup.js": "^2.8.0",
|
|
||||||
"cropperjs": "^1.6.1",
|
"cropperjs": "^1.6.1",
|
||||||
|
"dayjs": "^1.11.11",
|
||||||
"echarts": "^5.5.0",
|
"echarts": "^5.5.0",
|
||||||
"element-plus": "^2.6.0",
|
"element-plus": "^2.7.3",
|
||||||
"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.46.0",
|
"monaco-editor": "^0.48.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",
|
||||||
@@ -32,9 +32,10 @@
|
|||||||
"sortablejs": "^1.15.2",
|
"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.21",
|
"vue": "^3.4.27",
|
||||||
"vue-router": "^4.3.0",
|
"vue-router": "^4.3.2",
|
||||||
"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",
|
||||||
@@ -48,15 +49,15 @@
|
|||||||
"@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.4",
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
"@vue/compiler-sfc": "^3.4.21",
|
"@vue/compiler-sfc": "^3.4.27",
|
||||||
"code-inspector-plugin": "^0.4.5",
|
"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.21.1",
|
"eslint-plugin-vue": "^9.25.0",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"sass": "^1.69.0",
|
"sass": "^1.77.1",
|
||||||
"typescript": "^5.3.2",
|
"typescript": "^5.4.5",
|
||||||
"vite": "^5.1.4",
|
"vite": "^5.2.11",
|
||||||
"vue-eslint-parser": "^9.4.2"
|
"vue-eslint-parser": "^9.4.2"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -1,9 +1,24 @@
|
|||||||
import EnumValue from './Enum';
|
import EnumValue from './Enum';
|
||||||
|
|
||||||
|
// 资源类型
|
||||||
|
export const ResourceTypeEnum = {
|
||||||
|
Machine: EnumValue.of(1, '机器').setExtra({ icon: 'Monitor', iconColor: 'var(--el-color-primary)' }).tagTypeSuccess(),
|
||||||
|
Db: EnumValue.of(2, '数据库实例').setExtra({ icon: 'Coin', iconColor: 'var(--el-color-warning)' }).tagTypeWarning(),
|
||||||
|
Redis: EnumValue.of(3, 'redis').setExtra({ icon: 'iconfont icon-redis', iconColor: 'var(--el-color-danger)' }).tagTypeInfo(),
|
||||||
|
Mongo: EnumValue.of(4, 'mongo').setExtra({ icon: 'iconfont icon-mongo', iconColor: 'var(--el-color-success)' }).tagTypeDanger(),
|
||||||
|
};
|
||||||
|
|
||||||
// 标签关联的资源类型
|
// 标签关联的资源类型
|
||||||
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: ResourceTypeEnum.Machine,
|
||||||
|
Db: ResourceTypeEnum.Db,
|
||||||
|
Redis: ResourceTypeEnum.Redis,
|
||||||
|
Mongo: ResourceTypeEnum.Mongo,
|
||||||
|
|
||||||
|
MachineAuthCert: EnumValue.of(11, '机器-授权凭证').setExtra({ icon: 'Ticket', iconColor: 'var(--el-color-success)' }),
|
||||||
|
DbAuthCert: EnumValue.of(21, '数据库-授权凭证').setExtra({ icon: 'Ticket', iconColor: 'var(--el-color-success)' }),
|
||||||
|
DbName: EnumValue.of(22, '数据库').setExtra({ icon: 'Coin' }),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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.4',
|
version: 'v1.8.5',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import request from './request';
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
login: (param: any) => request.post('/auth/accounts/login', param),
|
login: (param: any) => request.post('/auth/accounts/login', param),
|
||||||
|
refreshToken: (param: any) => request.get('/auth/accounts/refreshToken', param),
|
||||||
otpVerify: (param: any) => request.post('/auth/accounts/otp-verify', param),
|
otpVerify: (param: any) => request.post('/auth/accounts/otp-verify', param),
|
||||||
getPublicKey: () => request.get('/common/public-key'),
|
getPublicKey: () => request.get('/common/public-key'),
|
||||||
getConfigValue: (params: any) => request.get('/sys/configs/value', params),
|
getConfigValue: (params: any) => request.get('/sys/configs/value', params),
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
export const AccountUsernamePattern = {
|
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位大小写字母、数字、_-.:',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export enum ResultEnum {
|
|||||||
PARAM_ERROR = 405,
|
PARAM_ERROR = 405,
|
||||||
SERVER_ERROR = 500,
|
SERVER_ERROR = 500,
|
||||||
NO_PERMISSION = 501,
|
NO_PERMISSION = 501,
|
||||||
|
ACCESS_TOKEN_INVALID = 502, // accessToken失效
|
||||||
}
|
}
|
||||||
|
|
||||||
export const baseUrl: string = config.baseApiUrl;
|
export const baseUrl: string = config.baseApiUrl;
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
export function dateFormat2(fmt: string, date: Date) {
|
|
||||||
let ret;
|
|
||||||
const opt = {
|
|
||||||
'y+': date.getFullYear().toString(), // 年
|
|
||||||
'M+': (date.getMonth() + 1).toString(), // 月
|
|
||||||
'd+': date.getDate().toString(), // 日
|
|
||||||
'H+': date.getHours().toString(), // 时
|
|
||||||
'm+': date.getMinutes().toString(), // 分
|
|
||||||
's+': date.getSeconds().toString(), // 秒
|
|
||||||
'S+': date.getMilliseconds() ? date.getMilliseconds().toString() : '', // 毫秒
|
|
||||||
// 有其他格式化字符需求可以继续添加,必须转化成字符串
|
|
||||||
};
|
|
||||||
for (const k in opt) {
|
|
||||||
ret = new RegExp('(' + k + ')').exec(fmt);
|
|
||||||
if (ret) {
|
|
||||||
fmt = fmt.replace(ret[1], ret[1].length == 1 ? opt[k] : opt[k].padStart(ret[1].length, '0'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fmt;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function dateStrFormat(fmt: string, dateStr: string) {
|
|
||||||
return dateFormat2(fmt, new Date(dateStr));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function dateFormat(dateStr: string) {
|
|
||||||
if (!dateStr) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return dateFormat2('yyyy-MM-dd HH:mm:ss', new Date(dateStr));
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,18 @@
|
|||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化日期
|
||||||
|
* @param date 日期 字符串 Date 时间戳等
|
||||||
|
* @param format 格式化格式 默认 YYYY-MM-DD HH:mm:ss
|
||||||
|
* @returns 格式化后内容
|
||||||
|
*/
|
||||||
|
export function formatDate(date: any, format: string = 'YYYY-MM-DD HH:mm:ss') {
|
||||||
|
if (!date) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return dayjs(date).format(format);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 格式化字节单位
|
* 格式化字节单位
|
||||||
* @param size byte size
|
* @param size byte size
|
||||||
@@ -46,110 +61,6 @@ export function convertToBytes(sizeStr: string) {
|
|||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* 年(Y) 可用1-4个占位符
|
|
||||||
* 月(m)、日(d)、小时(H)、分(M)、秒(S) 可用1-2个占位符
|
|
||||||
* 星期(W) 可用1-3个占位符
|
|
||||||
* 季度(q为阿拉伯数字,Q为中文数字)可用1或4个占位符
|
|
||||||
*
|
|
||||||
* let date = new Date()
|
|
||||||
* formatDate(date, "YYYY-mm-dd HH:MM:SS") // 2020-02-09 14:04:23
|
|
||||||
* formatDate(date, "YYYY-mm-dd HH:MM:SS Q") // 2020-02-09 14:09:03 一
|
|
||||||
* formatDate(date, "YYYY-mm-dd HH:MM:SS WWW") // 2020-02-09 14:45:12 星期日
|
|
||||||
* formatDate(date, "YYYY-mm-dd HH:MM:SS QQQQ") // 2020-02-09 14:09:36 第一季度
|
|
||||||
* formatDate(date, "YYYY-mm-dd HH:MM:SS WWW QQQQ") // 2020-02-09 14:46:12 星期日 第一季度
|
|
||||||
*/
|
|
||||||
export function formatDate(date: Date, format: string) {
|
|
||||||
let we = date.getDay(); // 星期
|
|
||||||
let qut = Math.floor((date.getMonth() + 3) / 3).toString(); // 季度
|
|
||||||
const opt: any = {
|
|
||||||
'Y+': date.getFullYear().toString(), // 年
|
|
||||||
'm+': (date.getMonth() + 1).toString(), // 月(月份从0开始,要+1)
|
|
||||||
'd+': date.getDate().toString(), // 日
|
|
||||||
'H+': date.getHours().toString(), // 时
|
|
||||||
'M+': date.getMinutes().toString(), // 分
|
|
||||||
'S+': date.getSeconds().toString(), // 秒
|
|
||||||
'q+': qut, // 季度
|
|
||||||
};
|
|
||||||
// 中文数字 (星期)
|
|
||||||
const week: any = {
|
|
||||||
'0': '日',
|
|
||||||
'1': '一',
|
|
||||||
'2': '二',
|
|
||||||
'3': '三',
|
|
||||||
'4': '四',
|
|
||||||
'5': '五',
|
|
||||||
'6': '六',
|
|
||||||
};
|
|
||||||
// 中文数字(季度)
|
|
||||||
const quarter: any = {
|
|
||||||
'1': '一',
|
|
||||||
'2': '二',
|
|
||||||
'3': '三',
|
|
||||||
'4': '四',
|
|
||||||
};
|
|
||||||
if (/(W+)/.test(format)) format = format.replace(RegExp.$1, RegExp.$1.length > 1 ? (RegExp.$1.length > 2 ? '星期' + week[we] : '周' + week[we]) : week[we]);
|
|
||||||
if (/(Q+)/.test(format)) format = format.replace(RegExp.$1, RegExp.$1.length == 4 ? '第' + quarter[qut] + '季度' : quarter[qut]);
|
|
||||||
for (let k in opt) {
|
|
||||||
let r = new RegExp('(' + k + ')').exec(format);
|
|
||||||
// 若输入的长度不为1,则前面补零
|
|
||||||
if (r) format = format.replace(r[1], RegExp.$1.length == 1 ? opt[k] : opt[k].padStart(RegExp.$1.length, '0'));
|
|
||||||
}
|
|
||||||
return format;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 10秒: 10 * 1000
|
|
||||||
* 1分: 60 * 1000
|
|
||||||
* 1小时: 60 * 60 * 1000
|
|
||||||
* 24小时:60 * 60 * 24 * 1000
|
|
||||||
* 3天: 60 * 60* 24 * 1000 * 3
|
|
||||||
*
|
|
||||||
* let data = new Date()
|
|
||||||
* formatPast(data) // 刚刚
|
|
||||||
* formatPast(data - 11 * 1000) // 11秒前
|
|
||||||
* formatPast(data - 2 * 60 * 1000) // 2分钟前
|
|
||||||
* formatPast(data - 60 * 60 * 2 * 1000) // 2小时前
|
|
||||||
* formatPast(data - 60 * 60 * 2 * 1000) // 2小时前
|
|
||||||
* formatPast(data - 60 * 60 * 71 * 1000) // 2天前
|
|
||||||
* formatPast("2020-06-01") // 2020-06-01
|
|
||||||
* formatPast("2020-06-01", "YYYY-mm-dd HH:MM:SS WWW QQQQ") // 2020-06-01 08:00:00 星期一 第二季度
|
|
||||||
*/
|
|
||||||
export function formatPast(param: any, format: string = 'YYYY-mm-dd') {
|
|
||||||
// 传入格式处理、存储转换值
|
|
||||||
let t: any, s: any;
|
|
||||||
// 获取js 时间戳
|
|
||||||
let time: any = new Date().getTime();
|
|
||||||
// 是否是对象
|
|
||||||
typeof param === 'string' || 'object' ? (t = new Date(param).getTime()) : (t = param);
|
|
||||||
// 当前时间戳 - 传入时间戳
|
|
||||||
time = Number.parseInt(`${time - t}`);
|
|
||||||
if (time < 10000) {
|
|
||||||
// 10秒内
|
|
||||||
return '刚刚';
|
|
||||||
} else if (time < 60000 && time >= 10000) {
|
|
||||||
// 超过10秒少于1分钟内
|
|
||||||
s = Math.floor(time / 1000);
|
|
||||||
return `${s}秒前`;
|
|
||||||
} else if (time < 3600000 && time >= 60000) {
|
|
||||||
// 超过1分钟少于1小时
|
|
||||||
s = Math.floor(time / 60000);
|
|
||||||
return `${s}分钟前`;
|
|
||||||
} else if (time < 86400000 && time >= 3600000) {
|
|
||||||
// 超过1小时少于24小时
|
|
||||||
s = Math.floor(time / 3600000);
|
|
||||||
return `${s}小时前`;
|
|
||||||
} else if (time < 259200000 && time >= 86400000) {
|
|
||||||
// 超过1天少于3天内
|
|
||||||
s = Math.floor(time / 86400000);
|
|
||||||
return `${s}天前`;
|
|
||||||
} else {
|
|
||||||
// 超过3天
|
|
||||||
let date = typeof param === 'string' || 'object' ? new Date(param) : param;
|
|
||||||
return formatDate(date, format);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 格式化指定时间数为人性化可阅读的内容(默认time为秒单位)
|
* 格式化指定时间数为人性化可阅读的内容(默认time为秒单位)
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
// 根据对象访问路径,获取对应的值
|
/**
|
||||||
|
* 根据对象访问路径,获取对应的值
|
||||||
|
*
|
||||||
|
* @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) {
|
export function getValueByPath(obj: any, path: string) {
|
||||||
const keys = path.split('.');
|
const keys = path.split('.');
|
||||||
let result = obj;
|
let result = obj;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { randomUuid } from './string';
|
import { randomUuid } from './string';
|
||||||
|
|
||||||
const TokenKey = 'm-token';
|
const TokenKey = 'm-token';
|
||||||
|
const RefreshTokenKey = 'm-refresh-token';
|
||||||
const UserKey = 'm-user';
|
const UserKey = 'm-user';
|
||||||
const TagViewsKey = 'm-tagViews';
|
const TagViewsKey = 'm-tagViews';
|
||||||
const ClientIdKey = 'm-clientId';
|
const ClientIdKey = 'm-clientId';
|
||||||
@@ -15,6 +16,14 @@ export function saveToken(token: string) {
|
|||||||
setLocal(TokenKey, token);
|
setLocal(TokenKey, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getRefreshToken(): string {
|
||||||
|
return getLocal(RefreshTokenKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveRefreshToken(refreshToken: string) {
|
||||||
|
return setLocal(RefreshTokenKey, refreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
// 获取登录用户基础信息
|
// 获取登录用户基础信息
|
||||||
export function getUser() {
|
export function getUser() {
|
||||||
return getLocal(UserKey);
|
return getLocal(UserKey);
|
||||||
@@ -39,6 +48,7 @@ export function getThemeConfig() {
|
|||||||
export function clearUser() {
|
export function clearUser() {
|
||||||
removeLocal(TokenKey);
|
removeLocal(TokenKey);
|
||||||
removeLocal(UserKey);
|
removeLocal(UserKey);
|
||||||
|
removeLocal(RefreshTokenKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTagViews() {
|
export function getTagViews() {
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -159,7 +159,7 @@
|
|||||||
@current-change="handlePageNumChange"
|
@current-change="handlePageNumChange"
|
||||||
@size-change="handlePageSizeChange"
|
@size-change="handlePageSizeChange"
|
||||||
style="text-align: right"
|
style="text-align: right"
|
||||||
layout="prev, pager, next, total, sizes, jumper"
|
layout="prev, pager, next, total, sizes"
|
||||||
:total="total"
|
:total="total"
|
||||||
v-model:current-page="queryForm.pageNum"
|
v-model:current-page="queryForm.pageNum"
|
||||||
v-model:page-size="queryForm.pageSize"
|
v-model:page-size="queryForm.pageSize"
|
||||||
@@ -185,7 +185,7 @@ 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;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import EnumValue from '@/common/Enum';
|
import EnumValue from '@/common/Enum';
|
||||||
import { dateFormat } from '@/common/utils/date';
|
import { formatDate } from '@/common/utils/format';
|
||||||
import { getValueByPath } from '@/common/utils/object';
|
import { getValueByPath } from '@/common/utils/object';
|
||||||
import { getTextWidth } from '@/common/utils/string';
|
import { getTextWidth } from '@/common/utils/string';
|
||||||
|
|
||||||
@@ -172,7 +172,7 @@ export class TableColumn {
|
|||||||
*/
|
*/
|
||||||
isTime(): TableColumn {
|
isTime(): TableColumn {
|
||||||
this.setFormatFunc((data: any, prop: string) => {
|
this.setFormatFunc((data: any, prop: string) => {
|
||||||
return dateFormat(getValueByPath(data, prop));
|
return formatDate(getValueByPath(data, prop));
|
||||||
});
|
});
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|||||||
505
mayfly_go_web/src/components/terminal-rdp/MachineRdp.vue
Normal file
505
mayfly_go_web/src/components/terminal-rdp/MachineRdp.vue
Normal file
@@ -0,0 +1,505 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<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
|
||||||
|
v-if="!state.fullscreen"
|
||||||
|
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>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
v-if="!state.fullscreen"
|
||||||
|
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';
|
||||||
|
import { joinClientParams } from '@/common/request';
|
||||||
|
|
||||||
|
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 + '&' + joinClientParams());
|
||||||
|
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>
|
||||||
137
mayfly_go_web/src/components/terminal-rdp/MachineRdpDialog.vue
Normal file
137
mayfly_go_web/src/components/terminal-rdp/MachineRdpDialog.vue
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
<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" :auth-cert="authCert" @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,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
authCert: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ 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 themes from './themes';
|
||||||
|
import { TrzszFilter } from 'trzsz';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
// mounted时,是否执行init方法
|
// mounted时,是否执行init方法
|
||||||
@@ -101,7 +102,6 @@ function init() {
|
|||||||
}
|
}
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
initTerm();
|
initTerm();
|
||||||
initSocket();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,16 +124,12 @@ function initTerm() {
|
|||||||
state.addon.fit = fitAddon;
|
state.addon.fit = fitAddon;
|
||||||
term.loadAddon(fitAddon);
|
term.loadAddon(fitAddon);
|
||||||
fitTerminal();
|
fitTerminal();
|
||||||
|
// 注册窗口大小监听器
|
||||||
|
useEventListener('resize', debounce(fitTerminal, 400));
|
||||||
|
|
||||||
// 注册搜索组件
|
initSocket();
|
||||||
const searchAddon = new SearchAddon();
|
// 注册其他插件
|
||||||
state.addon.search = searchAddon;
|
loadAddon();
|
||||||
term.loadAddon(searchAddon);
|
|
||||||
|
|
||||||
// 注册 url link组件
|
|
||||||
const weblinks = new WebLinksAddon();
|
|
||||||
state.addon.weblinks = weblinks;
|
|
||||||
term.loadAddon(weblinks);
|
|
||||||
|
|
||||||
// 注册自定义快捷键
|
// 注册自定义快捷键
|
||||||
term.attachCustomKeyEventHandler((event: KeyboardEvent) => {
|
term.attachCustomKeyEventHandler((event: KeyboardEvent) => {
|
||||||
@@ -148,22 +144,16 @@ function initTerm() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function initSocket() {
|
function initSocket() {
|
||||||
if (props.socketUrl) {
|
if (!props.socketUrl) {
|
||||||
socket = new WebSocket(`${props.socketUrl}&rows=${term?.rows}&cols=${term?.cols}`);
|
return;
|
||||||
}
|
}
|
||||||
|
socket = new WebSocket(`${props.socketUrl}&rows=${term?.rows}&cols=${term?.cols}`);
|
||||||
// 监听socket连接
|
// 监听socket连接
|
||||||
socket.onopen = () => {
|
socket.onopen = () => {
|
||||||
// 注册心跳
|
// 注册心跳
|
||||||
pingInterval = setInterval(sendPing, 15000);
|
pingInterval = setInterval(sendPing, 15000);
|
||||||
state.status = TerminalStatus.Connected;
|
state.status = TerminalStatus.Connected;
|
||||||
|
|
||||||
// 注册 terminal 事件
|
|
||||||
term.onResize((event) => sendResize(event.cols, event.rows));
|
|
||||||
term.onData((event) => sendCmd(event));
|
|
||||||
|
|
||||||
// // 注册窗口大小监听器
|
|
||||||
useEventListener('resize', debounce(fitTerminal, 400));
|
|
||||||
focus();
|
focus();
|
||||||
|
|
||||||
// 如果有初始要执行的命令,则发送执行命令
|
// 如果有初始要执行的命令,则发送执行命令
|
||||||
@@ -183,14 +173,63 @@ function initSocket() {
|
|||||||
console.log('terminal socket close...', e.reason);
|
console.log('terminal socket close...', e.reason);
|
||||||
state.status = TerminalStatus.Disconnected;
|
state.status = TerminalStatus.Disconnected;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 监听socket消息
|
|
||||||
socket.onmessage = (msg: any) => {
|
|
||||||
// msg.data是真正后端返回的数据
|
|
||||||
term.write(msg.data);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadAddon() {
|
||||||
|
// 注册搜索组件
|
||||||
|
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 getTerminalTheme = () => {
|
||||||
const terminalTheme = themeConfig.value.terminalTheme;
|
const terminalTheme = themeConfig.value.terminalTheme;
|
||||||
// 如果不是自定义主题,则返回内置主题
|
// 如果不是自定义主题,则返回内置主题
|
||||||
@@ -229,7 +268,7 @@ enum MsgType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const send = (msg: any) => {
|
const send = (msg: any) => {
|
||||||
state.status == TerminalStatus.Connected && socket.send(msg);
|
state.status == TerminalStatus.Connected && socket?.send(msg);
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendResize = (cols: number, rows: number) => {
|
const sendResize = (cols: number, rows: number) => {
|
||||||
@@ -266,7 +305,7 @@ const getStatus = (): TerminalStatus => {
|
|||||||
return state.status;
|
return state.status;
|
||||||
};
|
};
|
||||||
|
|
||||||
defineExpose({ init, fitTerminal, focus, clear, close, getStatus, sendResize });
|
defineExpose({ init, fitTerminal, focus, clear, close, getStatus, sendResize, write2Term, writeln2Term });
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
#terminal-body {
|
#terminal-body {
|
||||||
@@ -276,9 +315,9 @@ defineExpose({ init, fitTerminal, focus, clear, close, getStatus, sendResize });
|
|||||||
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"
|
||||||
@@ -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';
|
||||||
|
|||||||
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>
|
||||||
@@ -44,6 +44,7 @@ export const usePageTable = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
let res = await api.request(sp);
|
let res = await api.request(sp);
|
||||||
|
res.list = res.list || [];
|
||||||
dataCallBack && (res = await dataCallBack(res));
|
dataCallBack && (res = await dataCallBack(res));
|
||||||
|
|
||||||
if (pageable) {
|
if (pageable) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import router from '@/router';
|
import router from '@/router';
|
||||||
import { getClientId, getToken } from '@/common/utils/storage';
|
import { clearUser, getClientId, getRefreshToken, getToken, saveRefreshToken, saveToken } from '@/common/utils/storage';
|
||||||
import { templateResolve } from '@/common/utils/string';
|
import { templateResolve } from '@/common/utils/string';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
import { createFetch } from '@vueuse/core';
|
import { createFetch } from '@vueuse/core';
|
||||||
@@ -8,6 +8,7 @@ import { Result, ResultEnum } from '@/common/request';
|
|||||||
import config from '@/common/config';
|
import config from '@/common/config';
|
||||||
import { unref } from 'vue';
|
import { unref } from 'vue';
|
||||||
import { URL_401 } from '@/router/staticRouter';
|
import { URL_401 } from '@/router/staticRouter';
|
||||||
|
import openApi from '@/common/openApi';
|
||||||
|
|
||||||
const baseUrl: string = config.baseApiUrl;
|
const baseUrl: string = config.baseApiUrl;
|
||||||
|
|
||||||
@@ -16,7 +17,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();
|
||||||
@@ -88,61 +89,104 @@ export function useApiFetch<T>(api: Api, params: any = null, reqOptions: Request
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
execute: async function () {
|
execute: async function () {
|
||||||
try {
|
return execUaf(uaf);
|
||||||
await uaf.execute(true);
|
|
||||||
} catch (e: any) {
|
|
||||||
const rejectPromise = Promise.reject(e);
|
|
||||||
|
|
||||||
if (e?.name == 'AbortError') {
|
|
||||||
console.log('请求已取消');
|
|
||||||
return rejectPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
const respStatus = uaf.response.value?.status;
|
|
||||||
if (respStatus == 404) {
|
|
||||||
ElMessage.error('请求接口不存在');
|
|
||||||
return rejectPromise;
|
|
||||||
}
|
|
||||||
if (respStatus == 500) {
|
|
||||||
ElMessage.error('服务器响应异常');
|
|
||||||
return rejectPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error(e);
|
|
||||||
ElMessage.error('网络请求错误');
|
|
||||||
return rejectPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result: Result = uaf.data.value as any;
|
|
||||||
if (!result) {
|
|
||||||
ElMessage.error('网络请求失败');
|
|
||||||
return Promise.reject(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果返回为成功结果,则将结果的data赋值给响应式data
|
|
||||||
if (result.code === ResultEnum.SUCCESS) {
|
|
||||||
uaf.data.value = result.data;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果提示没有权限,则跳转至无权限页面
|
|
||||||
if (result.code === ResultEnum.NO_PERMISSION) {
|
|
||||||
router.push({
|
|
||||||
path: URL_401,
|
|
||||||
});
|
|
||||||
return Promise.reject(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果返回的code不为成功,则会返回对应的错误msg,则直接统一通知即可。忽略登录超时或没有权限的提示(直接跳转至401页面)
|
|
||||||
if (result.msg && result?.code != ResultEnum.NO_PERMISSION) {
|
|
||||||
ElMessage.error(result.msg);
|
|
||||||
uaf.error.value = new Error(result.msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.reject(result);
|
|
||||||
},
|
},
|
||||||
isFetching: uaf.isFetching,
|
isFetching: uaf.isFetching,
|
||||||
data: uaf.data,
|
data: uaf.data,
|
||||||
abort: uaf.abort,
|
abort: uaf.abort,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let refreshingToken = false;
|
||||||
|
let queue: any[] = [];
|
||||||
|
|
||||||
|
async function execUaf(uaf: any) {
|
||||||
|
try {
|
||||||
|
await uaf.execute(true);
|
||||||
|
} catch (e: any) {
|
||||||
|
const rejectPromise = Promise.reject(e);
|
||||||
|
|
||||||
|
if (e?.name == 'AbortError') {
|
||||||
|
console.log('请求已取消');
|
||||||
|
return rejectPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
const respStatus = uaf.response.value?.status;
|
||||||
|
if (respStatus == 404) {
|
||||||
|
ElMessage.error('请求接口不存在');
|
||||||
|
return rejectPromise;
|
||||||
|
}
|
||||||
|
if (respStatus == 500) {
|
||||||
|
ElMessage.error('服务器响应异常');
|
||||||
|
return rejectPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(e);
|
||||||
|
ElMessage.error('网络请求错误');
|
||||||
|
return rejectPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: Result = uaf.data.value as any;
|
||||||
|
if (!result) {
|
||||||
|
ElMessage.error('网络请求失败');
|
||||||
|
return Promise.reject(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultCode = result.code;
|
||||||
|
|
||||||
|
// 如果返回为成功结果,则将结果的data赋值给响应式data
|
||||||
|
if (resultCode === ResultEnum.SUCCESS) {
|
||||||
|
uaf.data.value = result.data;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是accessToken失效,则使用refreshToken刷新token
|
||||||
|
if (resultCode == ResultEnum.ACCESS_TOKEN_INVALID) {
|
||||||
|
if (refreshingToken) {
|
||||||
|
// 请求加入队列等待, 防止并发多次请求refreshToken
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
queue.push(() => {
|
||||||
|
resolve(execUaf(uaf));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
refreshingToken = true;
|
||||||
|
const res = await openApi.refreshToken({ refresh_token: getRefreshToken() });
|
||||||
|
saveToken(res.token);
|
||||||
|
saveRefreshToken(res.refresh_token);
|
||||||
|
// 重新缓存后端用户权限code
|
||||||
|
await openApi.getPermissions();
|
||||||
|
|
||||||
|
// 执行accessToken失效的请求
|
||||||
|
queue.forEach((resolve: any) => {
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
clearUser();
|
||||||
|
} finally {
|
||||||
|
refreshingToken = false;
|
||||||
|
queue = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
await execUaf(uaf);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果提示没有权限,则跳转至无权限页面
|
||||||
|
if (resultCode === ResultEnum.NO_PERMISSION) {
|
||||||
|
router.push({
|
||||||
|
path: URL_401,
|
||||||
|
});
|
||||||
|
return Promise.reject(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果返回的code不为成功,则会返回对应的错误msg,则直接统一通知即可。忽略登录超时或没有权限的提示(直接跳转至401页面)
|
||||||
|
if (result.msg && resultCode != ResultEnum.NO_PERMISSION) {
|
||||||
|
ElMessage.error(result.msg);
|
||||||
|
uaf.error.value = new Error(result.msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(result);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
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);
|
|
||||||
};
|
|
||||||
@@ -89,13 +89,18 @@ 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);
|
||||||
|
|
||||||
// 将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 +131,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -152,6 +159,6 @@ export function dynamicImport(dynamicViewsModules: Record<string, Function>, com
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error(`未匹配到[${component}]组件名对应的组件文件`);
|
console.warn(`未匹配到[${component}]组件名对应的组件文件`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// 错误页面路由
|
// 错误页面路由
|
||||||
|
|||||||
29
mayfly_go_web/src/store/autoOpenResource.ts
Normal file
29
mayfly_go_web/src/store/autoOpenResource.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动打开资源
|
||||||
|
*/
|
||||||
|
export const useAutoOpenResource = defineStore('autoOpenResource', {
|
||||||
|
state: () => ({
|
||||||
|
autoOpenResource: {
|
||||||
|
machineCodePath: '',
|
||||||
|
dbCodePath: '',
|
||||||
|
redisCodePath: '',
|
||||||
|
mongoCodePath: '',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
setMachineCodePath(codePath: string) {
|
||||||
|
this.autoOpenResource.machineCodePath = codePath;
|
||||||
|
},
|
||||||
|
setDbCodePath(codePath: string) {
|
||||||
|
this.autoOpenResource.dbCodePath = codePath;
|
||||||
|
},
|
||||||
|
setRedisCodePath(codePath: string) {
|
||||||
|
this.autoOpenResource.redisCodePath = codePath;
|
||||||
|
},
|
||||||
|
setMongoCodePath(codePath: string) {
|
||||||
|
this.autoOpenResource.mongoCodePath = codePath;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { dateFormat2 } from '@/common/utils/date';
|
import { formatDate } from '@/common/utils/format';
|
||||||
import { useUserInfo } from '@/store/userInfo';
|
import { useUserInfo } from '@/store/userInfo';
|
||||||
import { getSysStyleConfig } from '@/common/sysconfig';
|
import { getSysStyleConfig } from '@/common/sysconfig';
|
||||||
import { getLocal, getThemeConfig } from '@/common/utils/storage';
|
import { getLocal, getThemeConfig } from '@/common/utils/storage';
|
||||||
@@ -114,7 +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',
|
terminalTheme: 'light',
|
||||||
// ssh终端字体颜色
|
// ssh终端字体颜色
|
||||||
terminalForeground: '#C5C8C6',
|
terminalForeground: '#C5C8C6',
|
||||||
// ssh终端背景色
|
// ssh终端背景色
|
||||||
@@ -191,7 +191,7 @@ export const useThemeConfig = defineStore('themeConfig', {
|
|||||||
},
|
},
|
||||||
// 设置水印时间为当前时间
|
// 设置水印时间为当前时间
|
||||||
setWatermarkNowTime() {
|
setWatermarkNowTime() {
|
||||||
this.themeConfig.watermarkText[1] = dateFormat2('yyyy-MM-dd HH:mm:ss', new Date());
|
this.themeConfig.watermarkText[1] = formatDate(new Date());
|
||||||
},
|
},
|
||||||
// 切换暗黑模式
|
// 切换暗黑模式
|
||||||
switchDark(isDark: boolean) {
|
switchDark(isDark: boolean) {
|
||||||
@@ -207,7 +207,7 @@ export const useThemeConfig = defineStore('themeConfig', {
|
|||||||
if (isDark) {
|
if (isDark) {
|
||||||
this.themeConfig.terminalTheme = 'dark';
|
this.themeConfig.terminalTheme = 'dark';
|
||||||
} else {
|
} else {
|
||||||
this.themeConfig.terminalTheme = 'solarizedLight';
|
this.themeConfig.terminalTheme = 'light';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
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';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<el-drawer @open="initSort" :title="title" v-model="visible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false">
|
<el-drawer @open="initSort" :title="title" v-model="visible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
|
||||||
<template #header>
|
<template #header>
|
||||||
<DrawerHeader :header="title" :back="cancel" />
|
<DrawerHeader :header="title" :back="cancel" />
|
||||||
</template>
|
</template>
|
||||||
@@ -21,6 +21,10 @@
|
|||||||
<el-input v-model.trim="form.remark" placeholder="备注" auto-complete="off" clearable></el-input>
|
<el-input v-model.trim="form.remark" placeholder="备注" auto-complete="off" clearable></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item ref="tagSelectRef" prop="codePaths" label="关联资源">
|
||||||
|
<tag-tree-check height="300px" v-model="form.codePaths" :tag-type="[TagResourceTypeEnum.DbName.value, TagResourceTypeEnum.Redis.value]" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
<el-divider content-position="left">审批节点</el-divider>
|
<el-divider content-position="left">审批节点</el-divider>
|
||||||
|
|
||||||
<el-table ref="taskTableRef" :data="tasks" row-key="taskKey" stripe style="width: 100%">
|
<el-table ref="taskTableRef" :data="tasks" row-key="taskKey" stripe style="width: 100%">
|
||||||
@@ -70,6 +74,8 @@ import AccountSelectFormItem from '@/views/system/account/components/AccountSele
|
|||||||
import Sortable from 'sortablejs';
|
import Sortable from 'sortablejs';
|
||||||
import { randomUuid } from '../../common/utils/string';
|
import { randomUuid } from '../../common/utils/string';
|
||||||
import { ProcdefStatus } from './enums';
|
import { ProcdefStatus } from './enums';
|
||||||
|
import TagTreeCheck from '../ops/component/TagTreeCheck.vue';
|
||||||
|
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
data: {
|
data: {
|
||||||
@@ -115,6 +121,7 @@ const state = reactive({
|
|||||||
remark: null,
|
remark: null,
|
||||||
// 流程的审批节点任务
|
// 流程的审批节点任务
|
||||||
tasks: '',
|
tasks: '',
|
||||||
|
codePaths: [],
|
||||||
},
|
},
|
||||||
sortable: '' as any,
|
sortable: '' as any,
|
||||||
});
|
});
|
||||||
@@ -126,6 +133,7 @@ const { isFetching: saveBtnLoading, execute: saveFlowDefExec } = procdefApi.save
|
|||||||
watch(props, (newValue: any) => {
|
watch(props, (newValue: any) => {
|
||||||
if (newValue.data) {
|
if (newValue.data) {
|
||||||
state.form = { ...newValue.data };
|
state.form = { ...newValue.data };
|
||||||
|
state.form.codePaths = newValue.data.tags?.map((tag: any) => tag.codePath);
|
||||||
const tasks = JSON.parse(state.form.tasks);
|
const tasks = JSON.parse(state.form.tasks);
|
||||||
tasks.forEach((t: any) => {
|
tasks.forEach((t: any) => {
|
||||||
t.userId = Number.parseInt(t.userId);
|
t.userId = Number.parseInt(t.userId);
|
||||||
@@ -160,25 +168,26 @@ const deleteTask = (idx: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const btnOk = async () => {
|
const btnOk = async () => {
|
||||||
formRef.value.validate(async (valid: boolean) => {
|
try {
|
||||||
if (!valid) {
|
await formRef.value.validate();
|
||||||
ElMessage.error('表单填写有误');
|
} catch (e: any) {
|
||||||
return false;
|
ElMessage.error('请正确填写信息');
|
||||||
}
|
return false;
|
||||||
const checkRes = checkTasks();
|
}
|
||||||
if (checkRes.err) {
|
|
||||||
ElMessage.error(checkRes.err);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.form.tasks = JSON.stringify(checkRes.tasks);
|
const checkRes = checkTasks();
|
||||||
await saveFlowDefExec();
|
if (checkRes.err) {
|
||||||
ElMessage.success('操作成功');
|
ElMessage.error(checkRes.err);
|
||||||
emit('val-change', state.form);
|
return false;
|
||||||
//重置表单域
|
}
|
||||||
formRef.value.resetFields();
|
|
||||||
state.form = {} as any;
|
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 = () => {
|
const checkTasks = () => {
|
||||||
|
|||||||
@@ -18,6 +18,10 @@
|
|||||||
<el-link @click="showProcdefTasks(data)" icon="view" type="primary" :underline="false"> </el-link>
|
<el-link @click="showProcdefTasks(data)" icon="view" type="primary" :underline="false"> </el-link>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template #codePaths="{ data }">
|
||||||
|
<TagCodePath :path="data.tags?.map((tag: any) => tag.codePath)" />
|
||||||
|
</template>
|
||||||
|
|
||||||
<template #action="{ data }">
|
<template #action="{ data }">
|
||||||
<el-button link v-if="actionBtns[perms.save]" @click="editFlowDef(data)" type="primary">编辑</el-button>
|
<el-button link v-if="actionBtns[perms.save]" @click="editFlowDef(data)" type="primary">编辑</el-button>
|
||||||
</template>
|
</template>
|
||||||
@@ -42,6 +46,7 @@ import { SearchItem } from '@/components/SearchForm';
|
|||||||
import ProcdefEdit from './ProcdefEdit.vue';
|
import ProcdefEdit from './ProcdefEdit.vue';
|
||||||
import ProcdefTasks from './components/ProcdefTasks.vue';
|
import ProcdefTasks from './components/ProcdefTasks.vue';
|
||||||
import { ProcdefStatus } from './enums';
|
import { ProcdefStatus } from './enums';
|
||||||
|
import TagCodePath from '../ops/component/TagCodePath.vue';
|
||||||
|
|
||||||
const perms = {
|
const perms = {
|
||||||
save: 'flow:procdef:save',
|
save: 'flow:procdef:save',
|
||||||
@@ -55,6 +60,7 @@ const columns = [
|
|||||||
TableColumn.new('status', '状态').typeTag(ProcdefStatus),
|
TableColumn.new('status', '状态').typeTag(ProcdefStatus),
|
||||||
TableColumn.new('remark', '备注'),
|
TableColumn.new('remark', '备注'),
|
||||||
TableColumn.new('tasks', '审批节点').isSlot().alignCenter().setMinWidth(60),
|
TableColumn.new('tasks', '审批节点').isSlot().alignCenter().setMinWidth(60),
|
||||||
|
TableColumn.new('codePaths', '关联资源').isSlot().setMinWidth('250px'),
|
||||||
TableColumn.new('creator', '创建账号'),
|
TableColumn.new('creator', '创建账号'),
|
||||||
TableColumn.new('createTime', '创建时间').isTime(),
|
TableColumn.new('createTime', '创建时间').isTime(),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -17,11 +17,11 @@
|
|||||||
<AccountInfo :account-id="procinst.creatorId" :username="procinst.creator" />
|
<AccountInfo :account-id="procinst.creatorId" :username="procinst.creator" />
|
||||||
<!-- {{ procinst.creator }} -->
|
<!-- {{ procinst.creator }} -->
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="发起时间">{{ dateFormat(procinst.createTime) }}</el-descriptions-item>
|
<el-descriptions-item label="发起时间">{{ formatDate(procinst.createTime) }}</el-descriptions-item>
|
||||||
|
|
||||||
<div v-if="procinst.duration">
|
<div v-if="procinst.duration">
|
||||||
<el-descriptions-item label="持续时间">{{ formatTime(procinst.duration) }}</el-descriptions-item>
|
<el-descriptions-item label="持续时间">{{ formatTime(procinst.duration) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="结束时间">{{ dateFormat(procinst.endTime) }}</el-descriptions-item>
|
<el-descriptions-item label="结束时间">{{ formatDate(procinst.endTime) }}</el-descriptions-item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-descriptions-item label="流程状态">
|
<el-descriptions-item label="流程状态">
|
||||||
@@ -86,11 +86,11 @@ import { procinstApi } from './api';
|
|||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||||
import { FlowBizType, ProcinstBizStatus, ProcinstTaskStatus, ProcinstStatus } from './enums';
|
import { FlowBizType, ProcinstBizStatus, ProcinstTaskStatus, ProcinstStatus } from './enums';
|
||||||
import { dateFormat } from '@/common/utils/date';
|
|
||||||
import ProcdefTasks from './components/ProcdefTasks.vue';
|
import ProcdefTasks from './components/ProcdefTasks.vue';
|
||||||
import { formatTime } from '@/common/utils/format';
|
import { formatTime } from '@/common/utils/format';
|
||||||
import EnumTag from '@/components/enumtag/EnumTag.vue';
|
import EnumTag from '@/components/enumtag/EnumTag.vue';
|
||||||
import AccountInfo from '@/views/system/account/components/AccountInfo.vue';
|
import AccountInfo from '@/views/system/account/components/AccountInfo.vue';
|
||||||
|
import { formatDate } from '@/common/utils/format';
|
||||||
|
|
||||||
const DbSqlExecBiz = defineAsyncComponent(() => import('./flowbiz/DbSqlExecBiz.vue'));
|
const DbSqlExecBiz = defineAsyncComponent(() => import('./flowbiz/DbSqlExecBiz.vue'));
|
||||||
const RedisRunWriteCmdBiz = defineAsyncComponent(() => import('./flowbiz/RedisRunWriteCmdBiz.vue'));
|
const RedisRunWriteCmdBiz = defineAsyncComponent(() => import('./flowbiz/RedisRunWriteCmdBiz.vue'));
|
||||||
|
|||||||
@@ -50,11 +50,17 @@ import { FlowBizType, ProcinstBizStatus, ProcinstStatus } from './enums';
|
|||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
import { formatTime } from '@/common/utils/format';
|
import { formatTime } from '@/common/utils/format';
|
||||||
|
|
||||||
const searchItems = [SearchItem.select('status', '流程状态').withEnum(ProcinstStatus), SearchItem.select('bizType', '业务类型').withEnum(FlowBizType)];
|
const searchItems = [
|
||||||
|
SearchItem.select('status', '流程状态').withEnum(ProcinstStatus),
|
||||||
|
SearchItem.select('bizType', '业务类型').withEnum(FlowBizType),
|
||||||
|
SearchItem.input('bizKey', '业务key'),
|
||||||
|
];
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
TableColumn.new('bizType', '业务').typeTag(FlowBizType),
|
TableColumn.new('bizType', '业务').typeTag(FlowBizType),
|
||||||
TableColumn.new('remark', '备注'),
|
TableColumn.new('remark', '备注'),
|
||||||
TableColumn.new('creator', '发起人'),
|
TableColumn.new('creator', '发起人'),
|
||||||
|
TableColumn.new('bizKey', '业务key'),
|
||||||
TableColumn.new('procdefName', '流程名'),
|
TableColumn.new('procdefName', '流程名'),
|
||||||
TableColumn.new('status', '流程状态').typeTag(ProcinstStatus),
|
TableColumn.new('status', '流程状态').typeTag(ProcinstStatus),
|
||||||
TableColumn.new('bizStatus', '业务状态').typeTag(ProcinstBizStatus),
|
TableColumn.new('bizStatus', '业务状态').typeTag(ProcinstBizStatus),
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ const columns = [
|
|||||||
TableColumn.new('procinst.creator', '发起人'),
|
TableColumn.new('procinst.creator', '发起人'),
|
||||||
TableColumn.new('procinst.status', '流程状态').typeTag(ProcinstStatus),
|
TableColumn.new('procinst.status', '流程状态').typeTag(ProcinstStatus),
|
||||||
TableColumn.new('status', '任务状态').typeTag(ProcinstTaskStatus),
|
TableColumn.new('status', '任务状态').typeTag(ProcinstTaskStatus),
|
||||||
|
TableColumn.new('procinst.bizKey', '业务key'),
|
||||||
TableColumn.new('procinst.procdefName', '流程名'),
|
TableColumn.new('procinst.procdefName', '流程名'),
|
||||||
TableColumn.new('taskName', '当前节点'),
|
TableColumn.new('taskName', '当前节点'),
|
||||||
TableColumn.new('procinst.createTime', '发起时间').isTime(),
|
TableColumn.new('procinst.createTime', '发起时间').isTime(),
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import Api from '@/common/Api';
|
|||||||
|
|
||||||
export const procdefApi = {
|
export const procdefApi = {
|
||||||
list: Api.newGet('/flow/procdefs'),
|
list: Api.newGet('/flow/procdefs'),
|
||||||
getByKey: Api.newGet('/flow/procdefs/{key}'),
|
getByResource: Api.newGet('/flow/procdefs/{resourceType}/{resourceCode}'),
|
||||||
save: Api.newPost('/flow/procdefs'),
|
save: Api.newPost('/flow/procdefs'),
|
||||||
del: Api.newDelete('/flow/procdefs/{id}'),
|
del: Api.newDelete('/flow/procdefs/{id}'),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<el-step v-for="task in tasksArr" :status="getStepStatus(task)" :title="task.name" :key="task.taskKey">
|
<el-step v-for="task in tasksArr" :status="getStepStatus(task)" :title="task.name" :key="task.taskKey">
|
||||||
<template #description>
|
<template #description>
|
||||||
<div>{{ `${task.accountUsername}(${task.accountName})` }}</div>
|
<div>{{ `${task.accountUsername}(${task.accountName})` }}</div>
|
||||||
<div v-if="task.completeTime">{{ `${dateFormat(task.completeTime)}` }}</div>
|
<div v-if="task.completeTime">{{ `${formatDate(task.completeTime)}` }}</div>
|
||||||
<div v-if="task.remark">{{ task.remark }}</div>
|
<div v-if="task.remark">{{ task.remark }}</div>
|
||||||
</template>
|
</template>
|
||||||
</el-step>
|
</el-step>
|
||||||
@@ -14,8 +14,7 @@
|
|||||||
import { toRefs, reactive, watch, onMounted } from 'vue';
|
import { toRefs, reactive, watch, onMounted } from 'vue';
|
||||||
import { accountApi } from '../../system/api';
|
import { accountApi } from '../../system/api';
|
||||||
import { ProcinstTaskStatus } from '../enums';
|
import { ProcinstTaskStatus } from '../enums';
|
||||||
import { dateFormat } from '@/common/utils/date';
|
import { formatDate } from '@/common/utils/format';
|
||||||
import { procdefApi } from '../api';
|
|
||||||
import { ElSteps, ElStep } from 'element-plus';
|
import { ElSteps, ElStep } from 'element-plus';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -23,8 +22,8 @@ const props = defineProps({
|
|||||||
tasks: {
|
tasks: {
|
||||||
type: [String, Object],
|
type: [String, Object],
|
||||||
},
|
},
|
||||||
procdefKey: {
|
procdef: {
|
||||||
type: String,
|
type: [Object],
|
||||||
},
|
},
|
||||||
// 流程实例任务列表
|
// 流程实例任务列表
|
||||||
procinstTasks: {
|
procinstTasks: {
|
||||||
@@ -54,7 +53,7 @@ watch(
|
|||||||
);
|
);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.procdefKey,
|
() => props.procdef,
|
||||||
async (newValue: any) => {
|
async (newValue: any) => {
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
parseTasksByKey(newValue);
|
parseTasksByKey(newValue);
|
||||||
@@ -63,15 +62,14 @@ watch(
|
|||||||
);
|
);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (props.procdefKey) {
|
if (props.procdef) {
|
||||||
parseTasksByKey(props.procdefKey);
|
parseTasksByKey(props.procdef);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
parseTasks(props.tasks);
|
parseTasks(props.tasks);
|
||||||
});
|
});
|
||||||
|
|
||||||
const parseTasksByKey = async (key: string) => {
|
const parseTasksByKey = async (procdef: any) => {
|
||||||
const procdef = await procdefApi.getByKey.request({ key });
|
|
||||||
parseTasks(procdef.tasks);
|
parseTasks(procdef.tasks);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,137 +1,541 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="home-container">
|
<div class="home-container personal">
|
||||||
<el-row :gutter="15">
|
<el-row :gutter="15">
|
||||||
<el-col :sm="6" class="mb15">
|
<!-- 个人信息 -->
|
||||||
<div @click="toPage({ id: 'personal' })" class="home-card-item home-card-first">
|
<el-col :xs="24" :sm="16">
|
||||||
<div class="flex-margin flex">
|
<el-card shadow="hover" header="个人信息">
|
||||||
<img :src="userInfo.photo" />
|
<div class="personal-user">
|
||||||
<div class="home-card-first-right ml15">
|
<div class="personal-user-left">
|
||||||
<div class="flex-margin">
|
<el-upload class="h100 personal-user-left-upload" action="" multiple :limit="1">
|
||||||
<div class="home-card-first-right-title">{{ `${currentTime}, ${userInfo.username}` }}</div>
|
<img :src="userInfo.photo" />
|
||||||
</div>
|
</el-upload>
|
||||||
|
</div>
|
||||||
|
<div class="personal-user-right">
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24" class="personal-title mb18"
|
||||||
|
>{{ currentTime }},{{ userInfo.name }},生活变的再糟糕,也不妨碍我变得更好!
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-row>
|
||||||
|
<el-col :xs="24" :sm="12" class="personal-item mb6">
|
||||||
|
<div class="personal-item-label">用户名:</div>
|
||||||
|
<div class="personal-item-value">{{ userInfo.username }}</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12" class="personal-item mb6">
|
||||||
|
<div class="personal-item-label">角色:</div>
|
||||||
|
<div class="personal-item-value">{{ roleInfo }}</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-row>
|
||||||
|
<el-col :xs="24" :sm="12" class="personal-item mb6">
|
||||||
|
<div class="personal-item-label">上次登录IP:</div>
|
||||||
|
<div class="personal-item-value">{{ userInfo.lastLoginIp }}</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12" class="personal-item mb6">
|
||||||
|
<div class="personal-item-label">上次登录时间:</div>
|
||||||
|
<div class="personal-item-value">{{ formatDate(userInfo.lastLoginTime) }}</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</el-card>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :sm="3" class="mb15" v-for="(v, k) in topCardItemList as any" :key="k">
|
|
||||||
<div @click="toPage(v)" class="home-card-item home-card-item-box" :style="{ background: v.color }">
|
<!-- 消息通知 -->
|
||||||
<div class="home-card-item-flex">
|
<el-col :xs="24" :sm="8" class="pl15 personal-info">
|
||||||
<div class="home-card-item-title pb3">{{ v.title }}</div>
|
<el-card shadow="hover">
|
||||||
<div class="home-card-item-title-num pb6" :id="v.id"></div>
|
<template #header>
|
||||||
|
<span>消息通知</span>
|
||||||
|
<span @click="showMsgs" class="personal-info-more">更多</span>
|
||||||
|
</template>
|
||||||
|
<div class="personal-info-box">
|
||||||
|
<ul class="personal-info-ul">
|
||||||
|
<li v-for="(v, k) in state.msgs as any" :key="k" class="personal-info-li">
|
||||||
|
<a class="personal-info-li-title">{{ `[${getMsgTypeDesc(v.type)}] ${v.msg}` }}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<i :class="v.icon" :style="{ color: v.iconColor }"></i>
|
</el-card>
|
||||||
</div>
|
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
|
<el-row :gutter="20" class="mt20 resource-info">
|
||||||
|
<el-col :sm="12">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<template #header>
|
||||||
|
<el-row justify="center">
|
||||||
|
<div class="resource-num pointer-icon" @click="toPage('machine')">
|
||||||
|
<SvgIcon
|
||||||
|
class="mb5 mr5"
|
||||||
|
:size="28"
|
||||||
|
:name="TagResourceTypeEnum.Machine.extra.icon"
|
||||||
|
:color="TagResourceTypeEnum.Machine.extra.iconColor"
|
||||||
|
/>
|
||||||
|
<span class="">{{ state.machine.num }}</span>
|
||||||
|
</div>
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
<el-row>
|
||||||
|
<el-col :sm="24">
|
||||||
|
<el-table :data="state.machine.opLogs" :height="state.resourceOpTableHeight" stripe size="small" empty-text="暂无操作记录">
|
||||||
|
<el-table-column prop="createTime" show-overflow-tooltip width="135">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ formatDate(scope.row.createTime) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="codePath" min-width="400" show-overflow-tooltip>
|
||||||
|
<template #default="scope">
|
||||||
|
<TagCodePath :path="scope.row.codePath" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column width="30">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-link @click="toPage('machine', scope.row.codePath)" type="primary" icon="Position"></el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :sm="12">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<template #header>
|
||||||
|
<el-row justify="center">
|
||||||
|
<div class="resource-num pointer-icon" @click="toPage('db')">
|
||||||
|
<SvgIcon class="mb5 mr5" :size="28" :name="TagResourceTypeEnum.Db.extra.icon" :color="TagResourceTypeEnum.Db.extra.iconColor" />
|
||||||
|
<span class="">{{ state.db.num }}</span>
|
||||||
|
</div>
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
<el-row>
|
||||||
|
<el-col :sm="24">
|
||||||
|
<el-table :data="state.db.opLogs" :height="state.resourceOpTableHeight" stripe size="small" empty-text="暂无操作记录">
|
||||||
|
<el-table-column prop="createTime" show-overflow-tooltip min-width="135">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ formatDate(scope.row.createTime) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="codePath" min-width="380" show-overflow-tooltip>
|
||||||
|
<template #default="scope">
|
||||||
|
<TagCodePath :path="scope.row.codePath" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column width="30">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-link @click="toPage('db', scope.row.codePath)" type="primary" icon="Position"></el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row :gutter="20" class="mt20 resource-info">
|
||||||
|
<el-col :sm="12">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<template #header>
|
||||||
|
<el-row justify="center">
|
||||||
|
<div class="resource-num pointer-icon" @click="toPage('redis')">
|
||||||
|
<SvgIcon
|
||||||
|
class="mb5 mr5"
|
||||||
|
:size="28"
|
||||||
|
:name="TagResourceTypeEnum.Redis.extra.icon"
|
||||||
|
:color="TagResourceTypeEnum.Redis.extra.iconColor"
|
||||||
|
/>
|
||||||
|
<span class="">{{ state.redis.num }}</span>
|
||||||
|
</div>
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
<el-row>
|
||||||
|
<el-col :sm="24">
|
||||||
|
<el-table :data="state.redis.opLogs" :height="state.resourceOpTableHeight" stripe size="small" empty-text="暂无操作记录">
|
||||||
|
<el-table-column prop="createTime" show-overflow-tooltip min-width="135">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ formatDate(scope.row.createTime) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="codePath" min-width="380" show-overflow-tooltip>
|
||||||
|
<template #default="scope">
|
||||||
|
<TagCodePath :path="scope.row.codePath" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column width="30">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-link @click="toPage('redis', scope.row.codePath)" type="primary" icon="Position"></el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :sm="12">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<template #header>
|
||||||
|
<el-row justify="center">
|
||||||
|
<div class="resource-num pointer-icon" @click="toPage('mongo')">
|
||||||
|
<SvgIcon
|
||||||
|
class="mb5 mr5"
|
||||||
|
:size="28"
|
||||||
|
:name="TagResourceTypeEnum.Mongo.extra.icon"
|
||||||
|
:color="TagResourceTypeEnum.Mongo.extra.iconColor"
|
||||||
|
/>
|
||||||
|
<span class="">{{ state.mongo.num }}</span>
|
||||||
|
</div>
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
<el-row>
|
||||||
|
<el-col :sm="24">
|
||||||
|
<el-table :data="state.mongo.opLogs" :height="state.resourceOpTableHeight" stripe size="small" empty-text="暂无操作记录">
|
||||||
|
<el-table-column prop="createTime" show-overflow-tooltip min-width="135">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ formatDate(scope.row.createTime) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="codePath" min-width="380" show-overflow-tooltip>
|
||||||
|
<template #default="scope">
|
||||||
|
<TagCodePath :path="scope.row.codePath" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column width="30">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-link @click="toPage('mongo', scope.row.codePath)" type="primary" icon="Position"></el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-dialog width="900px" title="消息" v-model="msgDialog.visible">
|
||||||
|
<el-table border :data="msgDialog.msgs.list" size="small">
|
||||||
|
<el-table-column property="type" label="类型" width="60">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ getMsgTypeDesc(scope.row.type) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column property="msg" label="消息"></el-table-column>
|
||||||
|
<el-table-column property="createTime" label="时间" width="150">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ formatDate(scope.row.createTime) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<el-row type="flex" class="mt5" justify="center">
|
||||||
|
<el-pagination
|
||||||
|
small
|
||||||
|
@current-change="searchMsg"
|
||||||
|
style="text-align: center"
|
||||||
|
background
|
||||||
|
layout="prev, pager, next, total, jumper"
|
||||||
|
:total="msgDialog.msgs.total"
|
||||||
|
v-model:current-page="msgDialog.query.pageNum"
|
||||||
|
:page-size="msgDialog.query.pageSize"
|
||||||
|
/>
|
||||||
|
</el-row>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { toRefs, reactive, onMounted, nextTick, computed } from 'vue';
|
import { toRefs, reactive, onMounted, computed } from 'vue';
|
||||||
// import * as echarts from 'echarts';
|
// import * as echarts from 'echarts';
|
||||||
import { CountUp } from 'countup.js';
|
|
||||||
import { formatAxis } from '@/common/utils/format';
|
import { formatAxis } from '@/common/utils/format';
|
||||||
import { indexApi } from './api';
|
import { indexApi } from './api';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useUserInfo } from '@/store/userInfo';
|
import { useUserInfo } from '@/store/userInfo';
|
||||||
|
import { personApi } from '../personal/api';
|
||||||
|
import { formatDate } from '@/common/utils/format';
|
||||||
|
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||||
|
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||||
|
import { resourceOpLogApi } from '../ops/tag/api';
|
||||||
|
import TagCodePath from '../ops/component/TagCodePath.vue';
|
||||||
|
import { useAutoOpenResource } from '@/store/autoOpenResource';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { userInfo } = storeToRefs(useUserInfo());
|
const { userInfo } = storeToRefs(useUserInfo());
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
topCardItemList: [
|
accountInfo: {
|
||||||
{
|
roles: [],
|
||||||
title: 'Linux机器',
|
},
|
||||||
id: 'machineNum',
|
msgs: [],
|
||||||
color: '#F95959',
|
msgDialog: {
|
||||||
|
visible: false,
|
||||||
|
query: {
|
||||||
|
pageSize: 10,
|
||||||
|
pageNum: 1,
|
||||||
},
|
},
|
||||||
{
|
msgs: {
|
||||||
title: '数据库',
|
list: [],
|
||||||
id: 'dbNum',
|
total: null,
|
||||||
color: '#8595F4',
|
|
||||||
},
|
},
|
||||||
{
|
},
|
||||||
title: 'redis',
|
resourceOpTableHeight: 180,
|
||||||
id: 'redisNum',
|
defaultLogSize: 5,
|
||||||
color: '#1abc9c',
|
machine: {
|
||||||
},
|
num: 0,
|
||||||
{
|
opLogs: [],
|
||||||
title: 'Mongo',
|
},
|
||||||
id: 'mongoNum',
|
db: {
|
||||||
color: '#FEBB50',
|
num: 0,
|
||||||
},
|
opLogs: [],
|
||||||
],
|
},
|
||||||
|
redis: {
|
||||||
|
num: 0,
|
||||||
|
opLogs: [],
|
||||||
|
},
|
||||||
|
mongo: {
|
||||||
|
num: 0,
|
||||||
|
opLogs: [],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { topCardItemList } = toRefs(state);
|
const { msgDialog } = toRefs(state);
|
||||||
|
|
||||||
|
const roleInfo = computed(() => {
|
||||||
|
if (state.accountInfo.roles.length == 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return state.accountInfo.roles.map((val: any) => val.roleName).join('、');
|
||||||
|
});
|
||||||
|
|
||||||
// 当前时间提示语
|
// 当前时间提示语
|
||||||
const currentTime = computed(() => {
|
const currentTime = computed(() => {
|
||||||
return formatAxis(new Date());
|
return formatAxis(new Date());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 页面加载时
|
||||||
|
onMounted(() => {
|
||||||
|
initData();
|
||||||
|
getAccountInfo();
|
||||||
|
|
||||||
|
getMsgs().then((res) => {
|
||||||
|
state.msgs = res.list;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const showMsgs = async () => {
|
||||||
|
state.msgDialog.query.pageNum = 1;
|
||||||
|
searchMsg();
|
||||||
|
state.msgDialog.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchMsg = async () => {
|
||||||
|
state.msgDialog.msgs = await getMsgs();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMsgTypeDesc = (type: number) => {
|
||||||
|
if (type == 1) {
|
||||||
|
return '登录';
|
||||||
|
}
|
||||||
|
if (type == 2) {
|
||||||
|
return '通知';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAccountInfo = async () => {
|
||||||
|
state.accountInfo = await personApi.accountInfo.request();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMsgs = async () => {
|
||||||
|
return await personApi.getMsgs.request(state.msgDialog.query);
|
||||||
|
};
|
||||||
|
|
||||||
// 初始化数字滚动
|
// 初始化数字滚动
|
||||||
const initNumCountUp = async () => {
|
const initData = async () => {
|
||||||
indexApi.machineDashbord.request().then((res: any) => {
|
resourceOpLogApi.getAccountResourceOpLogs
|
||||||
nextTick(() => {
|
.request({ resourceType: TagResourceTypeEnum.MachineAuthCert.value, pageSize: state.defaultLogSize })
|
||||||
new CountUp('machineNum', res.machineNum).start();
|
.then((res: any) => {
|
||||||
|
state.machine.opLogs = res.list;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
resourceOpLogApi.getAccountResourceOpLogs.request({ resourceType: TagResourceTypeEnum.DbName.value, pageSize: state.defaultLogSize }).then((res: any) => {
|
||||||
|
state.db.opLogs = res.list;
|
||||||
|
});
|
||||||
|
|
||||||
|
resourceOpLogApi.getAccountResourceOpLogs.request({ resourceType: TagResourceTypeEnum.Redis.value, pageSize: state.defaultLogSize }).then((res: any) => {
|
||||||
|
state.redis.opLogs = res.list;
|
||||||
|
});
|
||||||
|
|
||||||
|
resourceOpLogApi.getAccountResourceOpLogs.request({ resourceType: TagResourceTypeEnum.Mongo.value, pageSize: state.defaultLogSize }).then((res: any) => {
|
||||||
|
state.mongo.opLogs = res.list;
|
||||||
|
});
|
||||||
|
|
||||||
|
indexApi.machineDashbord.request().then((res: any) => {
|
||||||
|
state.machine.num = res.machineNum;
|
||||||
});
|
});
|
||||||
|
|
||||||
indexApi.dbDashbord.request().then((res: any) => {
|
indexApi.dbDashbord.request().then((res: any) => {
|
||||||
nextTick(() => {
|
state.db.num = res.dbNum;
|
||||||
new CountUp('dbNum', res.dbNum).start();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
indexApi.redisDashbord.request().then((res: any) => {
|
indexApi.redisDashbord.request().then((res: any) => {
|
||||||
nextTick(() => {
|
state.redis.num = res.redisNum;
|
||||||
new CountUp('redisNum', res.redisNum).start();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
indexApi.mongoDashbord.request().then((res: any) => {
|
indexApi.mongoDashbord.request().then((res: any) => {
|
||||||
nextTick(() => {
|
state.mongo.num = res.mongoNum;
|
||||||
new CountUp('mongoNum', res.mongoNum).start();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const toPage = (item: any) => {
|
const toPage = (item: any, codePath = '') => {
|
||||||
switch (item.id) {
|
let path;
|
||||||
|
switch (item) {
|
||||||
case 'personal': {
|
case 'personal': {
|
||||||
router.push('/personal');
|
router.push('/personal');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'mongoNum': {
|
case 'mongo': {
|
||||||
router.push('/mongo/mongo-data-operation');
|
useAutoOpenResource().setMongoCodePath(codePath);
|
||||||
|
path = '/mongo/mongo-data-operation';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'machineNum': {
|
case 'machine': {
|
||||||
router.push('/machine/machines-op');
|
useAutoOpenResource().setMachineCodePath(codePath);
|
||||||
|
path = '/machine/machines-op';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'dbNum': {
|
case 'db': {
|
||||||
router.push('/dbms/sql-exec');
|
useAutoOpenResource().setDbCodePath(codePath);
|
||||||
|
path = '/dbms/sql-exec';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'redisNum': {
|
case 'redis': {
|
||||||
router.push('/redis/data-operation');
|
useAutoOpenResource().setRedisCodePath(codePath);
|
||||||
|
path = '/redis/data-operation';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// 页面加载时
|
router.push({ path });
|
||||||
onMounted(() => {
|
};
|
||||||
initNumCountUp();
|
|
||||||
// initHomeLaboratory();
|
|
||||||
// initHomeOvertime();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
@import '@/theme/mixins/index.scss';
|
||||||
|
|
||||||
|
.personal {
|
||||||
|
.personal-user {
|
||||||
|
height: 130px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.personal-user-left {
|
||||||
|
width: 100px;
|
||||||
|
height: 130px;
|
||||||
|
border-radius: 3px;
|
||||||
|
|
||||||
|
::v-deep(.el-upload) {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.personal-user-left-upload {
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
img {
|
||||||
|
animation: logoAnimation 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.personal-user-right {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0 15px;
|
||||||
|
|
||||||
|
.personal-title {
|
||||||
|
font-size: 18px;
|
||||||
|
@include text-ellipsis(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.personal-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 13px;
|
||||||
|
|
||||||
|
.personal-item-label {
|
||||||
|
color: gray;
|
||||||
|
@include text-ellipsis(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.personal-item-value {
|
||||||
|
@include text-ellipsis(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.personal-info {
|
||||||
|
.personal-info-more {
|
||||||
|
float: right;
|
||||||
|
color: gray;
|
||||||
|
font-size: 13px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.personal-info-box {
|
||||||
|
height: 130px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.personal-info-ul {
|
||||||
|
list-style: none;
|
||||||
|
|
||||||
|
.personal-info-li {
|
||||||
|
font-size: 13px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
|
||||||
|
.personal-info-li-title {
|
||||||
|
display: inline-block;
|
||||||
|
@include text-ellipsis(1);
|
||||||
|
color: grey;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
& a:hover {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-info {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
::v-deep(.el-card__header) {
|
||||||
|
padding: 2px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-num {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 2vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.home-container {
|
.home-container {
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
|
||||||
@@ -182,7 +586,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.home-card-item-title-num {
|
.home-card-item-title-num {
|
||||||
font-size: 18px;
|
font-size: 2vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-card-item-tip-num {
|
.home-card-item-tip-num {
|
||||||
@@ -190,124 +594,5 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-card-first {
|
|
||||||
background: var(--bg-main-color);
|
|
||||||
border: 1px solid var(--el-border-color-light, #ebeef5);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 60px;
|
|
||||||
height: 60px;
|
|
||||||
border-radius: 100%;
|
|
||||||
border: 2px solid var(--el-color-primary-light-5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-card-first-right {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
.home-card-first-right-msg {
|
|
||||||
font-size: 13px;
|
|
||||||
color: gray;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-monitor {
|
|
||||||
height: 200px;
|
|
||||||
|
|
||||||
.flex-warp-item {
|
|
||||||
width: 50%;
|
|
||||||
height: 100px;
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
.flex-warp-item-box {
|
|
||||||
margin: auto;
|
|
||||||
height: auto;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-warning-card {
|
|
||||||
height: 292px;
|
|
||||||
|
|
||||||
::v-deep(.el-card) {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-dynamic {
|
|
||||||
height: 200px;
|
|
||||||
|
|
||||||
.home-dynamic-item {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
height: 60px;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
&:first-of-type {
|
|
||||||
.home-dynamic-item-line {
|
|
||||||
i {
|
|
||||||
color: orange !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-dynamic-item-left {
|
|
||||||
text-align: right;
|
|
||||||
|
|
||||||
.home-dynamic-item-left-time1 {
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-dynamic-item-left-time2 {
|
|
||||||
font-size: 13px;
|
|
||||||
color: gray;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-dynamic-item-line {
|
|
||||||
height: 60px;
|
|
||||||
border-right: 2px dashed #dfdfdf;
|
|
||||||
margin: 0 20px;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
i {
|
|
||||||
color: var(--el-color-primary);
|
|
||||||
font-size: 12px;
|
|
||||||
position: absolute;
|
|
||||||
top: 1px;
|
|
||||||
left: -6px;
|
|
||||||
transform: rotate(46deg);
|
|
||||||
background: white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-dynamic-item-right {
|
|
||||||
flex: 1;
|
|
||||||
|
|
||||||
.home-dynamic-item-right-title {
|
|
||||||
i {
|
|
||||||
margin-right: 5px;
|
|
||||||
border: 1px solid #dfdfdf;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
border-radius: 100%;
|
|
||||||
padding: 3px 2px 2px;
|
|
||||||
text-align: center;
|
|
||||||
color: var(--el-color-primary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-dynamic-item-right-label {
|
|
||||||
font-size: 13px;
|
|
||||||
color: gray;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ import { nextTick, onMounted, ref, toRefs, reactive, computed } from 'vue';
|
|||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
import { initRouter } from '@/router/index';
|
import { initRouter } from '@/router/index';
|
||||||
import { saveToken, saveUser } from '@/common/utils/storage';
|
import { getRefreshToken, saveRefreshToken, saveToken, saveUser } from '@/common/utils/storage';
|
||||||
import { formatAxis } from '@/common/utils/format';
|
import { formatAxis } from '@/common/utils/format';
|
||||||
import openApi from '@/common/openApi';
|
import openApi from '@/common/openApi';
|
||||||
import { RsaEncrypt } from '@/common/rsa';
|
import { RsaEncrypt } from '@/common/rsa';
|
||||||
@@ -279,19 +279,20 @@ const login = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const otpVerify = async () => {
|
const otpVerify = async () => {
|
||||||
otpFormRef.value.validate(async (valid: boolean) => {
|
try {
|
||||||
if (!valid) {
|
await otpFormRef.value.validate();
|
||||||
return false;
|
} catch (e: any) {
|
||||||
}
|
return false;
|
||||||
try {
|
}
|
||||||
state.loading.otpConfirm = true;
|
|
||||||
const accessToken = await openApi.otpVerify(state.otpDialog.form);
|
try {
|
||||||
await signInSuccess(accessToken);
|
state.loading.otpConfirm = true;
|
||||||
state.otpDialog.visible = false;
|
const res = await openApi.otpVerify(state.otpDialog.form);
|
||||||
} finally {
|
await signInSuccess(res.token, res.refresh_token);
|
||||||
state.loading.otpConfirm = false;
|
state.otpDialog.visible = false;
|
||||||
}
|
} finally {
|
||||||
});
|
state.loading.otpConfirm = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 登录
|
// 登录
|
||||||
@@ -327,22 +328,23 @@ const onSignIn = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateUserInfo = async () => {
|
const updateUserInfo = async () => {
|
||||||
baseInfoFormRef.value.validate(async (valid: boolean) => {
|
try {
|
||||||
if (!valid) {
|
await baseInfoFormRef.value.validate();
|
||||||
return false;
|
} catch (e: any) {
|
||||||
}
|
return false;
|
||||||
try {
|
}
|
||||||
state.loading.updateUserConfirm = true;
|
|
||||||
const form = state.baseInfoDialog.form;
|
try {
|
||||||
await personApi.updateAccount.request(state.baseInfoDialog.form);
|
state.loading.updateUserConfirm = true;
|
||||||
state.baseInfoDialog.visible = false;
|
const form = state.baseInfoDialog.form;
|
||||||
useUserInfo().userInfo.username = form.username;
|
await personApi.updateAccount.request(state.baseInfoDialog.form);
|
||||||
useUserInfo().userInfo.name = form.name;
|
state.baseInfoDialog.visible = false;
|
||||||
await toIndex();
|
useUserInfo().userInfo.username = form.username;
|
||||||
} finally {
|
useUserInfo().userInfo.name = form.name;
|
||||||
state.loading.updateUserConfirm = false;
|
await toIndex();
|
||||||
}
|
} finally {
|
||||||
});
|
state.loading.updateUserConfirm = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loginResDeal = (loginRes: any) => {
|
const loginResDeal = (loginRes: any) => {
|
||||||
@@ -366,7 +368,7 @@ const loginResDeal = (loginRes: any) => {
|
|||||||
const token = loginRes.token;
|
const token = loginRes.token;
|
||||||
// 如果不需要 otp校验,则该token即为accessToken,否则为otp校验token
|
// 如果不需要 otp校验,则该token即为accessToken,否则为otp校验token
|
||||||
if (loginRes.otp == -1) {
|
if (loginRes.otp == -1) {
|
||||||
signInSuccess(token);
|
signInSuccess(token, loginRes.refresh_token);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -379,12 +381,16 @@ const loginResDeal = (loginRes: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 登录成功后的跳转
|
// 登录成功后的跳转
|
||||||
const signInSuccess = async (accessToken: string = '') => {
|
const signInSuccess = async (accessToken: string = '', refreshToken = '') => {
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
accessToken = getToken();
|
accessToken = getToken();
|
||||||
}
|
}
|
||||||
|
if (!refreshToken) {
|
||||||
|
refreshToken = getRefreshToken();
|
||||||
|
}
|
||||||
// 存储 token 到浏览器缓存
|
// 存储 token 到浏览器缓存
|
||||||
saveToken(accessToken);
|
saveToken(accessToken);
|
||||||
|
saveRefreshToken(refreshToken);
|
||||||
|
|
||||||
// 初始化路由
|
// 初始化路由
|
||||||
await initRouter();
|
await initRouter();
|
||||||
@@ -415,26 +421,27 @@ const toIndex = async () => {
|
|||||||
}, 300);
|
}, 300);
|
||||||
};
|
};
|
||||||
|
|
||||||
const changePwd = () => {
|
const changePwd = async () => {
|
||||||
changePwdFormRef.value.validate(async (valid: boolean) => {
|
try {
|
||||||
if (!valid) {
|
await changePwdFormRef.value.validate();
|
||||||
return false;
|
} catch (e: any) {
|
||||||
}
|
return false;
|
||||||
try {
|
}
|
||||||
state.loading.changePwd = true;
|
|
||||||
const form = state.changePwdDialog.form;
|
try {
|
||||||
const changePwdReq: any = { ...form };
|
state.loading.changePwd = true;
|
||||||
changePwdReq.oldPassword = await RsaEncrypt(form.oldPassword);
|
const form = state.changePwdDialog.form;
|
||||||
changePwdReq.newPassword = await RsaEncrypt(form.newPassword);
|
const changePwdReq: any = { ...form };
|
||||||
await openApi.changePwd(changePwdReq);
|
changePwdReq.oldPassword = await RsaEncrypt(form.oldPassword);
|
||||||
ElMessage.success('密码修改成功, 新密码已填充至登录密码框');
|
changePwdReq.newPassword = await RsaEncrypt(form.newPassword);
|
||||||
state.loginForm.password = state.changePwdDialog.form.newPassword;
|
await openApi.changePwd(changePwdReq);
|
||||||
state.changePwdDialog.visible = false;
|
ElMessage.success('密码修改成功, 新密码已填充至登录密码框');
|
||||||
getCaptcha();
|
state.loginForm.password = state.changePwdDialog.form.newPassword;
|
||||||
} finally {
|
state.changePwdDialog.visible = false;
|
||||||
state.loading.changePwd = false;
|
getCaptcha();
|
||||||
}
|
} finally {
|
||||||
});
|
state.loading.changePwd = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const cancelChangePwd = () => {
|
const cancelChangePwd = () => {
|
||||||
|
|||||||
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 value-key="name" 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>
|
||||||
271
mayfly_go_web/src/views/ops/component/ResourceAuthCertEdit.vue
Normal file
271
mayfly_go_web/src/views/ops/component/ResourceAuthCertEdit.vue
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
<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-option
|
||||||
|
:key="TagResourceTypeEnum.Redis.value"
|
||||||
|
:label="TagResourceTypeEnum.Redis.label"
|
||||||
|
:value="TagResourceTypeEnum.Redis.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, computed, watch } 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'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
resourceCode: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请输入资源编号',
|
||||||
|
trigger: ['change', '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;
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(dialogVisible, (val: any) => {
|
||||||
|
if (val) {
|
||||||
|
setForm(props.authCert);
|
||||||
|
} else {
|
||||||
|
cancelEdit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
state.form = { ...DefaultForm };
|
||||||
|
acForm.value?.resetFields();
|
||||||
|
emit('cancel');
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
const btnOk = async () => {
|
||||||
|
acForm.value.validate(async (valid: boolean) => {
|
||||||
|
if (valid) {
|
||||||
|
emit('confirm', { ...state.form });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss"></style>
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
<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;
|
||||||
|
};
|
||||||
|
|
||||||
|
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,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="props.tags">
|
<div v-if="props.tags">
|
||||||
<el-row v-for="(tag, idx) in props.tags?.slice(0, 1)" :key="idx">
|
<el-row v-for="(tag, idx) in props.tags?.slice(0, 1)" :key="idx">
|
||||||
<TagInfo :tag-path="tag.tagPath" />
|
<TagInfo :tag-path="tag.codePath" />
|
||||||
<span class="ml3">{{ tag.tagPath }}</span>
|
<span class="ml3">{{ tag.codePath }}</span>
|
||||||
|
|
||||||
<!-- 展示剩余的标签信息 -->
|
<!-- 展示剩余的标签信息 -->
|
||||||
<el-popover :show-after="300" v-if="props.tags.length > 1 && idx == 0" placement="top-start" width="230" trigger="hover">
|
<el-popover :show-after="300" v-if="props.tags.length > 1 && idx == 0" placement="top-start" width="230" trigger="hover">
|
||||||
@@ -11,8 +11,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<el-row v-for="i in props.tags.slice(1)" :key="i">
|
<el-row v-for="i in props.tags.slice(1)" :key="i">
|
||||||
<TagInfo :tag-path="i.tagPath" />
|
<TagInfo :tag-path="i.codePath" />
|
||||||
<span class="ml3">{{ i.tagPath }}</span>
|
<span class="ml3">{{ i.codePath }}</span>
|
||||||
</el-row>
|
</el-row>
|
||||||
</el-popover>
|
</el-popover>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { toRefs, reactive, onMounted } from 'vue';
|
import { toRefs, reactive, onMounted } from 'vue';
|
||||||
import { machineApi } from '../machine/api';
|
import { machineApi } from '../machine/api';
|
||||||
|
import { MachineProtocolEnum } from '../machine/enums';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
@@ -46,7 +47,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, protocol: MachineProtocolEnum.Ssh.value });
|
||||||
state.sshTunnelMachineList = res.list;
|
state.sshTunnelMachineList = res.list;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
87
mayfly_go_web/src/views/ops/component/TagCodePath.vue
Normal file
87
mayfly_go_web/src/views/ops/component/TagCodePath.vue
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="paths">
|
||||||
|
<el-row v-for="(path, idx) in paths?.slice(0, 1)" :key="idx">
|
||||||
|
<span v-for="item in parseTagPath(path)" :key="item.code">
|
||||||
|
<SvgIcon
|
||||||
|
:name="EnumValue.getEnumByValue(TagResourceTypeEnum, item.type)?.extra.icon"
|
||||||
|
:color="EnumValue.getEnumByValue(TagResourceTypeEnum, item.type)?.extra.iconColor"
|
||||||
|
class="mr2"
|
||||||
|
/>
|
||||||
|
<span> {{ item.code }}</span>
|
||||||
|
<SvgIcon v-if="!item.isEnd" class="mr5 ml5" name="arrow-right" />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- 展示剩余的标签信息 -->
|
||||||
|
<el-popover :show-after="300" v-if="paths.length > 1 && idx == 0" placement="bottom" width="500" trigger="hover">
|
||||||
|
<template #reference>
|
||||||
|
<SvgIcon class="mt5 ml5" color="var(--el-color-primary)" name="MoreFilled" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-row v-for="i in paths.slice(1)" :key="i">
|
||||||
|
<span v-for="item in parseTagPath(i)" :key="item.code">
|
||||||
|
<SvgIcon
|
||||||
|
:name="EnumValue.getEnumByValue(TagResourceTypeEnum, item.type)?.extra.icon"
|
||||||
|
:color="EnumValue.getEnumByValue(TagResourceTypeEnum, item.type)?.extra.iconColor"
|
||||||
|
class="mr2"
|
||||||
|
/>
|
||||||
|
<span> {{ item.code }}</span>
|
||||||
|
<SvgIcon v-if="!item.isEnd" class="mr5 ml5" name="arrow-right" />
|
||||||
|
</span>
|
||||||
|
</el-row>
|
||||||
|
</el-popover>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||||
|
import EnumValue from '@/common/Enum';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
path: {
|
||||||
|
type: [String, Array<string>],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const paths = computed(() => {
|
||||||
|
if (Array.isArray(props.path)) {
|
||||||
|
return props.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [props.path];
|
||||||
|
});
|
||||||
|
|
||||||
|
const parseTagPath = (tagPath: string = '') => {
|
||||||
|
if (!tagPath) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const res = [] as any;
|
||||||
|
const codes = tagPath.split('/');
|
||||||
|
for (let code of codes) {
|
||||||
|
const typeAndCode = code.split('|');
|
||||||
|
|
||||||
|
if (typeAndCode.length == 1) {
|
||||||
|
const tagCode = typeAndCode[0];
|
||||||
|
if (!tagCode) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.push({
|
||||||
|
type: TagResourceTypeEnum.Tag.value,
|
||||||
|
code: typeAndCode[0],
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.push({
|
||||||
|
type: typeAndCode[0],
|
||||||
|
code: typeAndCode[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res[res.length - 1].isEnd = true;
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss"></style>
|
||||||
@@ -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,11 +10,12 @@
|
|||||||
: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"
|
||||||
|
:default-expanded-keys="props.defaultExpandedKeys"
|
||||||
>
|
>
|
||||||
<template #default="{ node, data }">
|
<template #default="{ node, data }">
|
||||||
<span @dblclick="treeNodeDblclick(data)" :class="data.type.nodeDblclickFunc ? 'none-select' : ''">
|
<span @dblclick="treeNodeDblclick(data)" :class="data.type.nodeDblclickFunc ? 'none-select' : ''">
|
||||||
@@ -34,7 +35,9 @@
|
|||||||
</slot>
|
</slot>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<slot :node="node" :data="data" name="suffix"></slot>
|
<span class="label-suffix">
|
||||||
|
<slot :node="node" :data="data" name="suffix"></slot>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</el-tree>
|
</el-tree>
|
||||||
@@ -56,6 +59,9 @@ const props = defineProps({
|
|||||||
type: [Number],
|
type: [Number],
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
defaultExpandedKeys: {
|
||||||
|
type: [Array],
|
||||||
|
},
|
||||||
tagPathNodeType: {
|
tagPathNodeType: {
|
||||||
type: [NodeType],
|
type: [NodeType],
|
||||||
required: true,
|
required: true,
|
||||||
@@ -140,8 +146,8 @@ 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.disabled && !data.type.nodeDblclickFunc && data.type.nodeClickFunc) {
|
||||||
|
emit('nodeClick', data);
|
||||||
data.type.nodeClickFunc(data);
|
data.type.nodeClickFunc(data);
|
||||||
}
|
}
|
||||||
// 关闭可能存在的右击菜单
|
// 关闭可能存在的右击菜单
|
||||||
@@ -199,18 +205,32 @@ const getNode = (nodeKey: any) => {
|
|||||||
return node;
|
return node;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setCurrentKey = (nodeKey: any) => {
|
||||||
|
treeRef.value.setCurrentKey(nodeKey);
|
||||||
|
};
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
reloadNode,
|
reloadNode,
|
||||||
|
getNode,
|
||||||
|
setCurrentKey,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<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;
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.label-suffix {
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
color: #c4c9c4;
|
||||||
|
font-size: 10px;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
167
mayfly_go_web/src/views/ops/component/TagTreeCheck.vue
Executable file
167
mayfly_go_web/src/views/ops/component/TagTreeCheck.vue
Executable file
@@ -0,0 +1,167 @@
|
|||||||
|
<template>
|
||||||
|
<div class="w100 tag-tree-check">
|
||||||
|
<el-input v-model="filterTag" @input="onFilterValChanged" clearable placeholder="输入关键字过滤" size="small" />
|
||||||
|
<div class="mt3" style="border: 1px solid var(--el-border-color)">
|
||||||
|
<el-scrollbar :style="{ height: props.height }">
|
||||||
|
<el-tree
|
||||||
|
v-bind="$attrs"
|
||||||
|
ref="tagTreeRef"
|
||||||
|
:data="state.tags"
|
||||||
|
:default-expanded-keys="checkedTags"
|
||||||
|
:default-checked-keys="checkedTags"
|
||||||
|
multiple
|
||||||
|
:render-after-expand="true"
|
||||||
|
show-checkbox
|
||||||
|
check-strictly
|
||||||
|
:node-key="$props.nodeKey"
|
||||||
|
:props="{
|
||||||
|
value: $props.nodeKey,
|
||||||
|
label: 'codePath',
|
||||||
|
children: 'children',
|
||||||
|
disabled: 'disabled',
|
||||||
|
}"
|
||||||
|
@check="tagTreeNodeCheck"
|
||||||
|
:filter-node-method="filterNode"
|
||||||
|
>
|
||||||
|
<template #default="{ data }">
|
||||||
|
<span>
|
||||||
|
<SvgIcon
|
||||||
|
:name="EnumValue.getEnumByValue(TagResourceTypeEnum, data.type)?.extra.icon"
|
||||||
|
:color="EnumValue.getEnumByValue(TagResourceTypeEnum, data.type)?.extra.iconColor"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span class="font13 ml5">
|
||||||
|
{{ data.code }}
|
||||||
|
<span style="color: #3c8dbc">【</span>
|
||||||
|
{{ data.name }}
|
||||||
|
<span style="color: #3c8dbc">】</span>
|
||||||
|
<el-tag v-if="data.children !== null" size="small">{{ data.children.length }} </el-tag>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-tree>
|
||||||
|
</el-scrollbar>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue';
|
||||||
|
import { tagApi } from '../tag/api';
|
||||||
|
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||||
|
import EnumValue from '@/common/Enum';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
height: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: 'calc(100vh - 330px)',
|
||||||
|
},
|
||||||
|
tagType: {
|
||||||
|
type: [Number, Array<Number>],
|
||||||
|
default: TagResourceTypeEnum.Tag.value,
|
||||||
|
},
|
||||||
|
nodeKey: {
|
||||||
|
type: String,
|
||||||
|
default: 'codePath',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const checkedTags = defineModel<Array<any>>('modelValue', {
|
||||||
|
default: () => [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const tagTreeRef: any = ref(null);
|
||||||
|
const filterTag = ref('');
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
tags: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
search();
|
||||||
|
});
|
||||||
|
|
||||||
|
const search = async () => {
|
||||||
|
let tagType: any = props.tagType;
|
||||||
|
if (Array.isArray(props.tagType)) {
|
||||||
|
tagType = props.tagType.join(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
state.tags = await tagApi.getTagTrees.request({ type: tagType });
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const checkedNodes = tagTreeRef.value.getCheckedNodes();
|
||||||
|
console.log('check nodes: ', checkedNodes);
|
||||||
|
// 禁用选中节点的所有父节点,不可选中
|
||||||
|
for (let checkNodeData of checkedNodes) {
|
||||||
|
disableParentNodes(tagTreeRef.value.getNode(checkNodeData.codePath).parent);
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterNode = (value: string, data: any) => {
|
||||||
|
if (!value) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return data.codePath.toLowerCase().includes(value) || data.name.includes(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFilterValChanged = (val: string) => {
|
||||||
|
tagTreeRef.value!.filter(val);
|
||||||
|
};
|
||||||
|
|
||||||
|
const tagTreeNodeCheck = (data: any) => {
|
||||||
|
const node = tagTreeRef.value.getNode(data.codePath);
|
||||||
|
console.log('check node: ', node);
|
||||||
|
|
||||||
|
if (node.checked) {
|
||||||
|
// 如果选中了子节点,则需要将父节点全部取消选中,并禁用父节点
|
||||||
|
unCheckParentNodes(node.parent);
|
||||||
|
disableParentNodes(node.parent);
|
||||||
|
} else {
|
||||||
|
// 如果取消了选中,则需要根据条件恢复父节点的选中状态
|
||||||
|
disableParentNodes(node.parent, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新绑定的值
|
||||||
|
checkedTags.value = tagTreeRef.value.getCheckedKeys(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const unCheckParentNodes = (node: any) => {
|
||||||
|
if (!node) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tagTreeRef.value.setChecked(node, false, false);
|
||||||
|
unCheckParentNodes(node.parent);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 禁用该节点以及所有父节点
|
||||||
|
* @param node 节点
|
||||||
|
* @param disable 是否禁用
|
||||||
|
*/
|
||||||
|
const disableParentNodes = (node: any, disable = true) => {
|
||||||
|
if (!node) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!disable) {
|
||||||
|
// 恢复为非禁用状态时,若同层级存在一个选中状态或者禁用状态,则继续禁用 不恢复非禁用状态。
|
||||||
|
for (let oneLevelNodes of node.childNodes) {
|
||||||
|
if (oneLevelNodes.checked || oneLevelNodes.data.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
node.data.disabled = disable;
|
||||||
|
disableParentNodes(node.parent, disable);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.tag-tree-check {
|
||||||
|
.el-tree {
|
||||||
|
min-width: 100%;
|
||||||
|
// 横向滚动生效
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -4,21 +4,20 @@
|
|||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
v-model="state.selectTags"
|
v-model="state.selectTags"
|
||||||
@change="changeTag"
|
@change="changeTag"
|
||||||
style="width: 100%"
|
|
||||||
:data="tags"
|
:data="tags"
|
||||||
placeholder="请选择关联标签"
|
placeholder="请选择关联标签"
|
||||||
:render-after-expand="true"
|
:default-expanded-keys="defaultExpandedKeys"
|
||||||
:default-expanded-keys="[state.selectTags]"
|
|
||||||
show-checkbox
|
show-checkbox
|
||||||
node-key="id"
|
node-key="codePath"
|
||||||
:props="{
|
:props="{
|
||||||
value: 'id',
|
value: 'codePath',
|
||||||
label: 'codePath',
|
label: 'codePath',
|
||||||
children: 'children',
|
children: 'children',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<template #default="{ data }">
|
<template #default="{ data }">
|
||||||
<span class="custom-tree-node">
|
<span class="custom-tree-node">
|
||||||
|
<SvgIcon :name="EnumValue.getEnumByValue(TagResourceTypeEnum, data.type)?.extra.icon" class="mr2" />
|
||||||
<span style="font-size: 13px">
|
<span style="font-size: 13px">
|
||||||
{{ data.code }}
|
{{ data.code }}
|
||||||
<span style="color: #3c8dbc">【</span>
|
<span style="color: #3c8dbc">【</span>
|
||||||
@@ -33,32 +32,45 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { toRefs, reactive, onMounted } from 'vue';
|
import { toRefs, reactive, onMounted, computed } from 'vue';
|
||||||
import { tagApi } from '../tag/api';
|
import { tagApi } from '../tag/api';
|
||||||
|
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||||
|
import EnumValue from '@/common/Enum';
|
||||||
|
|
||||||
//定义事件
|
//定义事件
|
||||||
const emit = defineEmits(['update:modelValue', 'changeTag', 'input']);
|
const emit = defineEmits(['update:modelValue', 'changeTag', 'input']);
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
selectTags: {
|
selectTags: {
|
||||||
type: [Array<any>],
|
type: [Array<any>, Object],
|
||||||
|
},
|
||||||
|
tagType: {
|
||||||
|
type: Number,
|
||||||
|
default: TagResourceTypeEnum.Tag.value,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
tags: [],
|
tags: [],
|
||||||
// 单选则为id,多选为id数组
|
// 单选则为codePath,多选为codePath数组
|
||||||
selectTags: [] as any,
|
selectTags: [] as any,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { tags } = toRefs(state);
|
const { tags } = toRefs(state);
|
||||||
|
|
||||||
onMounted(async () => {
|
const defaultExpandedKeys = computed(() => {
|
||||||
if (props.selectTags) {
|
if (Array.isArray(state.selectTags)) {
|
||||||
state.selectTags = props.selectTags;
|
// 如果 state.selectTags 是数组,直接返回
|
||||||
|
return state.selectTags;
|
||||||
}
|
}
|
||||||
|
|
||||||
state.tags = await tagApi.getTagTrees.request(null);
|
// 如果 state.selectTags 不是数组,转换为包含 state.selectTags 的数组
|
||||||
|
return [state.selectTags];
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
state.selectTags = props.selectTags;
|
||||||
|
state.tags = await tagApi.getTagTrees.request({ type: props.tagType });
|
||||||
});
|
});
|
||||||
|
|
||||||
const changeTag = () => {
|
const changeTag = () => {
|
||||||
|
|||||||
@@ -171,3 +171,44 @@ export function getTagPathSearchItem(resourceType: number) {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据标签路径获取对应的类型与编号数组
|
||||||
|
* @param codePath 编号路径 tag1/tag2/1|xxx/11|yyy/
|
||||||
|
* @returns {1: ['xxx'], 11: ['yyy']}
|
||||||
|
*/
|
||||||
|
export function getTagTypeCodeByPath(codePath: string) {
|
||||||
|
const result = {};
|
||||||
|
const parts = codePath.split('/'); // 切分字符串并保留数字和对应的值部分
|
||||||
|
|
||||||
|
for (let part of parts) {
|
||||||
|
if (!part) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let [key, value] = part.split('|'); // 分割数字和值部分
|
||||||
|
// 如果不存在第二个参数,则说明为标签类型
|
||||||
|
if (!value) {
|
||||||
|
value = key;
|
||||||
|
key = '-1';
|
||||||
|
}
|
||||||
|
if (!result[key]) {
|
||||||
|
result[key] = [];
|
||||||
|
}
|
||||||
|
result[key].push(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expandCodePath(codePath: string) {
|
||||||
|
const parts = codePath.split('/');
|
||||||
|
const result = [];
|
||||||
|
let currentPath = '';
|
||||||
|
|
||||||
|
for (let i = 0; i < parts.length - 1; i++) {
|
||||||
|
currentPath += parts[i] + '/';
|
||||||
|
result.push(currentPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,49 +10,44 @@
|
|||||||
width="38%"
|
width="38%"
|
||||||
>
|
>
|
||||||
<el-form :model="form" ref="dbForm" :rules="rules" label-width="auto">
|
<el-form :model="form" ref="dbForm" :rules="rules" label-width="auto">
|
||||||
<el-form-item ref="tagSelectRef" prop="tagId" label="标签" required>
|
<el-form-item prop="code" label="编号" required>
|
||||||
<tag-tree-select
|
<el-input
|
||||||
@change-tag="
|
:disabled="form.id"
|
||||||
(tagIds) => {
|
v-model.trim="form.code"
|
||||||
form.tagId = tagIds;
|
placeholder="请输入编号 (大小写字母、数字、_-.:), 不可修改"
|
||||||
tagSelectRef.validate();
|
auto-complete="off"
|
||||||
}
|
></el-input>
|
||||||
"
|
</el-form-item>
|
||||||
multiple
|
<el-form-item prop="name" label="名称" required>
|
||||||
:select-tags="form.tagId"
|
<el-input v-model.trim="form.name" placeholder="请输入数据库别名" auto-complete="off"></el-input>
|
||||||
style="width: 100%"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item prop="instanceId" label="数据库实例" required>
|
<el-form-item prop="authCertName" label="授权凭证" required>
|
||||||
<el-select
|
<el-select v-model="form.authCertName" placeholder="请选择授权凭证" filterable>
|
||||||
:disabled="form.id !== undefined"
|
<el-option v-for="item in state.authCerts" :key="item.id" :label="`${item.name}`" :value="item.name">
|
||||||
remote
|
|
||||||
:remote-method="getInstances"
|
|
||||||
@change="changeInstance"
|
|
||||||
v-model="form.instanceId"
|
|
||||||
placeholder="请输入实例名称搜索并选择实例"
|
|
||||||
filterable
|
|
||||||
clearable
|
|
||||||
class="w100"
|
|
||||||
>
|
|
||||||
<el-option v-for="item in state.instances" :key="item.id" :label="`${item.name}`" :value="item.id">
|
|
||||||
{{ item.name }}
|
{{ item.name }}
|
||||||
<el-divider direction="vertical" border-style="dashed" />
|
|
||||||
|
|
||||||
{{ item.type }} / {{ item.host }}:{{ item.port }}
|
|
||||||
<el-divider direction="vertical" border-style="dashed" />
|
<el-divider direction="vertical" border-style="dashed" />
|
||||||
{{ item.username }}
|
{{ 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-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item prop="name" label="别名" required>
|
<el-form-item prop="getDatabaseMode" label="获库方式" required>
|
||||||
<el-input v-model.trim="form.name" placeholder="请输入数据库别名" auto-complete="off"></el-input>
|
<el-select v-model="form.getDatabaseMode" @change="onChangeGetDatabaseMode" placeholder="请选择库名获取方式">
|
||||||
|
<el-option v-for="item in DbGetDbNamesMode" :key="item.value" :label="item.label" :value="item.value"> </el-option>
|
||||||
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item prop="database" label="数据库名">
|
<el-form-item prop="database" label="数据库名">
|
||||||
<el-select
|
<el-select
|
||||||
|
:disabled="form.getDatabaseMode == DbGetDbNamesMode.Auto.value || !form.authCertName"
|
||||||
v-model="dbNamesSelected"
|
v-model="dbNamesSelected"
|
||||||
multiple
|
multiple
|
||||||
clearable
|
clearable
|
||||||
@@ -61,8 +56,9 @@
|
|||||||
filterable
|
filterable
|
||||||
:filter-method="filterDbNames"
|
:filter-method="filterDbNames"
|
||||||
allow-create
|
allow-create
|
||||||
placeholder="请确保数据库实例信息填写完整后获取库名"
|
placeholder="获库方式为‘指定库名’时,可选择"
|
||||||
style="width: 100%"
|
@focus="getAllDatabase(form.authCertName)"
|
||||||
|
:loading="state.loadingDbNames"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<el-checkbox v-model="checkAllDbNames" :indeterminate="indeterminateDbNames" @change="handleCheckAll"> 全选 </el-checkbox>
|
<el-checkbox v-model="checkAllDbNames" :indeterminate="indeterminateDbNames" @change="handleCheckAll"> 全选 </el-checkbox>
|
||||||
@@ -74,14 +70,12 @@
|
|||||||
<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>
|
||||||
<div class="dialog-footer">
|
<div class="dialog-footer">
|
||||||
<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" @click="btnOk">确 定</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
@@ -92,15 +86,23 @@
|
|||||||
import { toRefs, reactive, watch, ref } from 'vue';
|
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 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 { DbType } from '@/views/ops/db/dialect';
|
||||||
|
import { ResourceCodePattern } from '@/common/pattern';
|
||||||
|
|
||||||
|
import EnumTag from '@/components/enumtag/EnumTag.vue';
|
||||||
|
import { AuthCertCiphertextTypeEnum } from '../tag/enums';
|
||||||
|
import { resourceAuthCertApi } from '../tag/api';
|
||||||
|
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||||
|
import { DbGetDbNamesMode } from './enums';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
visible: {
|
visible: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
},
|
},
|
||||||
|
instance: {
|
||||||
|
type: [Boolean, Object],
|
||||||
|
},
|
||||||
db: {
|
db: {
|
||||||
type: [Boolean, Object],
|
type: [Boolean, Object],
|
||||||
},
|
},
|
||||||
@@ -110,7 +112,7 @@ const props = defineProps({
|
|||||||
});
|
});
|
||||||
|
|
||||||
//定义事件
|
//定义事件
|
||||||
const emit = defineEmits(['update:visible', 'cancel', 'val-change']);
|
const emit = defineEmits(['update:visible', 'cancel', 'val-change', 'confirm']);
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
tagId: [
|
tagId: [
|
||||||
@@ -128,7 +130,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,
|
||||||
@@ -136,10 +149,17 @@ const rules = {
|
|||||||
trigger: ['change', 'blur'],
|
trigger: ['change', 'blur'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
database: [
|
authCertName: [
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: '请添加数据库',
|
message: '请选择授权凭证',
|
||||||
|
trigger: ['change', 'blur'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
getDatabaseMode: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请选择库名获取方式',
|
||||||
trigger: ['change', 'blur'],
|
trigger: ['change', 'blur'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -149,7 +169,7 @@ const checkAllDbNames = ref(false);
|
|||||||
const indeterminateDbNames = ref(false);
|
const indeterminateDbNames = ref(false);
|
||||||
|
|
||||||
const dbForm: any = ref(null);
|
const dbForm: any = ref(null);
|
||||||
const tagSelectRef: any = ref(null);
|
// const tagSelectRef: any = ref(null);
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
dialogVisible: false,
|
dialogVisible: false,
|
||||||
@@ -157,47 +177,67 @@ 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: [],
|
|
||||||
name: null,
|
name: null,
|
||||||
code: '',
|
code: '',
|
||||||
|
getDatabaseMode: DbGetDbNamesMode.Auto.value,
|
||||||
database: '',
|
database: '',
|
||||||
remark: '',
|
remark: '',
|
||||||
instanceId: null as any,
|
instanceId: null as any,
|
||||||
flowProcdefKey: '',
|
authCertName: '',
|
||||||
},
|
},
|
||||||
instances: [] as any,
|
instances: [] as any,
|
||||||
|
loadingDbNames: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { dialogVisible, allDatabases, form, dbNamesSelected } = toRefs(state);
|
const { dialogVisible, allDatabases, form, dbNamesSelected } = toRefs(state);
|
||||||
|
|
||||||
const { isFetching: saveBtnLoading, execute: saveDbExec } = dbApi.saveDb.useApi(form);
|
watch(
|
||||||
|
() => props.visible,
|
||||||
watch(props, async (newValue: any) => {
|
() => {
|
||||||
state.dialogVisible = newValue.visible;
|
state.dialogVisible = props.visible;
|
||||||
if (!state.dialogVisible) {
|
if (!state.dialogVisible) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
const db: any = props.db;
|
||||||
|
if (db.code) {
|
||||||
|
state.form = { ...db };
|
||||||
|
if (db.getDatabaseMode == DbGetDbNamesMode.Assign.value) {
|
||||||
|
// 将数据库名使用空格切割,获取所有数据库列表
|
||||||
|
state.dbNamesSelected = db.database.split(' ');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
state.form = { getDatabaseMode: DbGetDbNamesMode.Auto.value } as any;
|
||||||
|
state.dbNamesSelected = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (newValue.db) {
|
);
|
||||||
state.form = { ...newValue.db };
|
|
||||||
state.form.tagId = newValue.db.tags.map((t: any) => t.tagId);
|
const onChangeGetDatabaseMode = (val: any) => {
|
||||||
// 将数据库名使用空格切割,获取所有数据库列表
|
if (val == DbGetDbNamesMode.Auto.value) {
|
||||||
state.dbNamesSelected = newValue.db.database.split(' ');
|
|
||||||
} else {
|
|
||||||
state.form = {} as any;
|
|
||||||
state.dbNamesSelected = [];
|
state.dbNamesSelected = [];
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
const changeInstance = () => {
|
|
||||||
state.dbNamesSelected = [];
|
|
||||||
getAllDatabase();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAllDatabase = async () => {
|
const getAuthCerts = async () => {
|
||||||
if (state.form.instanceId > 0) {
|
const inst: any = props.instance;
|
||||||
let dbs = await dbApi.getAllDatabase.request({ instanceId: state.form.instanceId });
|
const res = await resourceAuthCertApi.listByQuery.request({
|
||||||
|
resourceCode: inst.code,
|
||||||
|
resourceType: TagResourceTypeEnum.Db.value,
|
||||||
|
pageSize: 100,
|
||||||
|
});
|
||||||
|
state.authCerts = res.list || [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAllDatabase = async (authCertName: string) => {
|
||||||
|
try {
|
||||||
|
state.loadingDbNames = true;
|
||||||
|
const req = { ...(props.instance as any) };
|
||||||
|
req.authCert = state.authCerts?.find((x: any) => x.name == authCertName);
|
||||||
|
let dbs = await dbApi.getAllDatabase.request(req);
|
||||||
state.allDatabases = dbs;
|
state.allDatabases = dbs;
|
||||||
|
|
||||||
// 如果是oracle,且没查出数据库列表,则取实例sid
|
// 如果是oracle,且没查出数据库列表,则取实例sid
|
||||||
@@ -205,40 +245,27 @@ const getAllDatabase = async () => {
|
|||||||
if (instance && instance.type === DbType.oracle && dbs.length === 0) {
|
if (instance && instance.type === DbType.oracle && dbs.length === 0) {
|
||||||
state.allDatabases = [instance.sid];
|
state.allDatabases = [instance.sid];
|
||||||
}
|
}
|
||||||
}
|
} finally {
|
||||||
};
|
state.loadingDbNames = false;
|
||||||
|
|
||||||
const getInstances = async (instanceName: string = '', id = 0) => {
|
|
||||||
if (!id && !instanceName) {
|
|
||||||
state.instances = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const data = await dbApi.instances.request({ id, name: instanceName });
|
|
||||||
if (data) {
|
|
||||||
state.instances = data.list;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const open = async () => {
|
const open = async () => {
|
||||||
if (state.form.instanceId) {
|
await getAuthCerts();
|
||||||
// 根据id获取,因为需要回显实例名称
|
if (state.form.authCertName) {
|
||||||
await getInstances('', state.form.instanceId);
|
await getAllDatabase(state.form.authCertName);
|
||||||
}
|
}
|
||||||
await getAllDatabase();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const btnOk = async () => {
|
const btnOk = async () => {
|
||||||
dbForm.value.validate(async (valid: boolean) => {
|
try {
|
||||||
if (!valid) {
|
await dbForm.value.validate();
|
||||||
ElMessage.error('请正确填写信息');
|
} catch (e: any) {
|
||||||
return false;
|
ElMessage.error('请正确填写信息');
|
||||||
}
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
await saveDbExec();
|
emit('confirm', state.form);
|
||||||
ElMessage.success('保存成功');
|
|
||||||
emit('val-change', state.form);
|
|
||||||
cancel();
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetInputDb = () => {
|
const resetInputDb = () => {
|
||||||
|
|||||||
@@ -6,9 +6,8 @@
|
|||||||
:before-query-fn="checkRouteTagPath"
|
:before-query-fn="checkRouteTagPath"
|
||||||
:search-items="searchItems"
|
:search-items="searchItems"
|
||||||
v-model:query-form="query"
|
v-model:query-form="query"
|
||||||
:show-selection="true"
|
|
||||||
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>
|
||||||
@@ -23,11 +22,6 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #tableHeader>
|
|
||||||
<el-button v-auth="perms.saveDb" type="primary" icon="plus" @click="editDb(false)">添加</el-button>
|
|
||||||
<el-button v-auth="perms.delDb" :disabled="selectionData.length < 1" @click="deleteDb()" type="danger" icon="delete">删除</el-button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #type="{ data }">
|
<template #type="{ data }">
|
||||||
<el-tooltip :content="data.type" placement="top">
|
<el-tooltip :content="data.type" placement="top">
|
||||||
<SvgIcon :name="getDbDialect(data.type).getInfo().icon" :size="20" />
|
<SvgIcon :name="getDbDialect(data.type).getInfo().icon" :size="20" />
|
||||||
@@ -38,16 +32,26 @@
|
|||||||
{{ `${data.host}:${data.port}` }}
|
{{ `${data.host}:${data.port}` }}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template #database="{ data }">
|
||||||
|
<el-popover placement="bottom" :width="200" trigger="click">
|
||||||
|
<template #reference>
|
||||||
|
<el-button @click="getDbNames(data)" type="primary" link>查看库</el-button>
|
||||||
|
</template>
|
||||||
|
<el-table :data="filterDbs" v-loading="state.loadingDbNames" size="small">
|
||||||
|
<el-table-column prop="dbName" label="数据库">
|
||||||
|
<template #header>
|
||||||
|
<el-input v-model="state.dbNameSearch" size="small" placeholder="库名: 输入可过滤" clearable />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-popover>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template #tagPath="{ data }">
|
<template #tagPath="{ data }">
|
||||||
<ResourceTags :tags="data.tags" />
|
<ResourceTags :tags="data.tags" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #action="{ data }">
|
<template #action="{ data }">
|
||||||
<span v-if="actionBtns[perms.saveDb]">
|
|
||||||
<el-button type="primary" @click="editDb(data)" link>编辑</el-button>
|
|
||||||
<el-divider direction="vertical" border-style="dashed" />
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<el-button type="primary" @click="onShowSqlExec(data)" link>SQL记录</el-button>
|
<el-button type="primary" @click="onShowSqlExec(data)" link>SQL记录</el-button>
|
||||||
<el-divider direction="vertical" border-style="dashed" />
|
<el-divider direction="vertical" border-style="dashed" />
|
||||||
|
|
||||||
@@ -61,7 +65,7 @@
|
|||||||
<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: 'backupDb', data }" v-if="actionBtns[perms.backupDb] && supportAction('backupDb', data.type)">
|
<el-dropdown-item :command="{ type: 'backupDb', data }" v-if="actionBtns[perms.backupDb] && supportAction('backupDb', data.type)">
|
||||||
备份任务
|
备份任务
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
@@ -88,16 +92,16 @@
|
|||||||
<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>
|
||||||
@@ -175,7 +179,7 @@
|
|||||||
<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.authCertName }}</el-descriptions-item>
|
||||||
<el-descriptions-item :span="1" label="类型">
|
<el-descriptions-item :span="1" label="类型">
|
||||||
<SvgIcon :name="getDbDialect(infoDialog.instance?.type).getInfo().icon" :size="20" />{{ infoDialog.instance?.type }}
|
<SvgIcon :name="getDbDialect(infoDialog.instance?.type).getInfo().icon" :size="20" />{{ infoDialog.instance?.type }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
@@ -183,28 +187,25 @@
|
|||||||
<el-descriptions-item :span="3" label="数据库">{{ infoDialog.data?.database }}</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="备注">{{ 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="创建时间">{{ formatDate(infoDialog.data?.createTime) }} </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="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="2" label="更新时间">{{ formatDate(infoDialog.data?.updateTime) }} </el-descriptions-item>
|
||||||
<el-descriptions-item :span="1" label="修改者">{{ infoDialog.data?.modifier }}</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>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, toRefs, reactive, onMounted, defineAsyncComponent, Ref } from 'vue';
|
import { computed, defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
|
||||||
import { dbApi } from './api';
|
import { dbApi } from './api';
|
||||||
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 { dateFormat } from '@/common/utils/date';
|
import { formatDate } from '@/common/utils/format';
|
||||||
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';
|
||||||
@@ -219,18 +220,17 @@ import DbBackupList from './DbBackupList.vue';
|
|||||||
import DbBackupHistoryList from './DbBackupHistoryList.vue';
|
import DbBackupHistoryList from './DbBackupHistoryList.vue';
|
||||||
import DbRestoreList from './DbRestoreList.vue';
|
import DbRestoreList from './DbRestoreList.vue';
|
||||||
import ResourceTags from '../component/ResourceTags.vue';
|
import ResourceTags from '../component/ResourceTags.vue';
|
||||||
|
import { sleep } from '@/common/utils/loading';
|
||||||
|
import { DbGetDbNamesMode } from './enums';
|
||||||
|
import { DbInst } from './db';
|
||||||
|
|
||||||
const DbEdit = defineAsyncComponent(() => import('./DbEdit.vue'));
|
const DbEdit = defineAsyncComponent(() => import('./DbEdit.vue'));
|
||||||
|
|
||||||
const perms = {
|
const searchItems = [
|
||||||
base: 'db',
|
getTagPathSearchItem(TagResourceTypeEnum.DbName.value),
|
||||||
saveDb: 'db:save',
|
SearchItem.slot('instanceId', '实例', 'instanceSelect'),
|
||||||
delDb: 'db:del',
|
SearchItem.input('code', '编号'),
|
||||||
backupDb: 'db:backup',
|
];
|
||||||
restoreDb: 'db:restore',
|
|
||||||
};
|
|
||||||
|
|
||||||
const searchItems = [getTagPathSearchItem(TagResourceTypeEnum.Db.value), SearchItem.slot('instanceId', '实例', 'instanceSelect')];
|
|
||||||
|
|
||||||
const columns = ref([
|
const columns = ref([
|
||||||
TableColumn.new('tags[0].tagPath', '关联标签').isSlot('tagPath').setAddWidth(20),
|
TableColumn.new('tags[0].tagPath', '关联标签').isSlot('tagPath').setAddWidth(20),
|
||||||
@@ -238,15 +238,22 @@ const columns = ref([
|
|||||||
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('authCertName', '授权凭证'),
|
||||||
TableColumn.new('flowProcdefKey', '关联流程'),
|
TableColumn.new('getDatabaseMode', '获库方式').typeTag(DbGetDbNamesMode),
|
||||||
|
TableColumn.new('database', '库').isSlot().setMinWidth(80),
|
||||||
TableColumn.new('remark', '备注'),
|
TableColumn.new('remark', '备注'),
|
||||||
|
TableColumn.new('code', '编号'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const perms = {
|
||||||
|
backupDb: 'db:backup',
|
||||||
|
restoreDb: 'db:restore',
|
||||||
|
};
|
||||||
|
|
||||||
// 该用户拥有的的操作列按钮权限
|
// 该用户拥有的的操作列按钮权限
|
||||||
// const actionBtns = hasPerms([perms.base, perms.saveDb]);
|
// const actionBtns = hasPerms([perms.base, perms.saveDb]);
|
||||||
const actionBtns = hasPerms(Object.values(perms));
|
const actionBtns = hasPerms(Object.values(perms));
|
||||||
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight().alignCenter();
|
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(180).fixedRight().alignCenter();
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const pageTableRef: Ref<any> = ref(null);
|
const pageTableRef: Ref<any> = ref(null);
|
||||||
@@ -254,6 +261,9 @@ const state = reactive({
|
|||||||
row: {} as any,
|
row: {} as any,
|
||||||
dbId: 0,
|
dbId: 0,
|
||||||
db: '',
|
db: '',
|
||||||
|
loadingDbNames: false,
|
||||||
|
currentDbNames: [],
|
||||||
|
dbNameSearch: '',
|
||||||
instances: [] as any,
|
instances: [] as any,
|
||||||
/**
|
/**
|
||||||
* 选中的数据
|
* 选中的数据
|
||||||
@@ -329,13 +339,37 @@ const state = reactive({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { db, selectionData, query, infoDialog, sqlExecLogDialog, exportDialog, dbEditDialog, dbBackupDialog, dbBackupHistoryDialog, dbRestoreDialog } =
|
const { db, query, infoDialog, sqlExecLogDialog, exportDialog, dbEditDialog, dbBackupDialog, dbBackupHistoryDialog, dbRestoreDialog } = toRefs(state);
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
search();
|
||||||
|
});
|
||||||
|
|
||||||
|
const getDbNames = async (db: any) => {
|
||||||
|
try {
|
||||||
|
state.loadingDbNames = true;
|
||||||
|
state.currentDbNames = await DbInst.getDbNames(db);
|
||||||
|
} finally {
|
||||||
|
state.loadingDbNames = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterDbs = computed(() => {
|
||||||
|
const dbNames = state.currentDbNames;
|
||||||
|
if (!dbNames) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const dbNameObjs = dbNames.map((x) => {
|
||||||
|
return {
|
||||||
|
dbName: x,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return dbNameObjs.filter((db: any) => {
|
||||||
|
return db.dbName.includes(state.dbNameSearch);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const checkRouteTagPath = (query: any) => {
|
const checkRouteTagPath = (query: any) => {
|
||||||
@@ -345,7 +379,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();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -382,10 +419,6 @@ const handleMoreActionCommand = (commond: any) => {
|
|||||||
showInfo(data);
|
showInfo(data);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case 'edit': {
|
|
||||||
editDb(data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
case 'dumpDb': {
|
case 'dumpDb': {
|
||||||
onDumpDbs(data);
|
onDumpDbs(data);
|
||||||
return;
|
return;
|
||||||
@@ -405,32 +438,6 @@ const handleMoreActionCommand = (commond: any) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const editDb = async (data: any) => {
|
|
||||||
if (!data) {
|
|
||||||
state.dbEditDialog.data = null;
|
|
||||||
state.dbEditDialog.title = '新增数据库资源';
|
|
||||||
} else {
|
|
||||||
state.dbEditDialog.data = data;
|
|
||||||
state.dbEditDialog.title = '修改数据库资源';
|
|
||||||
}
|
|
||||||
state.dbEditDialog.visible = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteDb = async () => {
|
|
||||||
try {
|
|
||||||
await ElMessageBox.confirm(`确定删除【${state.selectionData.map((x: any) => x.name).join(', ')}】库?`, '提示', {
|
|
||||||
confirmButtonText: '确定',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning',
|
|
||||||
});
|
|
||||||
await dbApi.deleteDb.request({ id: state.selectionData.map((x: any) => x.id).join(',') });
|
|
||||||
ElMessage.success('删除成功');
|
|
||||||
search();
|
|
||||||
} catch (err) {
|
|
||||||
//
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onShowSqlExec = async (row: any) => {
|
const onShowSqlExec = async (row: any) => {
|
||||||
state.sqlExecLogDialog.title = `${row.name}`;
|
state.sqlExecLogDialog.title = `${row.name}`;
|
||||||
state.sqlExecLogDialog.dbId = row.id;
|
state.sqlExecLogDialog.dbId = row.id;
|
||||||
@@ -485,9 +492,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 == '结构') {
|
||||||
@@ -496,13 +502,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;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -515,6 +523,8 @@ const supportAction = (action: string, dbType: string): boolean => {
|
|||||||
}
|
}
|
||||||
return actions.includes(action);
|
return actions.includes(action);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
defineExpose({ search });
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.db-list {
|
.db-list {
|
||||||
|
|||||||
@@ -45,14 +45,14 @@
|
|||||||
<el-descriptions :column="1" border>
|
<el-descriptions :column="1" border>
|
||||||
<el-descriptions-item :span="1" label="数据库名称">{{ infoDialog.data.dbName }}</el-descriptions-item>
|
<el-descriptions-item :span="1" label="数据库名称">{{ infoDialog.data.dbName }}</el-descriptions-item>
|
||||||
<el-descriptions-item v-if="infoDialog.data.pointInTime" :span="1" label="恢复时间点">{{
|
<el-descriptions-item v-if="infoDialog.data.pointInTime" :span="1" label="恢复时间点">{{
|
||||||
dateFormat(infoDialog.data.pointInTime)
|
formatDate(infoDialog.data.pointInTime)
|
||||||
}}</el-descriptions-item>
|
}}</el-descriptions-item>
|
||||||
<el-descriptions-item v-if="!infoDialog.data.pointInTime" :span="1" label="数据库备份">{{
|
<el-descriptions-item v-if="!infoDialog.data.pointInTime" :span="1" label="数据库备份">{{
|
||||||
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="开始时间">{{ formatDate(infoDialog.data.startTime) }}</el-descriptions-item>
|
||||||
<el-descriptions-item :span="1" label="是否启用">{{ infoDialog.data.enabledDesc }}</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="执行时间">{{ formatDate(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>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
@@ -66,7 +66,7 @@ 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, ElMessageBox } from 'element-plus';
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
import { dateFormat } from '@/common/utils/date';
|
import { formatDate } from '@/common/utils/format';
|
||||||
const DbRestoreEdit = defineAsyncComponent(() => import('./DbRestoreEdit.vue'));
|
const DbRestoreEdit = defineAsyncComponent(() => import('./DbRestoreEdit.vue'));
|
||||||
const pageTableRef: Ref<any> = ref(null);
|
const pageTableRef: Ref<any> = ref(null);
|
||||||
|
|
||||||
|
|||||||
332
mayfly_go_web/src/views/ops/db/DbTransferEdit.vue
Normal file
332
mayfly_go_web/src/views/ops/db/DbTransferEdit.vue
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
<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>
|
||||||
190
mayfly_go_web/src/views/ops/db/InstanceDbConf.vue
Normal file
190
mayfly_go_web/src/views/ops/db/InstanceDbConf.vue
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-drawer :title="title" v-model="dialogVisible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="50%">
|
||||||
|
<template #header>
|
||||||
|
<DrawerHeader :header="title" :back="cancel" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-table :data="state.dbs" stripe>
|
||||||
|
<el-table-column prop="name" label="名称" show-overflow-tooltip min-width="100"> </el-table-column>
|
||||||
|
<el-table-column prop="authCertName" label="授权凭证" min-width="120" show-overflow-tooltip> </el-table-column>
|
||||||
|
<el-table-column prop="getDatabaseMode" label="获库方式" min-width="80">
|
||||||
|
<template #default="scope">
|
||||||
|
<EnumTag :enums="DbGetDbNamesMode" :value="scope.row.getDatabaseMode" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="database" label="库" min-width="80">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-popover placement="bottom" :width="200" trigger="click">
|
||||||
|
<template #reference>
|
||||||
|
<el-button @click="getDbNames(scope.row)" type="primary" link>查看库</el-button>
|
||||||
|
</template>
|
||||||
|
<el-table :data="filterDbs" size="small" v-loading="state.loadingDbNames">
|
||||||
|
<el-table-column prop="dbName" label="数据库">
|
||||||
|
<template #header>
|
||||||
|
<el-input v-model="state.dbNameSearch" size="small" placeholder="库名: 输入可过滤" clearable />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-popover>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="remark" label="备注" show-overflow-tooltip min-width="120"> </el-table-column>
|
||||||
|
<el-table-column prop="code" label="编号" show-overflow-tooltip min-width="120"> </el-table-column>
|
||||||
|
<el-table-column min-wdith="120px">
|
||||||
|
<template #header>
|
||||||
|
操作
|
||||||
|
<el-button v-auth="perms.saveDb" type="primary" circle size="small" icon="Plus" @click="editDb(null)"> </el-button>
|
||||||
|
</template>
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button v-auth="perms.saveDb" @click="editDb(scope.row)" type="primary" icon="edit" link></el-button>
|
||||||
|
<el-button class="ml1" v-auth="perms.delDb" type="danger" @click="deleteDb(scope.row)" icon="delete" link></el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<db-edit
|
||||||
|
@confirm="confirmEditDb"
|
||||||
|
@cancel="cancelEditDb"
|
||||||
|
:title="dbEditDialog.title"
|
||||||
|
v-model:visible="dbEditDialog.visible"
|
||||||
|
:instance="props.instance"
|
||||||
|
v-model:db="dbEditDialog.data"
|
||||||
|
></db-edit>
|
||||||
|
</el-drawer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, reactive, toRefs, watchEffect } from 'vue';
|
||||||
|
import { dbApi } from './api';
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||||
|
import DbEdit from './DbEdit.vue';
|
||||||
|
import EnumTag from '@/components/enumtag/EnumTag.vue';
|
||||||
|
import { DbGetDbNamesMode } from './enums';
|
||||||
|
import { DbInst } from './db';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
instance: {
|
||||||
|
type: [Object],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const perms = {
|
||||||
|
base: 'db',
|
||||||
|
saveDb: 'db:save',
|
||||||
|
delDb: 'db:del',
|
||||||
|
};
|
||||||
|
|
||||||
|
//定义事件
|
||||||
|
const emit = defineEmits(['update:visible', 'cancel', 'val-change']);
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
dialogVisible: false,
|
||||||
|
dbs: [] as any,
|
||||||
|
loadingDbNames: false,
|
||||||
|
currentDbNames: [], // 当前数据库名
|
||||||
|
dbNameSearch: '',
|
||||||
|
dbEditDialog: {
|
||||||
|
visible: false,
|
||||||
|
data: null as any,
|
||||||
|
title: '新增数据库',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { dialogVisible, dbEditDialog } = toRefs(state);
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
state.dialogVisible = props.visible;
|
||||||
|
if (!state.dialogVisible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDbs();
|
||||||
|
});
|
||||||
|
|
||||||
|
const getDbNames = async (db: any) => {
|
||||||
|
try {
|
||||||
|
state.loadingDbNames = true;
|
||||||
|
state.currentDbNames = await DbInst.getDbNames(db);
|
||||||
|
} finally {
|
||||||
|
state.loadingDbNames = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterDbs = computed(() => {
|
||||||
|
const dbNames = state.currentDbNames;
|
||||||
|
if (!dbNames) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const dbNameObjs = dbNames.map((x) => {
|
||||||
|
return {
|
||||||
|
dbName: x,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return dbNameObjs.filter((db: any) => {
|
||||||
|
return db.dbName.includes(state.dbNameSearch);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
emit('update:visible', false);
|
||||||
|
emit('cancel');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDbs = () => {
|
||||||
|
dbApi.dbs.request({ pageSize: 200, instanceId: props.instance.id }).then((res: any) => {
|
||||||
|
state.dbs = res.list || [];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const editDb = (data: any) => {
|
||||||
|
if (data) {
|
||||||
|
state.dbEditDialog.data = { ...data };
|
||||||
|
} else {
|
||||||
|
state.dbEditDialog.data = {
|
||||||
|
instanceId: props.instance.id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
state.dbEditDialog.title = data ? '编辑数据库' : '新增数据库';
|
||||||
|
state.dbEditDialog.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteDb = async (db: any) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确定删除【${db.name}】库?`, '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
});
|
||||||
|
await dbApi.deleteDb.request({ id: db.id });
|
||||||
|
ElMessage.success('删除成功');
|
||||||
|
getDbs();
|
||||||
|
} catch (err) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmEditDb = async (db: any) => {
|
||||||
|
db.instanceId = props.instance.id;
|
||||||
|
await dbApi.saveDb.request(db);
|
||||||
|
ElMessage.success('保存成功');
|
||||||
|
getDbs();
|
||||||
|
cancelEditDb();
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelEditDb = () => {
|
||||||
|
state.dbEditDialog.visible = false;
|
||||||
|
state.dbEditDialog.data = {};
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss"></style>
|
||||||
@@ -1,111 +1,142 @@
|
|||||||
<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="name" label="别名" required>
|
|
||||||
<el-input v-model.trim="form.name" placeholder="请输入数据库别名" auto-complete="off"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item prop="type" label="类型" required>
|
|
||||||
<el-select @change="changeDbType" style="width: 100%" v-model="form.type" placeholder="请选择数据库类型">
|
|
||||||
<el-option
|
|
||||||
v-for="(dbTypeAndDialect, key) in getDbDialectMap()"
|
|
||||||
:key="key"
|
|
||||||
:value="dbTypeAndDialect[0]"
|
|
||||||
:label="dbTypeAndDialect[1].getInfo().name"
|
|
||||||
>
|
|
||||||
<SvgIcon :name="dbTypeAndDialect[1].getInfo().icon" :size="20" />
|
|
||||||
{{ dbTypeAndDialect[1].getInfo().name }}
|
|
||||||
</el-option>
|
|
||||||
|
|
||||||
<template #prefix>
|
<el-form-item ref="tagSelectRef" prop="tagCodePaths" label="标签">
|
||||||
<SvgIcon :name="getDbDialect(form.type).getInfo().icon" :size="20" />
|
<tag-tree-select
|
||||||
</template>
|
multiple
|
||||||
</el-select>
|
@change-tag="
|
||||||
</el-form-item>
|
(paths: any) => {
|
||||||
<el-form-item v-if="form.type !== DbType.sqlite" prop="host" label="host" required>
|
form.tagCodePaths = paths;
|
||||||
<el-col :span="18">
|
tagSelectRef.validate();
|
||||||
<el-input :disabled="form.id !== undefined" v-model.trim="form.host" placeholder="请输入主机ip" auto-complete="off"></el-input>
|
}
|
||||||
</el-col>
|
"
|
||||||
<el-col style="text-align: center" :span="1">:</el-col>
|
:select-tags="form.tagCodePaths"
|
||||||
<el-col :span="5">
|
style="width: 100%"
|
||||||
<el-input type="number" v-model.number="form.port" placeholder="端口"></el-input>
|
/>
|
||||||
</el-col>
|
</el-form-item>
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item v-if="form.type === DbType.sqlite" prop="host" label="sqlite地址">
|
<el-form-item prop="code" label="编号" required>
|
||||||
<el-input v-model.trim="form.host" placeholder="请输入sqlite文件在服务器的绝对地址"></el-input>
|
<el-input
|
||||||
</el-form-item>
|
:disabled="form.id"
|
||||||
|
v-model.trim="form.code"
|
||||||
|
placeholder="请输入编号 (大小写字母、数字、_-.:), 不可修改"
|
||||||
|
auto-complete="off"
|
||||||
|
></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item v-if="form.type === DbType.oracle" prop="sid" label="SID">
|
<el-form-item prop="name" label="名称" required>
|
||||||
<el-input v-model.trim="form.sid" placeholder="请输入服务id"></el-input>
|
<el-input v-model.trim="form.name" placeholder="请输入数据库别名" auto-complete="off"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<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>
|
|
||||||
</el-popover>
|
|
||||||
</template>
|
|
||||||
</el-input>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item prop="remark" label="备注">
|
<el-form-item prop="type" label="类型" required>
|
||||||
<el-input v-model="form.remark" auto-complete="off" type="textarea"></el-input>
|
<el-select @change="changeDbType" style="width: 100%" v-model="form.type" placeholder="请选择数据库类型">
|
||||||
</el-form-item>
|
<el-option
|
||||||
</el-tab-pane>
|
v-for="(dbTypeAndDialect, key) in getDbDialectMap()"
|
||||||
|
:key="key"
|
||||||
|
:value="dbTypeAndDialect[0]"
|
||||||
|
:label="dbTypeAndDialect[1].getInfo().name"
|
||||||
|
>
|
||||||
|
<SvgIcon :name="dbTypeAndDialect[1].getInfo().icon" :size="20" />
|
||||||
|
{{ dbTypeAndDialect[1].getInfo().name }}
|
||||||
|
</el-option>
|
||||||
|
|
||||||
<el-tab-pane label="其他配置" name="other">
|
<template #prefix>
|
||||||
<el-form-item prop="params" label="连接参数">
|
<SvgIcon :name="getDbDialect(form.type).getInfo().icon" :size="20" />
|
||||||
<el-input v-model.trim="form.params" placeholder="其他连接参数,形如: key1=value1&key2=value2">
|
</template>
|
||||||
<!-- <template #suffix>
|
</el-select>
|
||||||
<el-link
|
</el-form-item>
|
||||||
target="_blank"
|
|
||||||
href="https://github.com/go-sql-driver/mysql#parameters"
|
|
||||||
:underline="false"
|
|
||||||
type="primary"
|
|
||||||
class="mr5"
|
|
||||||
>参数参考</el-link
|
|
||||||
>
|
|
||||||
</template> -->
|
|
||||||
</el-input>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item prop="sshTunnelMachineId" label="SSH隧道">
|
<el-form-item v-if="form.type !== DbType.sqlite" prop="host" label="host" required>
|
||||||
<ssh-tunnel-select v-model="form.sshTunnelMachineId" />
|
<el-col :span="18">
|
||||||
</el-form-item>
|
<el-input v-model.trim="form.host" placeholder="请输入ip" auto-complete="off"></el-input>
|
||||||
</el-tab-pane>
|
</el-col>
|
||||||
</el-tabs>
|
<el-col style="text-align: center" :span="1">:</el-col>
|
||||||
|
<el-col :span="5">
|
||||||
|
<el-input type="number" v-model.number="form.port" placeholder="端口"></el-input>
|
||||||
|
</el-col>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item v-if="form.type === DbType.sqlite" prop="host" label="sqlite地址">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<el-divider content-position="left">其他</el-divider>
|
||||||
|
<el-form-item prop="params" label="连接参数">
|
||||||
|
<el-input v-model.trim="form.params" placeholder="其他连接参数,形如: key1=value1&key2=value2"> </el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item prop="sshTunnelMachineId" label="SSH隧道">
|
||||||
|
<ssh-tunnel-select v-model="form.sshTunnelMachineId" />
|
||||||
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="dialog-footer">
|
<el-button @click="cancel()">取 消</el-button>
|
||||||
<el-button @click="testConn" :loading="testConnBtnLoading" type="success">测试连接</el-button>
|
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk">确 定</el-button>
|
||||||
<el-button @click="cancel()">取 消</el-button>
|
|
||||||
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk">确 定</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-drawer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { reactive, ref, toRefs, watch } from 'vue';
|
import { reactive, ref, toRefs, watchEffect } 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, getDbDialectMap } 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';
|
||||||
|
import TagTreeSelect from '../component/TagTreeSelect.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
visible: {
|
visible: {
|
||||||
@@ -123,6 +154,25 @@ const props = defineProps({
|
|||||||
const emit = defineEmits(['update:visible', 'cancel', 'val-change']);
|
const emit = defineEmits(['update:visible', 'cancel', 'val-change']);
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
|
tagCodePaths: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请选择标签',
|
||||||
|
trigger: ['change'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
code: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请输入编码',
|
||||||
|
trigger: ['change', 'blur'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: ResourceCodePattern.pattern,
|
||||||
|
message: ResourceCodePattern.message,
|
||||||
|
trigger: ['blur'],
|
||||||
|
},
|
||||||
|
],
|
||||||
name: [
|
name: [
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
@@ -144,13 +194,6 @@ const rules = {
|
|||||||
trigger: ['blur'],
|
trigger: ['blur'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
username: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: '请输入用户名',
|
|
||||||
trigger: ['change', 'blur'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
sid: [
|
sid: [
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
@@ -161,108 +204,109 @@ const rules = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const dbForm: any = ref(null);
|
const dbForm: any = ref(null);
|
||||||
|
const tagSelectRef: any = ref(null);
|
||||||
|
|
||||||
|
const DefaultForm = {
|
||||||
|
id: null,
|
||||||
|
type: DbType.mysql,
|
||||||
|
code: '',
|
||||||
|
name: null,
|
||||||
|
host: '',
|
||||||
|
port: getDbDialect(DbType.mysql).getInfo().defaultPort,
|
||||||
|
extra: '', // 连接需要的额外参数(json字符串)
|
||||||
|
params: null,
|
||||||
|
remark: '',
|
||||||
|
sshTunnelMachineId: null as any,
|
||||||
|
authCerts: [],
|
||||||
|
tagCodePaths: [],
|
||||||
|
};
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
dialogVisible: false,
|
dialogVisible: false,
|
||||||
tabActiveName: 'basic',
|
extra: {} as any, // 连接需要的额外参数(json)
|
||||||
form: {
|
form: DefaultForm,
|
||||||
id: null,
|
submitForm: {} as any,
|
||||||
type: '',
|
|
||||||
name: null,
|
|
||||||
host: '',
|
|
||||||
port: null,
|
|
||||||
username: null,
|
|
||||||
sid: null, // oracle类项目需要服务id
|
|
||||||
password: null,
|
|
||||||
params: null,
|
|
||||||
remark: '',
|
|
||||||
sshTunnelMachineId: null as any,
|
|
||||||
},
|
|
||||||
submitForm: {},
|
|
||||||
// 原密码
|
|
||||||
pwd: '',
|
|
||||||
// 原用户名
|
|
||||||
oldUserName: null,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { dialogVisible, tabActiveName, form, submitForm, pwd } = toRefs(state);
|
const { dialogVisible, form, submitForm } = toRefs(state);
|
||||||
|
|
||||||
const { isFetching: saveBtnLoading, execute: saveInstanceExec } = dbApi.saveInstance.useApi(submitForm);
|
const { isFetching: saveBtnLoading, execute: saveInstanceExec, data: saveInstanceRes } = dbApi.saveInstance.useApi(submitForm);
|
||||||
const { isFetching: testConnBtnLoading, execute: testConnExec } = dbApi.testConn.useApi(submitForm);
|
const { isFetching: testConnBtnLoading, execute: testConnExec } = dbApi.testConn.useApi(submitForm);
|
||||||
|
|
||||||
watch(props, (newValue: any) => {
|
watchEffect(() => {
|
||||||
state.dialogVisible = newValue.visible;
|
state.dialogVisible = props.visible;
|
||||||
if (!state.dialogVisible) {
|
if (!state.dialogVisible) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
state.tabActiveName = 'basic';
|
const dbInst: any = props.data;
|
||||||
if (newValue.data) {
|
if (dbInst) {
|
||||||
state.form = { ...newValue.data };
|
state.form = { ...dbInst };
|
||||||
state.oldUserName = state.form.username;
|
state.form.tagCodePaths = dbInst.tags.map((t: any) => t.codePath) || [];
|
||||||
|
try {
|
||||||
|
state.extra = JSON.parse(state.form.extra);
|
||||||
|
} catch (e) {
|
||||||
|
state.extra = {};
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
state.form = { port: null, type: DbType.mysql } as any;
|
state.form = { ...DefaultForm };
|
||||||
state.oldUserName = null;
|
state.form.authCerts = [];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const changeDbType = (val: string) => {
|
|
||||||
if (!state.form.id) {
|
|
||||||
state.form.port = getDbDialect(val).getInfo().defaultPort as any;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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: any = { ...state.form };
|
||||||
reqForm.password = await RsaEncrypt(reqForm.password);
|
reqForm.selectAuthCert = null;
|
||||||
|
reqForm.tags = null;
|
||||||
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) => {
|
try {
|
||||||
if (!valid) {
|
await dbForm.value.validate();
|
||||||
ElMessage.error('请正确填写信息');
|
} catch (e: any) {
|
||||||
return false;
|
ElMessage.error('请正确填写信息');
|
||||||
}
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
state.submitForm = await getReqForm();
|
state.submitForm = await getReqForm();
|
||||||
await testConnExec();
|
state.submitForm.authCerts = [authCert];
|
||||||
ElMessage.success('连接成功');
|
await testConnExec();
|
||||||
});
|
ElMessage.success('连接成功');
|
||||||
};
|
};
|
||||||
|
|
||||||
const btnOk = async () => {
|
const btnOk = async () => {
|
||||||
if (state.form.type !== DbType.sqlite) {
|
try {
|
||||||
if (!state.form.id) {
|
await dbForm.value.validate();
|
||||||
notBlank(state.form.password, '新增操作,密码不可为空');
|
} catch (e: any) {
|
||||||
} else if (state.form.username != state.oldUserName) {
|
ElMessage.error('请正确填写信息');
|
||||||
notBlank(state.form.password, '已修改用户名,请输入密码');
|
return false;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dbForm.value.validate(async (valid: boolean) => {
|
state.submitForm = await getReqForm();
|
||||||
if (!valid) {
|
await saveInstanceExec();
|
||||||
ElMessage.error('请正确填写信息');
|
ElMessage.success('保存成功');
|
||||||
return false;
|
state.form.id = saveInstanceRes as any;
|
||||||
}
|
emit('val-change', state.form);
|
||||||
|
cancel();
|
||||||
state.submitForm = await getReqForm();
|
|
||||||
await saveInstanceExec();
|
|
||||||
ElMessage.success('保存成功');
|
|
||||||
emit('val-change', state.form);
|
|
||||||
cancel();
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const cancel = () => {
|
const cancel = () => {
|
||||||
emit('update:visible', false);
|
emit('update:visible', false);
|
||||||
emit('cancel');
|
emit('cancel');
|
||||||
|
state.extra = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeDbType = (val: string) => {
|
||||||
|
if (!state.form.id) {
|
||||||
|
state.form.port = getDbDialect(val).getInfo().defaultPort as any;
|
||||||
|
}
|
||||||
|
state.extra = {};
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss"></style>
|
<style lang="scss"></style>
|
||||||
|
|||||||
@@ -3,11 +3,13 @@
|
|||||||
<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"
|
||||||
v-model:selection-data="state.selectionData"
|
v-model:selection-data="state.selectionData"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
|
lazy
|
||||||
>
|
>
|
||||||
<template #tableHeader>
|
<template #tableHeader>
|
||||||
<el-button v-auth="perms.saveInstance" type="primary" icon="plus" @click="editInstance(false)">添加</el-button>
|
<el-button v-auth="perms.saveInstance" type="primary" icon="plus" @click="editInstance(false)">添加</el-button>
|
||||||
@@ -16,6 +18,14 @@
|
|||||||
>
|
>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template #tagPath="{ data }">
|
||||||
|
<ResourceTags :tags="data.tags" />
|
||||||
|
</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="getDbDialect(data.type).getInfo().name" 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" />
|
||||||
@@ -25,6 +35,7 @@
|
|||||||
<template #action="{ data }">
|
<template #action="{ data }">
|
||||||
<el-button @click="showInfo(data)" link>详情</el-button>
|
<el-button @click="showInfo(data)" link>详情</el-button>
|
||||||
<el-button v-if="actionBtns[perms.saveInstance]" @click="editInstance(data)" type="primary" link>编辑</el-button>
|
<el-button v-if="actionBtns[perms.saveInstance]" @click="editInstance(data)" type="primary" link>编辑</el-button>
|
||||||
|
<el-button v-if="actionBtns[perms.saveDb]" @click="editDb(data)" type="primary" link>库配置</el-button>
|
||||||
</template>
|
</template>
|
||||||
</page-table>
|
</page-table>
|
||||||
|
|
||||||
@@ -35,7 +46,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>
|
||||||
@@ -43,20 +53,22 @@
|
|||||||
|
|
||||||
<el-descriptions-item :span="3" label="SSH隧道">{{ infoDialog.data.sshTunnelMachineId > 0 ? '是' : '否' }} </el-descriptions-item>
|
<el-descriptions-item :span="3" label="SSH隧道">{{ infoDialog.data.sshTunnelMachineId > 0 ? '是' : '否' }} </el-descriptions-item>
|
||||||
|
|
||||||
<el-descriptions-item :span="2" label="创建时间">{{ dateFormat(infoDialog.data.createTime) }} </el-descriptions-item>
|
<el-descriptions-item :span="2" label="创建时间">{{ formatDate(infoDialog.data.createTime) }} </el-descriptions-item>
|
||||||
<el-descriptions-item :span="1" label="创建者">{{ infoDialog.data.creator }}</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="2" label="更新时间">{{ formatDate(infoDialog.data.updateTime) }} </el-descriptions-item>
|
||||||
<el-descriptions-item :span="1" label="修改者">{{ infoDialog.data.modifier }}</el-descriptions-item>
|
<el-descriptions-item :span="1" label="修改者">{{ infoDialog.data.modifier }}</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<instance-edit
|
<instance-edit
|
||||||
@val-change="search"
|
@val-change="search()"
|
||||||
:title="instanceEditDialog.title"
|
:title="instanceEditDialog.title"
|
||||||
v-model:visible="instanceEditDialog.visible"
|
v-model:visible="instanceEditDialog.visible"
|
||||||
v-model:data="instanceEditDialog.data"
|
v-model:data="instanceEditDialog.data"
|
||||||
></instance-edit>
|
></instance-edit>
|
||||||
|
|
||||||
|
<instance-db-conf :title="dbEditDialog.title" v-model:visible="dbEditDialog.visible" :instance="dbEditDialog.instance" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -64,35 +76,50 @@
|
|||||||
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } 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 { formatDate } from '@/common/utils/format';
|
||||||
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';
|
||||||
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';
|
||||||
|
import ResourceTags from '../component/ResourceTags.vue';
|
||||||
|
import { getTagPathSearchItem } from '../component/tag';
|
||||||
|
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||||
|
|
||||||
const InstanceEdit = defineAsyncComponent(() => import('./InstanceEdit.vue'));
|
const InstanceEdit = defineAsyncComponent(() => import('./InstanceEdit.vue'));
|
||||||
|
const InstanceDbConf = defineAsyncComponent(() => import('./InstanceDbConf.vue'));
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
lazy: {
|
||||||
|
type: [Boolean],
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const perms = {
|
const perms = {
|
||||||
saveInstance: 'db:instance:save',
|
saveInstance: 'db:instance:save',
|
||||||
delInstance: 'db:instance:del',
|
delInstance: 'db:instance:del',
|
||||||
|
saveDb: 'db:save',
|
||||||
};
|
};
|
||||||
|
|
||||||
const searchItems = [SearchItem.input('name', '名称')];
|
const searchItems = [getTagPathSearchItem(TagResourceTypeEnum.Db.value), SearchItem.input('code', '编号'), SearchItem.input('name', '名称')];
|
||||||
|
|
||||||
const columns = ref([
|
const columns = ref([
|
||||||
|
TableColumn.new('tags[0].tagPath', '关联标签').isSlot('tagPath').setAddWidth(20),
|
||||||
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', '备注'),
|
||||||
|
TableColumn.new('code', '编号'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 该用户拥有的的操作列按钮权限
|
// 该用户拥有的的操作列按钮权限
|
||||||
const actionBtns = hasPerms(Object.values(perms));
|
const actionBtns = hasPerms(Object.values(perms));
|
||||||
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(110).fixedRight().alignCenter();
|
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(180).fixedRight().alignCenter();
|
||||||
const pageTableRef: Ref<any> = ref(null);
|
const pageTableRef: Ref<any> = ref(null);
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
@@ -108,6 +135,7 @@ const state = reactive({
|
|||||||
*/
|
*/
|
||||||
query: {
|
query: {
|
||||||
name: null,
|
name: null,
|
||||||
|
tagPath: '',
|
||||||
pageNum: 1,
|
pageNum: 1,
|
||||||
pageSize: 0,
|
pageSize: 0,
|
||||||
},
|
},
|
||||||
@@ -120,20 +148,40 @@ const state = reactive({
|
|||||||
data: null as any,
|
data: null as any,
|
||||||
title: '新增数据库实例',
|
title: '新增数据库实例',
|
||||||
},
|
},
|
||||||
|
dbEditDialog: {
|
||||||
|
visible: false,
|
||||||
|
instance: null as any,
|
||||||
|
title: '新增数据库实例',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { selectionData, query, infoDialog, instanceEditDialog } = toRefs(state);
|
const { selectionData, query, infoDialog, instanceEditDialog, dbEditDialog } = 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 search = () => {
|
const search = (tagPath: string = '') => {
|
||||||
|
if (tagPath) {
|
||||||
|
state.query.tagPath = tagPath;
|
||||||
|
}
|
||||||
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;
|
||||||
@@ -164,5 +212,13 @@ const deleteInstance = async () => {
|
|||||||
//
|
//
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const editDb = (data: any) => {
|
||||||
|
state.dbEditDialog.instance = data;
|
||||||
|
state.dbEditDialog.title = `配置 "${data.name}" 数据库`;
|
||||||
|
state.dbEditDialog.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({ search });
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss"></style>
|
<style lang="scss"></style>
|
||||||
|
|||||||
@@ -2,7 +2,12 @@
|
|||||||
<div class="db-sql-exec">
|
<div class="db-sql-exec">
|
||||||
<Splitpanes class="default-theme">
|
<Splitpanes class="default-theme">
|
||||||
<Pane size="20" max-size="30">
|
<Pane size="20" max-size="30">
|
||||||
<tag-tree :resource-type="TagResourceTypeEnum.Db.value" :tag-path-node-type="NodeTypeTagPath" ref="tagTreeRef">
|
<tag-tree
|
||||||
|
:default-expanded-keys="state.defaultExpendKey"
|
||||||
|
:resource-type="TagResourceTypeEnum.DbName.value"
|
||||||
|
:tag-path-node-type="NodeTypeTagPath"
|
||||||
|
ref="tagTreeRef"
|
||||||
|
>
|
||||||
<template #prefix="{ data }">
|
<template #prefix="{ data }">
|
||||||
<span v-if="data.type.value == SqlExecNodeType.DbInst">
|
<span v-if="data.type.value == SqlExecNodeType.DbInst">
|
||||||
<el-popover
|
<el-popover
|
||||||
@@ -27,8 +32,8 @@
|
|||||||
<el-descriptions-item label="数据库版本">
|
<el-descriptions-item label="数据库版本">
|
||||||
<span v-loading="loadingServerInfo"> {{ `${dbServerInfo?.version}` }}</span>
|
<span v-loading="loadingServerInfo"> {{ `${dbServerInfo?.version}` }}</span>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="user">
|
<el-descriptions-item label="授权凭证">
|
||||||
{{ data.params.username }}
|
{{ data.params.authCertName }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="备注">
|
<el-descriptions-item label="备注">
|
||||||
{{ data.params.remark }}
|
{{ data.params.remark }}
|
||||||
@@ -42,10 +47,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #suffix="{ data }">
|
<template #suffix="{ data }">
|
||||||
<span class="db-table-size" v-if="data.type.value == SqlExecNodeType.Table && data.params.size">{{ ` ${data.params.size}` }}</span>
|
<span v-if="data.type.value == SqlExecNodeType.Table && data.params.size">{{ ` ${data.params.size}` }}</span>
|
||||||
<span class="db-table-size" v-if="data.type.value == SqlExecNodeType.TableMenu && data.params.dbTableSize">{{
|
<span v-if="data.type.value == SqlExecNodeType.TableMenu && data.params.dbTableSize">{{ ` ${data.params.dbTableSize}` }}</span>
|
||||||
` ${data.params.dbTableSize}`
|
|
||||||
}}</span>
|
|
||||||
</template>
|
</template>
|
||||||
</tag-tree>
|
</tag-tree>
|
||||||
</Pane>
|
</Pane>
|
||||||
@@ -143,7 +146,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"
|
:flow-procdef="dt.params.flowProcdef"
|
||||||
:height="state.tablesOpHeight"
|
:height="state.tablesOpHeight"
|
||||||
/>
|
/>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
@@ -158,7 +161,7 @@
|
|||||||
:dbId="tableCreateDialog.dbId"
|
:dbId="tableCreateDialog.dbId"
|
||||||
:db="tableCreateDialog.db"
|
:db="tableCreateDialog.db"
|
||||||
:dbType="tableCreateDialog.dbType"
|
:dbType="tableCreateDialog.dbType"
|
||||||
:flow-procdef-key="tableCreateDialog.flowProcdefKey"
|
:flow-procdef="tableCreateDialog.flowProcdef"
|
||||||
:data="tableCreateDialog.data"
|
:data="tableCreateDialog.data"
|
||||||
v-model:visible="tableCreateDialog.visible"
|
v-model:visible="tableCreateDialog.visible"
|
||||||
@submit-sql="onSubmitEditTableSql"
|
@submit-sql="onSubmitEditTableSql"
|
||||||
@@ -167,11 +170,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { defineAsyncComponent, h, onBeforeUnmount, onMounted, reactive, ref, toRefs } from 'vue';
|
import { defineAsyncComponent, h, onBeforeUnmount, onMounted, reactive, ref, toRefs, watch } from 'vue';
|
||||||
import { ElCheckbox, 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, getTagTypeCodeByPath } from '../component/tag';
|
||||||
import TagTree from '../component/TagTree.vue';
|
import TagTree from '../component/TagTree.vue';
|
||||||
import { dbApi } from './api';
|
import { dbApi } from './api';
|
||||||
import { dispposeCompletionItemProvider } from '@/components/monaco/completionItemProvider';
|
import { dispposeCompletionItemProvider } from '@/components/monaco/completionItemProvider';
|
||||||
@@ -183,6 +186,9 @@ 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';
|
import SqlExecBox from '@/views/ops/db/component/sqleditor/SqlExecBox';
|
||||||
|
import { useAutoOpenResource } from '@/store/autoOpenResource';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { procdefApi } from '@/views/flow/api';
|
||||||
|
|
||||||
const DbTableOp = defineAsyncComponent(() => import('./component/table/DbTableOp.vue'));
|
const DbTableOp = defineAsyncComponent(() => import('./component/table/DbTableOp.vue'));
|
||||||
const DbSqlEditor = defineAsyncComponent(() => import('./component/sqleditor/DbSqlEditor.vue'));
|
const DbSqlEditor = defineAsyncComponent(() => import('./component/sqleditor/DbSqlEditor.vue'));
|
||||||
@@ -236,7 +242,7 @@ const nodeClickChangeDb = (nodeData: TagTreeNode) => {
|
|||||||
type: params.type,
|
type: params.type,
|
||||||
tagPath: params.tagPath,
|
tagPath: params.tagPath,
|
||||||
databases: params.dbs,
|
databases: params.dbs,
|
||||||
flowProcdefKey: params.flowProcdefKey,
|
flowProcdef: params.flowProcdef,
|
||||||
},
|
},
|
||||||
params.db
|
params.db
|
||||||
);
|
);
|
||||||
@@ -258,15 +264,17 @@ const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath)
|
|||||||
await sleep(100);
|
await sleep(100);
|
||||||
return dbInfos?.map((x: any) => {
|
return dbInfos?.map((x: any) => {
|
||||||
x.tagPath = parentNode.key;
|
x.tagPath = parentNode.key;
|
||||||
return new TagTreeNode(`${parentNode.key}.${x.id}`, x.name, NodeTypeDbInst).withParams(x);
|
return new TagTreeNode(`${x.code}`, x.name, NodeTypeDbInst).withParams(x);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.withContextMenuItems([ContextmenuItemRefresh]);
|
.withContextMenuItems([ContextmenuItemRefresh]);
|
||||||
|
|
||||||
// 数据库实例节点类型
|
// 数据库实例节点类型
|
||||||
const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((parentNode: TagTreeNode) => {
|
const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
const params = parentNode.params;
|
const params = parentNode.params;
|
||||||
const dbs = params.database.split(' ')?.sort();
|
const dbs = (await DbInst.getDbNames(params))?.sort();
|
||||||
|
|
||||||
|
const flowProcdef = await procdefApi.getByResource.request({ resourceType: TagResourceTypeEnum.DbName.value, resourceCode: params.code });
|
||||||
return dbs.map((x: any) => {
|
return dbs.map((x: any) => {
|
||||||
return new TagTreeNode(`${parentNode.key}.${x}`, x, NodeTypeDb)
|
return new TagTreeNode(`${parentNode.key}.${x}`, x, NodeTypeDb)
|
||||||
.withParams({
|
.withParams({
|
||||||
@@ -277,7 +285,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,
|
flowProcdef: flowProcdef,
|
||||||
})
|
})
|
||||||
.withIcon(DbIcon);
|
.withIcon(DbIcon);
|
||||||
});
|
});
|
||||||
@@ -302,11 +310,12 @@ const NodeTypeDb = new NodeType(SqlExecNodeType.Db)
|
|||||||
return new TagTreeNode(`${params.id}.${params.db}.schema.${sn}`, sn, NodeTypePostgresSchema).withParams(nParams).withIcon(SchemaIcon);
|
return new TagTreeNode(`${params.id}.${params.db}.schema.${sn}`, sn, NodeTypePostgresSchema).withParams(nParams).withIcon(SchemaIcon);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return NodeTypeTables(params);
|
|
||||||
|
return getNodeTypeTables(params);
|
||||||
})
|
})
|
||||||
.withNodeClickFunc(nodeClickChangeDb);
|
.withNodeClickFunc(nodeClickChangeDb);
|
||||||
|
|
||||||
const NodeTypeTables = (params: any) => {
|
const getNodeTypeTables = (params: any) => {
|
||||||
let tableKey = `${params.id}.${params.db}.table-menu`;
|
let tableKey = `${params.id}.${params.db}.table-menu`;
|
||||||
let sqlKey = getSqlMenuNodeKey(params.id, params.db);
|
let sqlKey = getSqlMenuNodeKey(params.id, params.db);
|
||||||
return [
|
return [
|
||||||
@@ -321,7 +330,7 @@ const NodeTypePostgresSchema = new NodeType(SqlExecNodeType.PgSchema)
|
|||||||
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
const params = parentNode.params;
|
const params = parentNode.params;
|
||||||
params.parentKey = parentNode.key;
|
params.parentKey = parentNode.key;
|
||||||
return NodeTypeTables(params);
|
return getNodeTypeTables(params);
|
||||||
})
|
})
|
||||||
.withNodeClickFunc(nodeClickChangeDb);
|
.withNodeClickFunc(nodeClickChangeDb);
|
||||||
|
|
||||||
@@ -337,7 +346,7 @@ 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, type, flowProcdefKey } = params;
|
let { id, db, type, flowProcdef, 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;
|
||||||
@@ -352,7 +361,8 @@ const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu)
|
|||||||
id,
|
id,
|
||||||
db,
|
db,
|
||||||
type,
|
type,
|
||||||
flowProcdefKey: flowProcdefKey,
|
schema,
|
||||||
|
flowProcdef: flowProcdef,
|
||||||
key: key,
|
key: key,
|
||||||
parentKey: parentNode.key,
|
parentKey: parentNode.key,
|
||||||
tableName: x.tableName,
|
tableName: x.tableName,
|
||||||
@@ -366,7 +376,10 @@ const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu)
|
|||||||
parentNode.params.dbTableSize = dbTableSize == 0 ? '' : 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)
|
||||||
@@ -413,6 +426,7 @@ const tagTreeRef: any = ref(null);
|
|||||||
|
|
||||||
const tabs: Map<string, TabInfo> = new Map();
|
const tabs: Map<string, TabInfo> = new Map();
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
|
defaultExpendKey: [] as any,
|
||||||
/**
|
/**
|
||||||
* 当前操作的数据库实例
|
* 当前操作的数据库实例
|
||||||
*/
|
*/
|
||||||
@@ -434,7 +448,7 @@ const state = reactive({
|
|||||||
dbId: 0,
|
dbId: 0,
|
||||||
db: '',
|
db: '',
|
||||||
dbType: '',
|
dbType: '',
|
||||||
flowProcdefKey: '',
|
flowProcdef: null as any,
|
||||||
data: {},
|
data: {},
|
||||||
parentKey: '',
|
parentKey: '',
|
||||||
},
|
},
|
||||||
@@ -447,7 +461,11 @@ const serverInfoReqParam = ref({
|
|||||||
});
|
});
|
||||||
const { execute: getDbServerInfo, isFetching: loadingServerInfo, data: dbServerInfo } = dbApi.getInstanceServerInfo.useApi<any>(serverInfoReqParam);
|
const { execute: getDbServerInfo, isFetching: loadingServerInfo, data: dbServerInfo } = dbApi.getInstanceServerInfo.useApi<any>(serverInfoReqParam);
|
||||||
|
|
||||||
|
const autoOpenResourceStore = useAutoOpenResource();
|
||||||
|
const { autoOpenResource } = storeToRefs(autoOpenResourceStore);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
autoOpenDb(autoOpenResource.value.dbCodePath);
|
||||||
setHeight();
|
setHeight();
|
||||||
// 监听浏览器窗口大小变化,更新对应组件高度
|
// 监听浏览器窗口大小变化,更新对应组件高度
|
||||||
useEventListener(window, 'resize', setHeight);
|
useEventListener(window, 'resize', setHeight);
|
||||||
@@ -457,6 +475,31 @@ onBeforeUnmount(() => {
|
|||||||
dispposeCompletionItemProvider('sql');
|
dispposeCompletionItemProvider('sql');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => autoOpenResource.value.dbCodePath,
|
||||||
|
(codePath: any) => {
|
||||||
|
autoOpenDb(codePath);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const autoOpenDb = (codePath: string) => {
|
||||||
|
if (!codePath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeAndCodes = getTagTypeCodeByPath(codePath);
|
||||||
|
const tagPath = typeAndCodes[TagResourceTypeEnum.Tag.value].join('/') + '/';
|
||||||
|
|
||||||
|
const dbCode = typeAndCodes[TagResourceTypeEnum.DbName.value][0];
|
||||||
|
state.defaultExpendKey = [tagPath, dbCode];
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
// 置空
|
||||||
|
autoOpenResourceStore.setDbCodePath('');
|
||||||
|
tagTreeRef.value.setCurrentKey(dbCode);
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置editor高度和数据表高度
|
* 设置editor高度和数据表高度
|
||||||
*/
|
*/
|
||||||
@@ -626,6 +669,8 @@ const onTabChange = () => {
|
|||||||
// 注册sql提示
|
// 注册sql提示
|
||||||
registerDbCompletionItemProvider(nowTab.dbId, nowTab.db, nowTab.params.dbs, nowDbInst.value.type);
|
registerDbCompletionItemProvider(nowTab.dbId, nowTab.db, nowTab.params.dbs, nowDbInst.value.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tagTreeRef.value.setCurrentKey(nowTab?.treeNodeKey);
|
||||||
};
|
};
|
||||||
|
|
||||||
const reloadSqls = (dbId: number, db: string) => {
|
const reloadSqls = (dbId: number, db: string) => {
|
||||||
@@ -657,7 +702,7 @@ const reloadNode = (nodeKey: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onEditTable = async (data: any) => {
|
const onEditTable = async (data: any) => {
|
||||||
let { db, id, tableName, tableComment, type, parentKey, key, flowProcdefKey } = data.params;
|
let { db, id, tableName, tableComment, type, parentKey, key, flowProcdef } = data.params;
|
||||||
// data.label就是表名
|
// data.label就是表名
|
||||||
if (tableName) {
|
if (tableName) {
|
||||||
state.tableCreateDialog.title = '修改表';
|
state.tableCreateDialog.title = '修改表';
|
||||||
@@ -676,12 +721,12 @@ const onEditTable = async (data: any) => {
|
|||||||
state.tableCreateDialog.dbId = id;
|
state.tableCreateDialog.dbId = id;
|
||||||
state.tableCreateDialog.db = db;
|
state.tableCreateDialog.db = db;
|
||||||
state.tableCreateDialog.dbType = type;
|
state.tableCreateDialog.dbType = type;
|
||||||
state.tableCreateDialog.flowProcdefKey = flowProcdefKey;
|
state.tableCreateDialog.flowProcdef = flowProcdef;
|
||||||
state.tableCreateDialog.visible = true;
|
state.tableCreateDialog.visible = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDeleteTable = async (data: any) => {
|
const onDeleteTable = async (data: any) => {
|
||||||
let { db, id, tableName, parentKey, flowProcdefKey } = data.params;
|
let { db, id, tableName, parentKey, flowProcdef, schema } = data.params;
|
||||||
await ElMessageBox.confirm(`此操作是永久性且无法撤销,确定删除【${tableName}】? `, '提示', {
|
await ElMessageBox.confirm(`此操作是永久性且无法撤销,确定删除【${tableName}】? `, '提示', {
|
||||||
confirmButtonText: '确定',
|
confirmButtonText: '确定',
|
||||||
cancelButtonText: '取消',
|
cancelButtonText: '取消',
|
||||||
@@ -689,8 +734,11 @@ const onDeleteTable = async (data: any) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 执行sql
|
// 执行sql
|
||||||
dbApi.sqlExec.request({ id, db, sql: `drop table ${getDbDialect(state.nowDbInst.type).quoteIdentifier(tableName)}` }).then(() => {
|
let dialect = getDbDialect(state.nowDbInst.type);
|
||||||
if (flowProcdefKey) {
|
let schemaStr = schema ? `${dialect.quoteIdentifier(schema)}.` : '';
|
||||||
|
|
||||||
|
dbApi.sqlExec.request({ id, db, sql: `drop table ${schemaStr + dialect.quoteIdentifier(tableName)}` }).then(() => {
|
||||||
|
if (flowProcdef) {
|
||||||
ElMessage.success('工单提交成功');
|
ElMessage.success('工单提交成功');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -702,7 +750,7 @@ const onDeleteTable = async (data: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onRenameTable = async (data: any) => {
|
const onRenameTable = async (data: any) => {
|
||||||
let { db, id, tableName, parentKey, flowProcdefKey } = data.params;
|
let { db, id, tableName, parentKey, flowProcdef } = data.params;
|
||||||
let tableData = { db, oldTableName: tableName, tableName };
|
let tableData = { db, oldTableName: tableName, tableName };
|
||||||
|
|
||||||
let value = ref(tableName);
|
let value = ref(tableName);
|
||||||
@@ -725,7 +773,7 @@ const onRenameTable = async (data: any) => {
|
|||||||
dbId: id as any,
|
dbId: id as any,
|
||||||
db: db as any,
|
db: db as any,
|
||||||
dbType: nowDbInst.value.getDialect().getInfo().formatSqlDialect,
|
dbType: nowDbInst.value.getDialect().getInfo().formatSqlDialect,
|
||||||
flowProcdefKey: flowProcdefKey,
|
flowProcdef: flowProcdef,
|
||||||
runSuccessCallback: () => {
|
runSuccessCallback: () => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
parentKey && reloadNode(parentKey);
|
parentKey && reloadNode(parentKey);
|
||||||
@@ -785,7 +833,7 @@ const getNowDbInfo = () => {
|
|||||||
name: di.name,
|
name: di.name,
|
||||||
type: di.type,
|
type: di.type,
|
||||||
host: di.host,
|
host: di.host,
|
||||||
flowProcdefKey: di.flowProcdefKey,
|
flowProcdef: di.flowProcdef,
|
||||||
dbName: state.db,
|
dbName: state.db,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -793,13 +841,8 @@ const getNowDbInfo = () => {
|
|||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.db-sql-exec {
|
.db-sql-exec {
|
||||||
.db-table-size {
|
|
||||||
color: #c4c9c4;
|
|
||||||
font-size: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.db-op {
|
.db-op {
|
||||||
height: calc(100vh - 108px);
|
height: calc(100vh - 106px);
|
||||||
}
|
}
|
||||||
|
|
||||||
#data-exec {
|
#data-exec {
|
||||||
|
|||||||
@@ -301,7 +301,9 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 该用户拥有的的操作列按钮权限
|
// 该用户拥有的的操作列按钮权限
|
||||||
|
|||||||
@@ -39,11 +39,11 @@ export const dbApi = {
|
|||||||
|
|
||||||
instances: Api.newGet('/instances'),
|
instances: Api.newGet('/instances'),
|
||||||
getInstance: Api.newGet('/instances/{instanceId}'),
|
getInstance: Api.newGet('/instances/{instanceId}'),
|
||||||
getAllDatabase: Api.newGet('/instances/{instanceId}/databases'),
|
getAllDatabase: Api.newPost('/instances/databases'),
|
||||||
|
getDbNamesByAc: Api.newGet('/instances/databases/{authCertName}'),
|
||||||
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}'),
|
||||||
|
|
||||||
// 获取数据库备份列表
|
// 获取数据库备份列表
|
||||||
@@ -83,6 +83,14 @@ 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 = {
|
export const dbSqlExecApi = {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import SvgIcon from '@/components/svgIcon/index.vue';
|
|||||||
import { getDbDialect, noSchemaTypes } 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';
|
||||||
|
import { DbInst } from '../db';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
dbId: {
|
dbId: {
|
||||||
@@ -101,9 +102,9 @@ const noSchemaType = (type: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 数据库实例节点类型
|
// 数据库实例节点类型
|
||||||
const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((parentNode: TagTreeNode) => {
|
const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
const params = parentNode.params;
|
const params = parentNode.params;
|
||||||
const dbs = params.database.split(' ')?.sort();
|
const dbs = (await DbInst.getDbNames(params))?.sort();
|
||||||
let fn: NodeType;
|
let fn: NodeType;
|
||||||
if (noSchemaType(params.type)) {
|
if (noSchemaType(params.type)) {
|
||||||
fn = MysqlNodeTypes;
|
fn = MysqlNodeTypes;
|
||||||
|
|||||||
@@ -113,7 +113,7 @@
|
|||||||
:loading="dt.loading"
|
:loading="dt.loading"
|
||||||
:abort-fn="dt.abortFn"
|
:abort-fn="dt.abortFn"
|
||||||
:height="tableDataHeight"
|
:height="tableDataHeight"
|
||||||
empty-text="tips: select *开头的单表查询或点击表名默认查询的数据,可双击数据在线修改"
|
:empty-text="state.tableDataEmptyText"
|
||||||
@change-updated-field="changeUpdatedField($event, dt)"
|
@change-updated-field="changeUpdatedField($event, dt)"
|
||||||
@data-delete="onDeleteData($event, dt)"
|
@data-delete="onDeleteData($event, dt)"
|
||||||
></db-table-data>
|
></db-table-data>
|
||||||
@@ -221,6 +221,7 @@ const state = reactive({
|
|||||||
activeTab: 1,
|
activeTab: 1,
|
||||||
editorHeight: '500',
|
editorHeight: '500',
|
||||||
tableDataHeight: '250px',
|
tableDataHeight: '250px',
|
||||||
|
tableDataEmptyText: 'tips: select *开头的单表查询或点击表名默认查询的数据,可双击数据在线修改',
|
||||||
});
|
});
|
||||||
|
|
||||||
const { tableDataHeight } = toRefs(state);
|
const { tableDataHeight } = toRefs(state);
|
||||||
@@ -295,44 +296,37 @@ const onRunSql = async (newTab = false) => {
|
|||||||
notBlank(sql && sql.trim(), '请选中需要执行的sql');
|
notBlank(sql && sql.trim(), '请选中需要执行的sql');
|
||||||
// 去除字符串前的空格、换行等
|
// 去除字符串前的空格、换行等
|
||||||
sql = sql.replace(/(^\s*)/g, '');
|
sql = sql.replace(/(^\s*)/g, '');
|
||||||
let execRemark = '';
|
|
||||||
let canRun = true;
|
|
||||||
|
|
||||||
// 简单截取前十个字符
|
// 简单截取前十个字符
|
||||||
const sqlPrefix = sql.slice(0, 10).toLowerCase();
|
const sqlPrefix = sql.slice(0, 10).toLowerCase();
|
||||||
if (
|
const nonQuery =
|
||||||
sqlPrefix.startsWith('update') ||
|
sqlPrefix.startsWith('update') ||
|
||||||
sqlPrefix.startsWith('insert') ||
|
sqlPrefix.startsWith('insert') ||
|
||||||
sqlPrefix.startsWith('delete') ||
|
sqlPrefix.startsWith('delete') ||
|
||||||
sqlPrefix.startsWith('alert') ||
|
sqlPrefix.startsWith('alert') ||
|
||||||
sqlPrefix.startsWith('drop') ||
|
sqlPrefix.startsWith('drop') ||
|
||||||
sqlPrefix.startsWith('create')
|
sqlPrefix.startsWith('create');
|
||||||
) {
|
|
||||||
const res: any = await ElMessageBox.prompt('请输入备注', 'Tip', {
|
// 启用工单审批
|
||||||
confirmButtonText: '确定',
|
if (nonQuery && getNowDbInst().flowProcdef) {
|
||||||
cancelButtonText: '取消',
|
try {
|
||||||
inputPattern: /^[\s\S]*.*[^\s][\s\S]*$/,
|
getNowDbInst().promptExeSql(props.dbName, sql, null, () => {
|
||||||
inputErrorMessage: '请输入执行该sql的备注信息',
|
ElMessage.success('工单提交成功');
|
||||||
});
|
});
|
||||||
execRemark = res.value;
|
} catch (e) {
|
||||||
if (!execRemark) {
|
ElMessage.success('工单提交失败');
|
||||||
canRun = false;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (!canRun) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 启用工单审批
|
let execRemark;
|
||||||
if (execRemark && getNowDbInst().flowProcdefKey) {
|
if (nonQuery) {
|
||||||
try {
|
const res: any = await ElMessageBox.prompt('请输入备注', 'Tip', {
|
||||||
await getNowDbInst().runSql(props.dbName, sql, execRemark);
|
confirmButtonText: '确定',
|
||||||
ElMessage.success('工单提交成功');
|
cancelButtonText: '取消',
|
||||||
return;
|
inputErrorMessage: '输入执行该sql的备注信息',
|
||||||
} catch (e) {
|
});
|
||||||
ElMessage.success('工单提交失败');
|
execRemark = res.value;
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let execRes: ExecResTab;
|
let execRes: ExecResTab;
|
||||||
@@ -368,9 +362,8 @@ const onRunSql = async (newTab = false) => {
|
|||||||
|
|
||||||
await execute();
|
await execute();
|
||||||
const colAndData: any = data.value;
|
const colAndData: any = data.value;
|
||||||
if (!colAndData.res || colAndData.res.length === 0) {
|
if (colAndData.res.length == 0) {
|
||||||
ElMessage.warning('未查询到结果集');
|
state.tableDataEmptyText = '查无数据';
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 要实时响应,故需要用索引改变数据才生效
|
// 要实时响应,故需要用索引改变数据才生效
|
||||||
@@ -467,7 +460,7 @@ const formatSql = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatDialect = getNowDbInst().getDialect().getInfo().formatSqlDialect;
|
const formatDialect: any = getNowDbInst().getDialect().getInfo().formatSqlDialect;
|
||||||
|
|
||||||
let sql = monacoEditor.getModel()?.getValueInRange(selection);
|
let sql = monacoEditor.getModel()?.getValueInRange(selection);
|
||||||
// 有选中sql则格式化并替换选中sql, 否则格式化编辑器所有内容
|
// 有选中sql则格式化并替换选中sql, 否则格式化编辑器所有内容
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export type SqlExecProps = {
|
|||||||
dbId: number;
|
dbId: number;
|
||||||
db: string;
|
db: string;
|
||||||
dbType?: string;
|
dbType?: string;
|
||||||
flowProcdefKey?: string;
|
flowProcdef?: any;
|
||||||
runSuccessCallback?: Function;
|
runSuccessCallback?: Function;
|
||||||
cancelCallback?: Function;
|
cancelCallback?: Function;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<el-dialog title="待执行SQL" v-model="dialogVisible" :show-close="false" width="600px">
|
<el-dialog title="待执行SQL" v-model="dialogVisible" :show-close="false" width="600px" :close-on-click-modal="false">
|
||||||
<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.flowProcdef ? '执行备注(必填)' : '执行备注(选填)'"
|
||||||
|
class="mt5"
|
||||||
|
/>
|
||||||
|
|
||||||
<div v-if="props.flowProcdefKey">
|
<div v-if="props.flowProcdef">
|
||||||
<el-divider content-position="left">审批节点</el-divider>
|
<el-divider content-position="left">审批节点</el-divider>
|
||||||
<procdef-tasks :procdef-key="props.flowProcdefKey" />
|
<procdef-tasks :procdef="props.flowProcdef" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -52,13 +58,16 @@ onMounted(() => {
|
|||||||
* 执行sql
|
* 执行sql
|
||||||
*/
|
*/
|
||||||
const runSql = async () => {
|
const runSql = async () => {
|
||||||
if (!state.remark) {
|
// 存在流程审批,则备注为必填
|
||||||
|
if (!state.remark && props.flowProcdef) {
|
||||||
ElMessage.error('请输入执行的备注信息');
|
ElMessage.error('请输入执行的备注信息');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
state.btnLoading = true;
|
state.btnLoading = true;
|
||||||
|
runSuccess = true;
|
||||||
|
|
||||||
const res = await dbApi.sqlExec.request({
|
const res = await dbApi.sqlExec.request({
|
||||||
id: props.dbId,
|
id: props.dbId,
|
||||||
db: props.db,
|
db: props.db,
|
||||||
@@ -67,8 +76,7 @@ const runSql = async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 存在流程审批
|
// 存在流程审批
|
||||||
if (props.flowProcdefKey) {
|
if (props.flowProcdef) {
|
||||||
runSuccess = false;
|
|
||||||
ElMessage.success('工单提交成功');
|
ElMessage.success('工单提交成功');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -80,7 +88,6 @@ const runSql = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
runSuccess = true;
|
|
||||||
ElMessage.success('执行成功');
|
ElMessage.success('执行成功');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
runSuccess = false;
|
runSuccess = false;
|
||||||
@@ -89,9 +96,9 @@ const runSql = async () => {
|
|||||||
if (props.runSuccessCallback) {
|
if (props.runSuccessCallback) {
|
||||||
props.runSuccessCallback();
|
props.runSuccessCallback();
|
||||||
}
|
}
|
||||||
|
cancel();
|
||||||
}
|
}
|
||||||
state.btnLoading = false;
|
state.btnLoading = false;
|
||||||
cancel();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -106,7 +113,7 @@ const cancel = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const open = () => {
|
const open = () => {
|
||||||
state.sqlValue = sqlFormatter(props.sql, { language: props.dbType || 'mysql' });
|
state.sqlValue = sqlFormatter(props.sql, { language: (props.dbType || 'mysql') as any });
|
||||||
state.dialogVisible = true;
|
state.dialogVisible = true;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
remarkInputRef.value?.focus();
|
remarkInputRef.value?.focus();
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="string-input-container w100" v-if="dataType == DataType.String">
|
<div class="string-input-container w100" v-if="dataType == DataType.String || dataType == DataType.Number">
|
||||||
<el-input
|
<el-input
|
||||||
v-if="dataType == DataType.String"
|
|
||||||
:ref="(el: any) => focus && el?.focus()"
|
:ref="(el: any) => focus && el?.focus()"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
@blur="handleBlur"
|
@blur="handleBlur"
|
||||||
@@ -13,18 +12,6 @@
|
|||||||
<SvgIcon v-if="showEditorIcon" @mousedown="openEditor" class="string-input-container-icon" name="FullScreen" :size="10" />
|
<SvgIcon v-if="showEditorIcon" @mousedown="openEditor" class="string-input-container-icon" name="FullScreen" :size="10" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-input
|
|
||||||
v-else-if="dataType == DataType.Number"
|
|
||||||
:ref="(el: any) => focus && el?.focus()"
|
|
||||||
:disabled="disabled"
|
|
||||||
@blur="handleBlur"
|
|
||||||
class="w100 mb4"
|
|
||||||
size="small"
|
|
||||||
v-model.number="itemValue"
|
|
||||||
:placeholder="placeholder"
|
|
||||||
type="number"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<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()"
|
||||||
@@ -75,7 +62,7 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref, Ref } from 'vue';
|
import { computed, ref, Ref } from 'vue';
|
||||||
import { ElInput } from 'element-plus';
|
import { ElInput, ElMessage } 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';
|
||||||
import MonacoEditorDialog from '@/components/monaco/MonacoEditorDialog';
|
import MonacoEditorDialog from '@/components/monaco/MonacoEditorDialog';
|
||||||
@@ -130,6 +117,10 @@ const handleBlur = () => {
|
|||||||
if (editorOpening.value) {
|
if (editorOpening.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (props.dataType == DataType.Number && itemValue.value && !/^-?\d*\.?\d+$/.test(itemValue.value)) {
|
||||||
|
ElMessage.error('输入内容与类型不匹配');
|
||||||
|
return;
|
||||||
|
}
|
||||||
emit('update:modelValue', itemValue.value);
|
emit('update:modelValue', itemValue.value);
|
||||||
emit('blur');
|
emit('blur');
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
<!-- 字段列的数据类型 -->
|
<!-- 字段列的数据类型 -->
|
||||||
<div class="column-type">
|
<div class="column-type">
|
||||||
<span v-if="column.dataTypeSubscript === 'icon-clock'">
|
<span v-if="column.dataTypeSubscript === 'icon-clock'">
|
||||||
<SvgIcon :size="10" name="Clock" style="cursor: unset" />
|
<SvgIcon :size="9" name="Clock" style="cursor: unset" />
|
||||||
</span>
|
</span>
|
||||||
<span class="font8" v-else>{{ column.dataTypeSubscript }}</span>
|
<span class="font8" v-else>{{ column.dataTypeSubscript }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -121,7 +121,7 @@
|
|||||||
|
|
||||||
<template #empty>
|
<template #empty>
|
||||||
<div style="text-align: center">
|
<div style="text-align: center">
|
||||||
<el-empty class="h100" :description="state.emptyText" :image-size="100" />
|
<el-empty class="h100" :description="props.emptyText" :image-size="100" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-v2>
|
</el-table-v2>
|
||||||
@@ -161,7 +161,7 @@ import { DbInst } from '@/views/ops/db/db';
|
|||||||
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
|
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
|
||||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||||
import { exportCsv, exportFile } from '@/common/utils/export';
|
import { exportCsv, exportFile } from '@/common/utils/export';
|
||||||
import { dateStrFormat } from '@/common/utils/date';
|
import { formatDate } from '@/common/utils/format';
|
||||||
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';
|
||||||
@@ -342,8 +342,6 @@ const state = reactive({
|
|||||||
columns: [] as any,
|
columns: [] as any,
|
||||||
loading: false,
|
loading: false,
|
||||||
tableHeight: '600px',
|
tableHeight: '600px',
|
||||||
emptyText: '',
|
|
||||||
|
|
||||||
execTime: 0,
|
execTime: 0,
|
||||||
contextmenu: {
|
contextmenu: {
|
||||||
dropdown: {
|
dropdown: {
|
||||||
@@ -434,7 +432,6 @@ onMounted(async () => {
|
|||||||
console.log('in DbTable mounted');
|
console.log('in DbTable mounted');
|
||||||
state.tableHeight = props.height;
|
state.tableHeight = props.height;
|
||||||
state.loading = props.loading;
|
state.loading = props.loading;
|
||||||
state.emptyText = props.emptyText;
|
|
||||||
|
|
||||||
state.dbId = props.dbId;
|
state.dbId = props.dbId;
|
||||||
state.dbType = getNowDbInst().type;
|
state.dbType = getNowDbInst().type;
|
||||||
@@ -461,7 +458,7 @@ 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]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -620,6 +617,10 @@ const onDeleteData = async () => {
|
|||||||
const db = state.db;
|
const db = state.db;
|
||||||
const dbInst = getNowDbInst();
|
const dbInst = getNowDbInst();
|
||||||
dbInst.promptExeSql(db, await dbInst.genDeleteByPrimaryKeysSql(db, state.table, deleteDatas as any), null, () => {
|
dbInst.promptExeSql(db, await dbInst.genDeleteByPrimaryKeysSql(db, state.table, deleteDatas as any), null, () => {
|
||||||
|
// 存在流程则恢复原值,需工单流程审批完后自动执行
|
||||||
|
if (dbInst.flowProcdef) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
emits('dataDelete', deleteDatas);
|
emits('dataDelete', deleteDatas);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -631,7 +632,7 @@ const onEditRowData = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data = selectionDatas[0];
|
const data = selectionDatas[0];
|
||||||
state.tableDataFormDialog.data = data;
|
state.tableDataFormDialog.data = { ...data };
|
||||||
state.tableDataFormDialog.title = `编辑表'${props.table}'数据`;
|
state.tableDataFormDialog.title = `编辑表'${props.table}'数据`;
|
||||||
state.tableDataFormDialog.visible = true;
|
state.tableDataFormDialog.visible = true;
|
||||||
};
|
};
|
||||||
@@ -677,13 +678,13 @@ const onExportCsv = () => {
|
|||||||
columnNames.push(column.columnName);
|
columnNames.push(column.columnName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
exportCsv(`数据导出-${state.table}-${dateStrFormat('yyyyMMddHHmm', new Date().toString())}`, columnNames, dataList);
|
exportCsv(`数据导出-${state.table}-${formatDate(new Date(), 'yyyyMMddHHmm')}`, columnNames, dataList);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onExportSql = async () => {
|
const onExportSql = async () => {
|
||||||
const selectionDatas = state.datas;
|
const selectionDatas = state.datas;
|
||||||
exportFile(
|
exportFile(
|
||||||
`数据导出-${state.table}-${dateStrFormat('yyyyMMddHHmm', new Date().toString())}.sql`,
|
`数据导出-${state.table}-${formatDate(new Date(), 'yyyyMMddHHmm')}.sql`,
|
||||||
await getNowDbInst().genInsertSql(state.db, state.table, selectionDatas)
|
await getNowDbInst().genInsertSql(state.db, state.table, selectionDatas)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -766,7 +767,12 @@ const submitUpdateFields = async () => {
|
|||||||
res += await dbInst.genUpdateSql(db, state.table, updateColumnValue, rowData);
|
res += await dbInst.genUpdateSql(db, state.table, updateColumnValue, rowData);
|
||||||
}
|
}
|
||||||
|
|
||||||
dbInst.promptExeSql(db, res, cancelUpdateFields, () => {
|
dbInst.promptExeSql(db, res, null, () => {
|
||||||
|
// 存在流程则恢复原值,需工单流程审批完后自动执行
|
||||||
|
if (dbInst.flowProcdef) {
|
||||||
|
cancelUpdateFields();
|
||||||
|
return;
|
||||||
|
}
|
||||||
triggerRefresh();
|
triggerRefresh();
|
||||||
cellUpdateMap.clear();
|
cellUpdateMap.clear();
|
||||||
changeUpdatedField();
|
changeUpdatedField();
|
||||||
@@ -813,11 +819,11 @@ const getFormatTimeValue = (dataType: DataType, originValue: string): string =>
|
|||||||
|
|
||||||
switch (dataType) {
|
switch (dataType) {
|
||||||
case DataType.Time:
|
case DataType.Time:
|
||||||
return dateStrFormat('HH:mm:ss', originValue);
|
return formatDate(originValue, 'HH:mm:ss');
|
||||||
case DataType.Date:
|
case DataType.Date:
|
||||||
return dateStrFormat('yyyy-MM-dd', originValue);
|
return formatDate(originValue, 'YYYY-MM-DD');
|
||||||
case DataType.DateTime:
|
case DataType.DateTime:
|
||||||
return dateStrFormat('yyyy-MM-dd HH:mm:ss', originValue);
|
return formatDate(originValue, 'YYYY-MM-DD HH:mm:ss');
|
||||||
default:
|
default:
|
||||||
return originValue;
|
return originValue;
|
||||||
}
|
}
|
||||||
@@ -883,8 +889,8 @@ 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: -5px;
|
top: -7px;
|
||||||
padding: 2px;
|
padding: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.column-right {
|
.column-right {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
:key="column.columnName"
|
:key="column.columnName"
|
||||||
class="w100 mb5"
|
class="w100 mb5"
|
||||||
:prop="column.columnName"
|
:prop="column.columnName"
|
||||||
:required="column.nullable != 'YES' && !column.isPrimaryKey && !column.isIdentity"
|
:required="!column.nullable && !column.isPrimaryKey && !column.isIdentity"
|
||||||
>
|
>
|
||||||
<template #label>
|
<template #label>
|
||||||
<span class="pointer" :title="`${column.columnType} | ${column.columnComment}`">
|
<span class="pointer" :title="`${column.columnType} | ${column.columnComment}`">
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
<ColumnFormItem
|
<ColumnFormItem
|
||||||
v-model="modelValue[`${column.columnName}`]"
|
v-model="modelValue[`${column.columnName}`]"
|
||||||
:data-type="dbInst.getDialect().getDataType(column.columnType)"
|
:data-type="dbInst.getDialect().getDataType(column.dataType)"
|
||||||
:placeholder="`${column.columnType} ${column.columnComment}`"
|
:placeholder="`${column.columnType} ${column.columnComment}`"
|
||||||
:column-name="column.columnName"
|
:column-name="column.columnName"
|
||||||
:disabled="column.isIdentity"
|
:disabled="column.isIdentity"
|
||||||
@@ -37,7 +37,6 @@ import { ref, watch, onMounted } from 'vue';
|
|||||||
import ColumnFormItem from './ColumnFormItem.vue';
|
import ColumnFormItem from './ColumnFormItem.vue';
|
||||||
import { DbInst } from '../../db';
|
import { DbInst } from '../../db';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
import { getDbDialect } from '@/views/ops/db/dialect';
|
|
||||||
|
|
||||||
export interface ColumnFormItemProps {
|
export interface ColumnFormItemProps {
|
||||||
dbInst: DbInst;
|
dbInst: DbInst;
|
||||||
@@ -86,35 +85,35 @@ const closeDialog = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const confirm = async () => {
|
const confirm = async () => {
|
||||||
dataForm.value.validate(async (valid: boolean) => {
|
try {
|
||||||
if (!valid) {
|
await dataForm.value.validate();
|
||||||
ElMessage.error('请正确填写数据信息');
|
} catch (e: any) {
|
||||||
return false;
|
ElMessage.error('请正确填写数据信息');
|
||||||
}
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const dbInst = props.dbInst;
|
const dbInst = props.dbInst;
|
||||||
const data = modelValue.value;
|
const data = modelValue.value;
|
||||||
const db = props.dbName;
|
const db = props.dbName;
|
||||||
const tableName = props.tableName;
|
const tableName = props.tableName;
|
||||||
|
|
||||||
let sql = '';
|
let sql = '';
|
||||||
if (oldValue) {
|
if (oldValue) {
|
||||||
const updateColumnValue = {};
|
const updateColumnValue = {};
|
||||||
Object.keys(oldValue).forEach((key) => {
|
Object.keys(oldValue).forEach((key) => {
|
||||||
// 如果新旧值不相等,则为需要更新的字段
|
// 如果新旧值不相等,则为需要更新的字段
|
||||||
if (oldValue[key] !== modelValue.value[key]) {
|
if (oldValue[key] !== modelValue.value[key]) {
|
||||||
updateColumnValue[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');
|
|
||||||
});
|
});
|
||||||
|
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>
|
</script>
|
||||||
|
|||||||
@@ -12,15 +12,29 @@
|
|||||||
width="auto"
|
width="auto"
|
||||||
title="表格字段配置"
|
title="表格字段配置"
|
||||||
trigger="click"
|
trigger="click"
|
||||||
|
@hide="triggerCheckedColumns"
|
||||||
>
|
>
|
||||||
<div v-for="(item, index) in columns" :key="index">
|
<div><el-input v-model="checkedShowColumns.searchKey" size="small" placeholder="输入列名或备注过滤" /></div>
|
||||||
|
<div>
|
||||||
<el-checkbox
|
<el-checkbox
|
||||||
v-model="item.show"
|
v-model="checkedShowColumns.checkedAllColumn"
|
||||||
:label="`${!item.columnComment ? item.columnName : item.columnName + ' [' + item.columnComment + ']'}`"
|
:indeterminate="checkedShowColumns.isIndeterminate"
|
||||||
:true-value="true"
|
@change="handleCheckAllColumnChange"
|
||||||
:false-value="false"
|
|
||||||
size="small"
|
size="small"
|
||||||
/>
|
>
|
||||||
|
选择所有
|
||||||
|
</el-checkbox>
|
||||||
|
|
||||||
|
<el-checkbox-group v-model="checkedShowColumns.columnNames" @change="handleCheckedColumnChange">
|
||||||
|
<div v-for="(item, index) in filterCheckedColumns" :key="index">
|
||||||
|
<el-checkbox
|
||||||
|
:key="index"
|
||||||
|
:label="`${!item.columnComment ? item.columnName : item.columnName + ' [' + item.columnComment + ']'}`"
|
||||||
|
:value="item.columnName"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-checkbox-group>
|
||||||
</div>
|
</div>
|
||||||
<template #reference>
|
<template #reference>
|
||||||
<el-link icon="Operation" size="small" :underline="false"></el-link>
|
<el-link icon="Operation" size="small" :underline="false"></el-link>
|
||||||
@@ -36,17 +50,6 @@
|
|||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<el-divider direction="vertical" border-style="dashed" />
|
<el-divider direction="vertical" border-style="dashed" />
|
||||||
|
|
||||||
<el-tooltip :show-after="500" class="box-item" effect="dark" content="commit" placement="top">
|
|
||||||
<template #content>
|
|
||||||
1. 右击数据/表头可显示操作菜单 <br />
|
|
||||||
2. 按住Ctrl点击数据则为多选 <br />
|
|
||||||
3. 双击单元格可编辑数据 <br />
|
|
||||||
4. 鼠标悬停字段名或标签树的表名可提示相关备注
|
|
||||||
</template>
|
|
||||||
<el-link icon="QuestionFilled" :underline="false"> </el-link>
|
|
||||||
</el-tooltip>
|
|
||||||
<el-divider direction="vertical" border-style="dashed" />
|
|
||||||
|
|
||||||
<!-- 表数据展示配置 -->
|
<!-- 表数据展示配置 -->
|
||||||
<el-popover
|
<el-popover
|
||||||
popper-style="max-height: 550px; overflow: auto; max-width: 450px"
|
popper-style="max-height: 550px; overflow: auto; max-width: 450px"
|
||||||
@@ -205,8 +208,8 @@
|
|||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<el-dialog v-model="conditionDialog.visible" :title="conditionDialog.title" width="420px">
|
<el-dialog v-model="conditionDialog.visible" :title="conditionDialog.title" width="460px">
|
||||||
<el-row>
|
<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>
|
||||||
@@ -329,9 +332,17 @@ const state = reactive({
|
|||||||
tableHeight: '600px',
|
tableHeight: '600px',
|
||||||
hasUpdatedFileds: false,
|
hasUpdatedFileds: false,
|
||||||
dbDialect: {} as DbDialect,
|
dbDialect: {} as DbDialect,
|
||||||
|
|
||||||
|
checkedShowColumns: {
|
||||||
|
searchKey: '',
|
||||||
|
checkedAllColumn: true,
|
||||||
|
isIndeterminate: false,
|
||||||
|
columnNames: [] as any,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { datas, condition, loading, columns, pageNum, pageSize, pageSizes, sql, hasUpdatedFileds, conditionDialog, addDataDialog } = toRefs(state);
|
const { datas, condition, loading, columns, checkedShowColumns, pageNum, pageSize, pageSizes, sql, hasUpdatedFileds, conditionDialog, addDataDialog } =
|
||||||
|
toRefs(state);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.tableHeight,
|
() => props.tableHeight,
|
||||||
@@ -351,6 +362,8 @@ onMounted(async () => {
|
|||||||
|
|
||||||
state.dbDialect = getNowDbInst().getDialect();
|
state.dbDialect = getNowDbInst().getDialect();
|
||||||
useEventListener('click', handlerWindowClick);
|
useEventListener('click', handlerWindowClick);
|
||||||
|
|
||||||
|
state.checkedShowColumns.columnNames = state.columns.map((item: any) => item.columnName);
|
||||||
});
|
});
|
||||||
|
|
||||||
const handlerWindowClick = () => {
|
const handlerWindowClick = () => {
|
||||||
@@ -414,6 +427,7 @@ const handleSetPageNum = async () => {
|
|||||||
state.pageNum = state.setPageNum;
|
state.pageNum = state.setPageNum;
|
||||||
await selectData();
|
await selectData();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCount = async () => {
|
const handleCount = async () => {
|
||||||
state.counting = true;
|
state.counting = true;
|
||||||
|
|
||||||
@@ -431,6 +445,24 @@ const handleCount = async () => {
|
|||||||
state.counting = false;
|
state.counting = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCheckAllColumnChange = (val: boolean) => {
|
||||||
|
state.checkedShowColumns.columnNames = val ? state.columns.map((x: any) => x.columnName) : [];
|
||||||
|
state.checkedShowColumns.isIndeterminate = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCheckedColumnChange = (value: string[]) => {
|
||||||
|
const checkedCount = value.length;
|
||||||
|
state.checkedShowColumns.checkedAllColumn = checkedCount === state.columns.length;
|
||||||
|
state.checkedShowColumns.isIndeterminate = checkedCount > 0 && checkedCount < state.columns.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
const triggerCheckedColumns = () => {
|
||||||
|
const checkedColumnNames = state.checkedShowColumns.columnNames;
|
||||||
|
for (let column of state.columns) {
|
||||||
|
column.show = checkedColumnNames.includes(column.columnName);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 完整的条件,每次选中后会重置条件框内容,故需要这个变量在获取建议时将文本框内容保存
|
// 完整的条件,每次选中后会重置条件框内容,故需要这个变量在获取建议时将文本框内容保存
|
||||||
let completeCond = '';
|
let completeCond = '';
|
||||||
// 是否存在列建议
|
// 是否存在列建议
|
||||||
@@ -463,7 +495,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} ''`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -490,16 +522,23 @@ const chooseCondColumnName = () => {
|
|||||||
* 过滤条件列名
|
* 过滤条件列名
|
||||||
*/
|
*/
|
||||||
const filterCondColumns = computed(() => {
|
const filterCondColumns = computed(() => {
|
||||||
|
return filterColumns(state.columnNameSearch);
|
||||||
|
});
|
||||||
|
|
||||||
|
const filterCheckedColumns = computed(() => {
|
||||||
|
return filterColumns(state.checkedShowColumns.searchKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
const filterColumns = (searchKey: string) => {
|
||||||
const columns = state.columns;
|
const columns = state.columns;
|
||||||
let columnNameSearch = state.columnNameSearch;
|
if (!searchKey) {
|
||||||
if (!columnNameSearch) {
|
|
||||||
return columns;
|
return columns;
|
||||||
}
|
}
|
||||||
columnNameSearch = columnNameSearch.toLowerCase();
|
searchKey = searchKey.toLowerCase();
|
||||||
return columns.filter((data: any) => {
|
return columns.filter((data: any) => {
|
||||||
return data.columnName.toLowerCase().includes(columnNameSearch) || data.columnComment.toLowerCase().includes(columnNameSearch);
|
return data.columnName.toLowerCase().includes(searchKey) || data.columnComment.toLowerCase().includes(searchKey);
|
||||||
});
|
});
|
||||||
});
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 条件查询,点击列信息后显示输入对应的值
|
* 条件查询,点击列信息后显示输入对应的值
|
||||||
@@ -524,7 +563,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 + state.dbDialect.wrapValue(row.columnType, conditionDialog.value!);
|
state.condition = condition + state.dbDialect.wrapValue(row.dataType, conditionDialog.value!);
|
||||||
onCancelCondition();
|
onCancelCondition();
|
||||||
condInputRef.value.focus();
|
condInputRef.value.focus();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -131,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: {
|
||||||
@@ -151,8 +152,8 @@ const props = defineProps({
|
|||||||
dbType: {
|
dbType: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
flowProcdefKey: {
|
flowProcdef: {
|
||||||
type: String,
|
type: Object,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -192,7 +193,7 @@ const state = reactive({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'numScale',
|
prop: 'numScale',
|
||||||
label: '小数点',
|
label: '小数精度',
|
||||||
width: 120,
|
width: 120,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -334,7 +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,
|
flowProcdef: props.flowProcdef,
|
||||||
runSuccessCallback: () => {
|
runSuccessCallback: () => {
|
||||||
emit('submit-sql', { tableName: state.tableData.tableName });
|
emit('submit-sql', { tableName: state.tableData.tableName });
|
||||||
// cancel();
|
// cancel();
|
||||||
@@ -412,7 +413,6 @@ const filterChangedData = (oldArr: object[], nowArr: object[], key: string): { d
|
|||||||
|
|
||||||
const genSql = () => {
|
const genSql = () => {
|
||||||
let data = state.tableData;
|
let data = state.tableData;
|
||||||
console.log(data);
|
|
||||||
// 创建表
|
// 创建表
|
||||||
if (!props.data?.edit) {
|
if (!props.data?.edit) {
|
||||||
let createTable = dbDialect.getCreateTableSql(data);
|
let createTable = dbDialect.getCreateTableSql(data);
|
||||||
@@ -501,12 +501,10 @@ watch(
|
|||||||
state.tableData.indexs.res = [];
|
state.tableData.indexs.res = [];
|
||||||
// 索引列下拉选
|
// 索引列下拉选
|
||||||
state.tableData.indexs.columns = [];
|
state.tableData.indexs.columns = [];
|
||||||
|
DbInst.initColumns(columns);
|
||||||
// 回显列
|
// 回显列
|
||||||
if (columns && Array.isArray(columns) && columns.length > 0) {
|
if (columns && Array.isArray(columns) && columns.length > 0) {
|
||||||
columns.forEach((a) => {
|
columns.forEach((a) => {
|
||||||
let typeObj = a.columnType.replace(')', '').split('(');
|
|
||||||
let type = typeObj[0];
|
|
||||||
let length = (typeObj.length > 1 && typeObj[1]) || '';
|
|
||||||
let defaultValue = '';
|
let defaultValue = '';
|
||||||
if (a.columnDefault) {
|
if (a.columnDefault) {
|
||||||
defaultValue = a.columnDefault.trim().replace(/^'|'$/g, '');
|
defaultValue = a.columnDefault.trim().replace(/^'|'$/g, '');
|
||||||
@@ -516,11 +514,11 @@ watch(
|
|||||||
let data = {
|
let data = {
|
||||||
name: a.columnName,
|
name: a.columnName,
|
||||||
oldName: a.columnName,
|
oldName: a.columnName,
|
||||||
type,
|
type: a.dataType,
|
||||||
value: defaultValue,
|
value: defaultValue,
|
||||||
length,
|
length: a.showLength,
|
||||||
numScale: a.numScale,
|
numScale: a.showScale,
|
||||||
notNull: a.nullable !== 'YES',
|
notNull: !a.nullable,
|
||||||
pri: a.isPrimaryKey,
|
pri: a.isPrimaryKey,
|
||||||
auto_increment: a.isIdentity /*a.extra?.indexOf('auto_increment') > -1*/,
|
auto_increment: a.isIdentity /*a.extra?.indexOf('auto_increment') > -1*/,
|
||||||
remark: a.columnComment,
|
remark: a.columnComment,
|
||||||
|
|||||||
@@ -1,28 +1,27 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="db-table">
|
<div class="db-table">
|
||||||
<el-row class="mb5">
|
<el-row class="mb5">
|
||||||
<el-popover v-model:visible="showDumpInfo" :width="470" placement="right" trigger="click">
|
<el-popover v-model:visible="state.dumpInfo.visible" trigger="click" :width="470" placement="right">
|
||||||
<template #reference>
|
<template #reference>
|
||||||
<el-button class="ml5" type="success" size="small">导出</el-button>
|
<el-button :disabled="state.dumpInfo.tables?.length == 0" class="ml5" type="success" size="small">导出</el-button>
|
||||||
</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>
|
||||||
|
|
||||||
<el-form-item label="导出表: ">
|
<el-form-item>
|
||||||
<el-table @selection-change="handleDumpTableSelectionChange" max-height="300" size="small" :data="tables">
|
<el-table :data="state.dumpInfo.tables" empty-text="请先选择要导出的表" max-height="300" size="small">
|
||||||
<el-table-column type="selection" width="45" />
|
|
||||||
<el-table-column property="tableName" label="表名" min-width="150" show-overflow-tooltip> </el-table-column>
|
<el-table-column property="tableName" label="表名" min-width="150" show-overflow-tooltip> </el-table-column>
|
||||||
<el-table-column property="tableComment" label="备注" min-width="150" show-overflow-tooltip> </el-table-column>
|
<el-table-column property="tableComment" label="备注" min-width="150" show-overflow-tooltip> </el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<div style="text-align: right">
|
<div style="text-align: right">
|
||||||
<el-button @click="showDumpInfo = false" size="small">取消</el-button>
|
<el-button @click="state.dumpInfo.visible = false" size="small">取消</el-button>
|
||||||
<el-button @click="dump(db)" type="success" size="small">确定</el-button>
|
<el-button @click="dump(db)" type="success" size="small">确定</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-popover>
|
</el-popover>
|
||||||
@@ -30,7 +29,9 @@
|
|||||||
<el-button type="primary" size="small" @click="openEditTable(false)">创建表</el-button>
|
<el-button type="primary" size="small" @click="openEditTable(false)">创建表</el-button>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<el-table v-loading="loading" border stripe :data="filterTableInfos" size="small" :height="height">
|
<el-table v-loading="loading" @selection-change="handleDumpTableSelectionChange" border stripe :data="filterTableInfos" size="small" :height="height">
|
||||||
|
<el-table-column type="selection" width="30" />
|
||||||
|
|
||||||
<el-table-column property="tableName" label="表名" min-width="150" show-overflow-tooltip>
|
<el-table-column property="tableName" label="表名" min-width="150" show-overflow-tooltip>
|
||||||
<template #header>
|
<template #header>
|
||||||
<el-input v-model="tableNameSearch" size="small" placeholder="表名: 输入可过滤" clearable />
|
<el-input v-model="tableNameSearch" size="small" placeholder="表名: 输入可过滤" clearable />
|
||||||
@@ -98,8 +99,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
|
||||||
@@ -108,7 +109,7 @@
|
|||||||
:dbId="dbId"
|
:dbId="dbId"
|
||||||
:db="db"
|
:db="db"
|
||||||
:dbType="dbType"
|
:dbType="dbType"
|
||||||
:flow-procdef-key="props.flowProcdefKey"
|
:flow-procdef="props.flowProcdef"
|
||||||
:data="tableCreateDialog.data"
|
:data="tableCreateDialog.data"
|
||||||
v-model:visible="tableCreateDialog.visible"
|
v-model:visible="tableCreateDialog.visible"
|
||||||
@submit-sql="onSubmitSql"
|
@submit-sql="onSubmitSql"
|
||||||
@@ -126,7 +127,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, editDbTypes } 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'));
|
||||||
|
|
||||||
@@ -147,8 +151,8 @@ const props = defineProps({
|
|||||||
type: [String],
|
type: [String],
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
flowProcdefKey: {
|
flowProcdef: {
|
||||||
type: [String],
|
type: [Object],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -158,8 +162,8 @@ const state = reactive({
|
|||||||
tables: [],
|
tables: [],
|
||||||
tableNameSearch: '',
|
tableNameSearch: '',
|
||||||
tableCommentSearch: '',
|
tableCommentSearch: '',
|
||||||
showDumpInfo: false,
|
|
||||||
dumpInfo: {
|
dumpInfo: {
|
||||||
|
visible: false,
|
||||||
id: 0,
|
id: 0,
|
||||||
db: '',
|
db: '',
|
||||||
type: 3,
|
type: 3,
|
||||||
@@ -198,19 +202,7 @@ const state = reactive({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const { loading, tableNameSearch, tableCommentSearch, dumpInfo, chooseTableName, columnDialog, indexDialog, ddlDialog, tableCreateDialog } = toRefs(state);
|
||||||
loading,
|
|
||||||
tables,
|
|
||||||
tableNameSearch,
|
|
||||||
tableCommentSearch,
|
|
||||||
showDumpInfo,
|
|
||||||
dumpInfo,
|
|
||||||
chooseTableName,
|
|
||||||
columnDialog,
|
|
||||||
indexDialog,
|
|
||||||
ddlDialog,
|
|
||||||
tableCreateDialog,
|
|
||||||
} = toRefs(state);
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
getTables();
|
getTables();
|
||||||
@@ -256,30 +248,33 @@ const getTables = async () => {
|
|||||||
* 选择导出数据库表
|
* 选择导出数据库表
|
||||||
*/
|
*/
|
||||||
const handleDumpTableSelectionChange = (vals: any) => {
|
const handleDumpTableSelectionChange = (vals: any) => {
|
||||||
state.dumpInfo.tables = vals.map((x: any) => x.tableName);
|
state.dumpInfo.tables = vals;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 数据库信息导出
|
* 数据库信息导出
|
||||||
*/
|
*/
|
||||||
const dump = (db: string) => {
|
const dump = (db: string) => {
|
||||||
isTrue(state.dumpInfo.tables.length > 0, '请选择要导出的表');
|
isTrue(state.dumpInfo.tables.length > 0, '请先选择要导出的表');
|
||||||
|
const tableNames = state.dumpInfo.tables.map((x: any) => x.tableName);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.setAttribute(
|
a.setAttribute(
|
||||||
'href',
|
'href',
|
||||||
`${config.baseApiUrl}/dbs/${props.dbId}/dump?db=${db}&type=${state.dumpInfo.type}&tables=${state.dumpInfo.tables.join(',')}&${joinClientParams()}`
|
`${config.baseApiUrl}/dbs/${props.dbId}/dump?db=${db}&type=${state.dumpInfo.type}&tables=${tableNames.join(',')}&${joinClientParams()}`
|
||||||
);
|
);
|
||||||
a.click();
|
a.click();
|
||||||
state.showDumpInfo = false;
|
state.dumpInfo.visible = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
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;
|
||||||
};
|
};
|
||||||
@@ -302,7 +297,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 as any });
|
||||||
state.ddlDialog.visible = true;
|
state.ddlDialog.visible = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -321,7 +317,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,
|
flowProcdef: props.flowProcdef,
|
||||||
runSuccessCallback: async () => {
|
runSuccessCallback: async () => {
|
||||||
await getTables();
|
await getTables();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ 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';
|
import { type RemovableRef, useLocalStorage } from '@vueuse/core';
|
||||||
|
import { DbGetDbNamesMode } from './enums';
|
||||||
|
|
||||||
const hintsStorage: RemovableRef<Map<string, any>> = useLocalStorage('db-table-hints', new Map());
|
const hintsStorage: RemovableRef<Map<string, any>> = useLocalStorage('db-table-hints', new Map());
|
||||||
const tableStorage: RemovableRef<Map<string, any>> = useLocalStorage('db-tables', new Map());
|
const tableStorage: RemovableRef<Map<string, any>> = useLocalStorage('db-tables', new Map());
|
||||||
@@ -41,9 +42,9 @@ export class DbInst {
|
|||||||
type: string;
|
type: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 流程定义key,若存在则需要审批执行
|
* 流程定义,若存在则需要审批执行
|
||||||
*/
|
*/
|
||||||
flowProcdefKey: string;
|
flowProcdef: any;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* dbName -> db
|
* dbName -> db
|
||||||
@@ -175,6 +176,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;
|
||||||
}
|
}
|
||||||
@@ -317,7 +321,7 @@ export class DbInst {
|
|||||||
let sql = `UPDATE ${schema}${this.wrapName(table)} SET `;
|
let sql = `UPDATE ${schema}${this.wrapName(table)} SET `;
|
||||||
// 主键列信息
|
// 主键列信息
|
||||||
const primaryKey = await this.loadTableColumn(dbName, table);
|
const primaryKey = await this.loadTableColumn(dbName, table);
|
||||||
let primaryKeyType = primaryKey.columnType;
|
let primaryKeyType = primaryKey.dataType;
|
||||||
let primaryKeyName = primaryKey.columnName;
|
let primaryKeyName = primaryKey.columnName;
|
||||||
let primaryKeyValue = rowData[primaryKeyName];
|
let primaryKeyValue = rowData[primaryKeyName];
|
||||||
const dialect = this.getDialect();
|
const dialect = this.getDialect();
|
||||||
@@ -325,7 +329,7 @@ export class DbInst {
|
|||||||
const v = columnValue[k];
|
const v = columnValue[k];
|
||||||
// 更新字段列信息
|
// 更新字段列信息
|
||||||
const updateColumn = await this.loadTableColumn(dbName, table, k);
|
const updateColumn = await this.loadTableColumn(dbName, table, k);
|
||||||
sql += ` ${this.wrapName(k)} = ${dialect.wrapValue(updateColumn.columnType, v)},`;
|
sql += ` ${this.wrapName(k)} = ${dialect.wrapValue(updateColumn.dataType, v)},`;
|
||||||
}
|
}
|
||||||
sql = sql.substring(0, sql.length - 1);
|
sql = sql.substring(0, sql.length - 1);
|
||||||
|
|
||||||
@@ -341,7 +345,7 @@ export class DbInst {
|
|||||||
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) => `${this.getDialect().wrapValue(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})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,7 +360,7 @@ export class DbInst {
|
|||||||
dbType: this.getDialect().getInfo().formatSqlDialect,
|
dbType: this.getDialect().getInfo().formatSqlDialect,
|
||||||
runSuccessCallback: successFunc,
|
runSuccessCallback: successFunc,
|
||||||
cancelCallback: cancelFunc,
|
cancelCallback: cancelFunc,
|
||||||
flowProcdefKey: this.flowProcdefKey,
|
flowProcdef: this.flowProcdef,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -380,6 +384,11 @@ export class DbInst {
|
|||||||
}
|
}
|
||||||
let dbInst = dbInstCache.get(inst.id);
|
let dbInst = dbInstCache.get(inst.id);
|
||||||
if (dbInst) {
|
if (dbInst) {
|
||||||
|
// 更新可能更改的流程定义
|
||||||
|
if (inst.flowProcdef !== undefined) {
|
||||||
|
dbInst.flowProcdef = inst.flowProcdef;
|
||||||
|
dbInstCache.set(dbInst.id, dbInst);
|
||||||
|
}
|
||||||
return dbInst;
|
return dbInst;
|
||||||
}
|
}
|
||||||
console.info(`new dbInst: ${inst.id}, tagPath: ${inst.tagPath}`);
|
console.info(`new dbInst: ${inst.id}, tagPath: ${inst.tagPath}`);
|
||||||
@@ -390,7 +399,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;
|
dbInst.flowProcdef = inst.flowProcdef;
|
||||||
|
|
||||||
dbInstCache.set(dbInst.id, dbInst);
|
dbInstCache.set(dbInst.id, dbInst);
|
||||||
return dbInst;
|
return dbInst;
|
||||||
@@ -426,7 +435,7 @@ export class DbInst {
|
|||||||
* @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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -466,6 +475,48 @@ 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.columnType = `${col.dataType}(${col.charMaxLength})`;
|
||||||
|
col.showLength = col.charMaxLength;
|
||||||
|
col.showScale = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (col.numPrecision > 0) {
|
||||||
|
if (col.numScale > 0) {
|
||||||
|
col.columnType = `${col.dataType}(${col.numPrecision},${col.numScale})`;
|
||||||
|
col.showScale = col.numScale;
|
||||||
|
} else {
|
||||||
|
col.columnType = `${col.dataType}(${col.numPrecision})`;
|
||||||
|
col.showScale = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
col.showLength = col.numPrecision;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
col.columnType = col.dataType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据数据库配置信息获取对应的库名列表
|
||||||
|
* @param db db配置信息
|
||||||
|
* @returns 库名列表
|
||||||
|
*/
|
||||||
|
static async getDbNames(db: any) {
|
||||||
|
if (db.getDatabaseMode == DbGetDbNamesMode.Assign.value) {
|
||||||
|
return db.database.split(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await dbApi.getDbNamesByAc.request({ authCertName: db.authCertName });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
DuplicateStrategy,
|
DuplicateStrategy,
|
||||||
EditorCompletion,
|
EditorCompletion,
|
||||||
EditorCompletionItem,
|
EditorCompletionItem,
|
||||||
|
QuoteEscape,
|
||||||
IndexDefinition,
|
IndexDefinition,
|
||||||
RowDefinition,
|
RowDefinition,
|
||||||
sqlColumnType,
|
sqlColumnType,
|
||||||
@@ -35,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: '' },
|
||||||
{
|
{
|
||||||
@@ -45,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' },
|
||||||
@@ -523,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)}'; `;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// 建表
|
// 建表
|
||||||
@@ -534,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;
|
||||||
@@ -569,7 +570,7 @@ class DMDialect implements DbDialect {
|
|||||||
changeData.add.forEach((a) => {
|
changeData.add.forEach((a) => {
|
||||||
modifySql += `ALTER TABLE ${dbTable} add COLUMN ${this.genColumnBasicSql(a)};`;
|
modifySql += `ALTER TABLE ${dbTable} add COLUMN ${this.genColumnBasicSql(a)};`;
|
||||||
if (a.remark) {
|
if (a.remark) {
|
||||||
commentSql += `COMMENT ON COLUMN ${dbTable}.${this.quoteIdentifier(a.name)} IS '${a.remark}';`;
|
commentSql += `COMMENT ON COLUMN ${dbTable}.${this.quoteIdentifier(a.name)} IS '${QuoteEscape(a.remark)}';`;
|
||||||
}
|
}
|
||||||
if (a.pri) {
|
if (a.pri) {
|
||||||
priArr.add(`"${a.name}"`);
|
priArr.add(`"${a.name}"`);
|
||||||
@@ -579,7 +580,7 @@ class DMDialect implements DbDialect {
|
|||||||
|
|
||||||
if (changeData.upd.length > 0) {
|
if (changeData.upd.length > 0) {
|
||||||
changeData.upd.forEach((a) => {
|
changeData.upd.forEach((a) => {
|
||||||
let cmtSql = `COMMENT ON COLUMN ${dbTable}.${this.quoteIdentifier(a.name)} IS '${a.remark}';`;
|
let cmtSql = `COMMENT ON COLUMN ${dbTable}.${this.quoteIdentifier(a.name)} IS '${QuoteEscape(a.remark)}';`;
|
||||||
if (a.remark && a.oldName === a.name) {
|
if (a.remark && a.oldName === a.name) {
|
||||||
commentSql += cmtSql;
|
commentSql += cmtSql;
|
||||||
}
|
}
|
||||||
@@ -675,7 +676,7 @@ class DMDialect implements DbDialect {
|
|||||||
}
|
}
|
||||||
if (tableData.oldTableComment !== tableData.tableComment) {
|
if (tableData.oldTableComment !== tableData.tableComment) {
|
||||||
let baseTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableData.tableName)}`;
|
let baseTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableData.tableName)}`;
|
||||||
sql += `COMMENT ON TABLE ${baseTable} IS '${tableData.tableComment}';`;
|
sql += `COMMENT ON TABLE ${baseTable} IS '${QuoteEscape(tableData.tableComment)}';`;
|
||||||
}
|
}
|
||||||
return sql;
|
return sql;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -262,6 +262,18 @@ export const getDbDialect = (dbType?: string): DbDialect => {
|
|||||||
return dbType2DialectMap.get(dbType!) || mysqlDialect;
|
return dbType2DialectMap.get(dbType!) || mysqlDialect;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 引号转义,多用于sql注释转义,防止拼接sql报错,如: comment xx is '注''释' 最终注释文本为: 注'释
|
||||||
|
* @author liuzongyang
|
||||||
|
* @since 2024/3/22 08:23
|
||||||
|
*/
|
||||||
|
export const QuoteEscape = (str: string): string => {
|
||||||
|
if (!str) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return str.replace(/'/g, "''");
|
||||||
|
};
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
console.log('init register db dialect');
|
console.log('init register db dialect');
|
||||||
registerDbDialect(DbType.mysql, mysqlDialect);
|
registerDbDialect(DbType.mysql, mysqlDialect);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
DuplicateStrategy,
|
DuplicateStrategy,
|
||||||
EditorCompletion,
|
EditorCompletion,
|
||||||
EditorCompletionItem,
|
EditorCompletionItem,
|
||||||
|
QuoteEscape,
|
||||||
IndexDefinition,
|
IndexDefinition,
|
||||||
RowDefinition,
|
RowDefinition,
|
||||||
} from './index';
|
} from './index';
|
||||||
@@ -225,7 +226,7 @@ class MssqlDialect implements DbDialect {
|
|||||||
item.name && fields.push(this.genColumnBasicSql(item));
|
item.name && fields.push(this.genColumnBasicSql(item));
|
||||||
item.remark &&
|
item.remark &&
|
||||||
fieldComments.push(
|
fieldComments.push(
|
||||||
`EXECUTE sp_addextendedproperty N'MS_Description', N'${item.remark}', N'SCHEMA', N'${schema}', N'TABLE', N'${data.tableName}', N'COLUMN', N'${item.name}'`
|
`EXECUTE sp_addextendedproperty N'MS_Description', N'${QuoteEscape(item.remark)}', N'SCHEMA', N'${schema}', N'TABLE', N'${data.tableName}', N'COLUMN', N'${item.name}'`
|
||||||
);
|
);
|
||||||
if (item.pri) {
|
if (item.pri) {
|
||||||
pks.push(`${this.quoteIdentifier(item.name)}`);
|
pks.push(`${this.quoteIdentifier(item.name)}`);
|
||||||
@@ -244,7 +245,7 @@ class MssqlDialect implements DbDialect {
|
|||||||
|
|
||||||
// 表注释
|
// 表注释
|
||||||
if (data.tableComment) {
|
if (data.tableComment) {
|
||||||
createTable += ` EXECUTE sp_addextendedproperty N'MS_Description', N'${data.tableComment}', N'SCHEMA', N'${schema}', N'TABLE', N'${data.tableName}';`;
|
createTable += ` EXECUTE sp_addextendedproperty N'MS_Description', N'${QuoteEscape(data.tableComment)}', N'SCHEMA', N'${schema}', N'TABLE', N'${data.tableName}';`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return createTable + createIndexSql + fieldComments.join(';');
|
return createTable + createIndexSql + fieldComments.join(';');
|
||||||
@@ -268,7 +269,7 @@ class MssqlDialect implements DbDialect {
|
|||||||
sql.push(` CREATE ${a.unique ? 'UNIQUE' : ''} NONCLUSTERED INDEX ${this.quoteIdentifier(a.indexName)} on ${baseTable} (${columnNames.join(',')})`);
|
sql.push(` CREATE ${a.unique ? 'UNIQUE' : ''} NONCLUSTERED INDEX ${this.quoteIdentifier(a.indexName)} on ${baseTable} (${columnNames.join(',')})`);
|
||||||
if (a.indexComment) {
|
if (a.indexComment) {
|
||||||
indexComment.push(
|
indexComment.push(
|
||||||
`EXECUTE sp_addextendedproperty N'MS_Description', N'${a.indexComment}', N'SCHEMA', N'${schema}', N'TABLE', N'${data.tableName}', N'INDEX', N'${a.indexName}'`
|
`EXECUTE sp_addextendedproperty N'MS_Description', N'${QuoteEscape(a.indexComment)}', N'SCHEMA', N'${schema}', N'TABLE', N'${data.tableName}', N'INDEX', N'${a.indexName}'`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -303,10 +304,10 @@ class MssqlDialect implements DbDialect {
|
|||||||
}
|
}
|
||||||
if (changeData.add.length > 0) {
|
if (changeData.add.length > 0) {
|
||||||
changeData.add.forEach((a) => {
|
changeData.add.forEach((a) => {
|
||||||
addArr.push(` ALTER TABLE ${baseTable} ADD COLUMN ${this.genColumnBasicSql(a)}`);
|
addArr.push(` ALTER TABLE ${baseTable} ADD ${this.genColumnBasicSql(a)}`);
|
||||||
if (a.remark) {
|
if (a.remark) {
|
||||||
addCommentArr.push(
|
addCommentArr.push(
|
||||||
`EXECUTE sp_addextendedproperty N'MS_Description', N'${a.remark}', N'SCHEMA', N'${schema}', N'TABLE', N'${tableName}', N'COLUMN', N'${a.name}'`
|
`EXECUTE sp_addextendedproperty N'MS_Description', N'${QuoteEscape(a.remark)}', N'SCHEMA', N'${schema}', N'TABLE', N'${tableName}', N'COLUMN', N'${a.name}'`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -315,7 +316,7 @@ class MssqlDialect implements DbDialect {
|
|||||||
if (changeData.upd.length > 0) {
|
if (changeData.upd.length > 0) {
|
||||||
changeData.upd.forEach((a) => {
|
changeData.upd.forEach((a) => {
|
||||||
if (a.oldName && a.name !== a.oldName) {
|
if (a.oldName && a.name !== a.oldName) {
|
||||||
renameArr.push(` EXEC sp_rename '${baseTable}.${this.quoteIdentifier(a.oldName)}', '${a.name}', 'COLUMN' `);
|
renameArr.push(` EXEC sp_rename '${baseTable}.${this.quoteIdentifier(a.oldName)}', '${QuoteEscape(a.name)}', 'COLUMN' `);
|
||||||
} else {
|
} else {
|
||||||
updArr.push(` ALTER TABLE ${baseTable} ALTER COLUMN ${this.genColumnBasicSql(a)} `);
|
updArr.push(` ALTER TABLE ${baseTable} ALTER COLUMN ${this.genColumnBasicSql(a)} `);
|
||||||
}
|
}
|
||||||
@@ -325,13 +326,13 @@ class MssqlDialect implements DbDialect {
|
|||||||
'TABLE', N'${tableName}',
|
'TABLE', N'${tableName}',
|
||||||
'COLUMN', N'${a.name}')) > 0)
|
'COLUMN', N'${a.name}')) > 0)
|
||||||
EXEC sp_updateextendedproperty
|
EXEC sp_updateextendedproperty
|
||||||
'MS_Description', N'${a.remark}',
|
'MS_Description', N'${QuoteEscape(a.remark)}',
|
||||||
'SCHEMA', N'${schema}',
|
'SCHEMA', N'${schema}',
|
||||||
'TABLE', N'${tableName}',
|
'TABLE', N'${tableName}',
|
||||||
'COLUMN', N'${a.name}'
|
'COLUMN', N'${a.name}'
|
||||||
ELSE
|
ELSE
|
||||||
EXEC sp_addextendedproperty
|
EXEC sp_addextendedproperty
|
||||||
'MS_Description', N'${a.remark}',
|
'MS_Description', N'${QuoteEscape(a.remark)}',
|
||||||
'SCHEMA', N'${schema}',
|
'SCHEMA', N'${schema}',
|
||||||
'TABLE', N'${tableName}',
|
'TABLE', N'${tableName}',
|
||||||
'COLUMN',N'${a.name}'`);
|
'COLUMN',N'${a.name}'`);
|
||||||
@@ -367,7 +368,7 @@ ELSE
|
|||||||
);
|
);
|
||||||
if (a.indexComment) {
|
if (a.indexComment) {
|
||||||
commentArr.push(
|
commentArr.push(
|
||||||
` EXEC sp_addextendedproperty N'MS_Description', N'${a.indexComment}', N'SCHEMA', N'${schema}', N'TABLE', N'${tableName}', N'INDEX', N'${a.indexName}' `
|
` EXEC sp_addextendedproperty N'MS_Description', N'${QuoteEscape(a.indexComment)}', N'SCHEMA', N'${schema}', N'TABLE', N'${tableName}', N'INDEX', N'${a.indexName}' `
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -413,7 +414,7 @@ ELSE
|
|||||||
|
|
||||||
if (tableData.oldTableComment !== tableData.tableComment) {
|
if (tableData.oldTableComment !== tableData.tableComment) {
|
||||||
// 转义注释中的单引号和换行符
|
// 转义注释中的单引号和换行符
|
||||||
let tableComment = tableData.tableComment.replaceAll(/'/g, '').replaceAll(/[\r\n]/g, ' ');
|
let tableComment = tableData.tableComment.replaceAll(/'/g, "'").replaceAll(/[\r\n]/g, ' ');
|
||||||
sql += `IF ((SELECT COUNT(*) FROM fn_listextendedproperty('MS_Description',
|
sql += `IF ((SELECT COUNT(*) FROM fn_listextendedproperty('MS_Description',
|
||||||
'SCHEMA', N'${schema}',
|
'SCHEMA', N'${schema}',
|
||||||
'TABLE', N'${tableData.tableName}', NULL, NULL)) > 0)
|
'TABLE', N'${tableData.tableName}', NULL, NULL)) > 0)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
DuplicateStrategy,
|
DuplicateStrategy,
|
||||||
EditorCompletion,
|
EditorCompletion,
|
||||||
EditorCompletionItem,
|
EditorCompletionItem,
|
||||||
|
QuoteEscape,
|
||||||
IndexDefinition,
|
IndexDefinition,
|
||||||
RowDefinition,
|
RowDefinition,
|
||||||
} from './index';
|
} from './index';
|
||||||
@@ -208,7 +209,7 @@ class MysqlDialect implements DbDialect {
|
|||||||
let onUpdate = 'update_time' === cl.name ? ' ON UPDATE CURRENT_TIMESTAMP ' : '';
|
let onUpdate = 'update_time' === cl.name ? ' ON UPDATE CURRENT_TIMESTAMP ' : '';
|
||||||
return ` ${this.quoteIdentifier(cl.name)} ${cl.type}${length} ${cl.notNull ? 'NOT NULL' : 'NULL'} ${
|
return ` ${this.quoteIdentifier(cl.name)} ${cl.type}${length} ${cl.notNull ? 'NOT NULL' : 'NULL'} ${
|
||||||
cl.auto_increment ? 'AUTO_INCREMENT' : ''
|
cl.auto_increment ? 'AUTO_INCREMENT' : ''
|
||||||
} ${defVal} ${onUpdate} comment '${cl.remark || ''}' `;
|
} ${defVal} ${onUpdate} comment '${QuoteEscape(cl.remark)}' `;
|
||||||
}
|
}
|
||||||
getCreateTableSql(data: any): string {
|
getCreateTableSql(data: any): string {
|
||||||
// 创建表结构
|
// 创建表结构
|
||||||
@@ -224,14 +225,14 @@ class MysqlDialect implements DbDialect {
|
|||||||
return `CREATE TABLE ${data.tableName}
|
return `CREATE TABLE ${data.tableName}
|
||||||
( ${fields.join(',')}
|
( ${fields.join(',')}
|
||||||
${pks ? `, PRIMARY KEY (${pks.join(',')})` : ''}
|
${pks ? `, PRIMARY KEY (${pks.join(',')})` : ''}
|
||||||
) COMMENT='${data.tableComment}';`;
|
) COMMENT='${QuoteEscape(data.tableComment)}';`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getCreateIndexSql(data: any): string {
|
getCreateIndexSql(data: any): string {
|
||||||
// 创建索引
|
// 创建索引
|
||||||
let sql = `ALTER TABLE ${data.tableName}`;
|
let sql = `ALTER TABLE ${data.tableName}`;
|
||||||
data.indexs.res.forEach((a: any) => {
|
data.indexs.res.forEach((a: any) => {
|
||||||
sql += ` ADD ${a.unique ? 'UNIQUE' : ''} INDEX ${a.indexName}(${a.columnNames.join(',')}) USING ${a.indexType} COMMENT '${a.indexComment}',`;
|
sql += ` ADD ${a.unique ? 'UNIQUE' : ''} INDEX ${a.indexName}(${a.columnNames.join(',')}) USING ${a.indexType} COMMENT '${QuoteEscape(a.indexComment)}',`;
|
||||||
});
|
});
|
||||||
return sql.substring(0, sql.length - 1) + ';';
|
return sql.substring(0, sql.length - 1) + ';';
|
||||||
}
|
}
|
||||||
@@ -312,9 +313,9 @@ class MysqlDialect implements DbDialect {
|
|||||||
sql += ',';
|
sql += ',';
|
||||||
}
|
}
|
||||||
addIndexs.forEach((a) => {
|
addIndexs.forEach((a) => {
|
||||||
sql += ` ADD ${a.unique ? 'UNIQUE' : ''} INDEX ${a.indexName}(${a.columnNames.join(',')}) USING ${a.indexType} COMMENT '${
|
sql += ` ADD ${a.unique ? 'UNIQUE' : ''} INDEX ${a.indexName}(${a.columnNames.join(',')}) USING ${a.indexType} COMMENT '${QuoteEscape(
|
||||||
a.indexComment
|
a.indexComment
|
||||||
}',`;
|
)}',`;
|
||||||
});
|
});
|
||||||
sql = sql.substring(0, sql.length - 1);
|
sql = sql.substring(0, sql.length - 1);
|
||||||
}
|
}
|
||||||
@@ -326,7 +327,7 @@ class MysqlDialect implements DbDialect {
|
|||||||
getModifyTableInfoSql(tableData: any): string {
|
getModifyTableInfoSql(tableData: any): string {
|
||||||
let sql = '';
|
let sql = '';
|
||||||
if (tableData.tableComment !== tableData.oldTableComment) {
|
if (tableData.tableComment !== tableData.oldTableComment) {
|
||||||
sql += `ALTER TABLE ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(tableData.oldTableName)} COMMENT '${tableData.tableComment}';`;
|
sql += `ALTER TABLE ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(tableData.oldTableName)} COMMENT '${QuoteEscape(tableData.tableComment)}';`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tableData.tableName !== tableData.oldTableName) {
|
if (tableData.tableName !== tableData.oldTableName) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
DuplicateStrategy,
|
DuplicateStrategy,
|
||||||
EditorCompletion,
|
EditorCompletion,
|
||||||
EditorCompletionItem,
|
EditorCompletionItem,
|
||||||
|
QuoteEscape,
|
||||||
IndexDefinition,
|
IndexDefinition,
|
||||||
RowDefinition,
|
RowDefinition,
|
||||||
sqlColumnType,
|
sqlColumnType,
|
||||||
@@ -296,7 +297,12 @@ class OracleDialect implements DbDialect {
|
|||||||
let length = this.getTypeLengthSql(cl);
|
let length = this.getTypeLengthSql(cl);
|
||||||
// 默认值
|
// 默认值
|
||||||
let defVal = this.getDefaultValueSql(cl);
|
let defVal = this.getDefaultValueSql(cl);
|
||||||
let incr = cl.auto_increment && create ? 'generated by default as IDENTITY' : '';
|
let incr = '';
|
||||||
|
if (cl.auto_increment && create) {
|
||||||
|
cl.type = 'number';
|
||||||
|
length = '';
|
||||||
|
incr = 'generated by default as IDENTITY';
|
||||||
|
}
|
||||||
// 如果有原名以原名为准
|
// 如果有原名以原名为准
|
||||||
let name = cl.oldName && cl.name !== cl.oldName ? cl.oldName : cl.name;
|
let name = cl.oldName && cl.name !== cl.oldName ? cl.oldName : cl.name;
|
||||||
let baseSql = ` ${this.quoteIdentifier(name)} ${cl.type}${length} ${incr}`;
|
let baseSql = ` ${this.quoteIdentifier(name)} ${cl.type}${length} ${incr}`;
|
||||||
@@ -319,7 +325,7 @@ class OracleDialect implements DbDialect {
|
|||||||
item.name && fields.push(this.genColumnBasicSql(item, true));
|
item.name && fields.push(this.genColumnBasicSql(item, true));
|
||||||
// 列注释
|
// 列注释
|
||||||
if (item.remark) {
|
if (item.remark) {
|
||||||
columCommentSql += ` COMMENT ON COLUMN ${dbTable}.${this.quoteIdentifier(item.name)} is '${item.remark}'; `;
|
columCommentSql += ` COMMENT ON COLUMN ${dbTable}.${this.quoteIdentifier(item.name)} is '${QuoteEscape(item.remark)}'; `;
|
||||||
}
|
}
|
||||||
// 主键
|
// 主键
|
||||||
if (item.pri) {
|
if (item.pri) {
|
||||||
@@ -329,13 +335,13 @@ class OracleDialect implements DbDialect {
|
|||||||
// 主键语句
|
// 主键语句
|
||||||
let prisql = '';
|
let prisql = '';
|
||||||
if (pris.length > 0) {
|
if (pris.length > 0) {
|
||||||
prisql = ` CONSTRAINT "PK_${data.tableName}" PRIMARY KEY (${pris.join(',')});`;
|
prisql = ` PRIMARY KEY (${pris.join(',')})`;
|
||||||
}
|
}
|
||||||
// 建表
|
// 建表
|
||||||
createSql = `CREATE TABLE ${dbTable} ( ${fields.join(',')} ) ${prisql ? ',' + prisql : ''};`;
|
createSql = `CREATE TABLE ${dbTable} ( ${fields.join(',')} ${prisql ? ',' + prisql : ''} ) ;`;
|
||||||
// 表注释
|
// 表注释
|
||||||
if (data.tableComment) {
|
if (data.tableComment) {
|
||||||
tableCommentSql = ` COMMENT ON TABLE ${dbTable} is '${data.tableComment}'; `;
|
tableCommentSql = ` COMMENT ON TABLE ${dbTable} is '${QuoteEscape(data.tableComment)}'; `;
|
||||||
}
|
}
|
||||||
|
|
||||||
return createSql + tableCommentSql + columCommentSql;
|
return createSql + tableCommentSql + columCommentSql;
|
||||||
@@ -374,7 +380,7 @@ class OracleDialect implements DbDialect {
|
|||||||
|
|
||||||
if (changeData.upd.length > 0) {
|
if (changeData.upd.length > 0) {
|
||||||
changeData.upd.forEach((a) => {
|
changeData.upd.forEach((a) => {
|
||||||
let commentSql = `COMMENT ON COLUMN ${dbTable}.${this.quoteIdentifier(a.name)} IS '${a.remark}'`;
|
let commentSql = `COMMENT ON COLUMN ${dbTable}.${this.quoteIdentifier(a.name)} IS '${QuoteEscape(a.remark)}'`;
|
||||||
if (a.remark && a.oldName === a.name) {
|
if (a.remark && a.oldName === a.name) {
|
||||||
commentArr.push(commentSql);
|
commentArr.push(commentSql);
|
||||||
}
|
}
|
||||||
@@ -396,7 +402,7 @@ class OracleDialect implements DbDialect {
|
|||||||
changeData.add.forEach((a) => {
|
changeData.add.forEach((a) => {
|
||||||
modifyArr.push(` ADD (${this.genColumnBasicSql(a, false)})`);
|
modifyArr.push(` ADD (${this.genColumnBasicSql(a, false)})`);
|
||||||
if (a.remark) {
|
if (a.remark) {
|
||||||
commentArr.push(`COMMENT ON COLUMN ${dbTable}.${this.quoteIdentifier(a.name)} is '${a.remark}'`);
|
commentArr.push(`COMMENT ON COLUMN ${dbTable}.${this.quoteIdentifier(a.name)} is '${QuoteEscape(a.remark)}'`);
|
||||||
}
|
}
|
||||||
if (a.pri) {
|
if (a.pri) {
|
||||||
priArr.add(`"${a.name}"`);
|
priArr.add(`"${a.name}"`);
|
||||||
@@ -481,7 +487,7 @@ class OracleDialect implements DbDialect {
|
|||||||
let sql = '';
|
let sql = '';
|
||||||
if (tableData.tableComment != tableData.oldTableComment) {
|
if (tableData.tableComment != tableData.oldTableComment) {
|
||||||
let dbTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableData.oldTableName)}`;
|
let dbTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableData.oldTableName)}`;
|
||||||
sql = `COMMENT ON TABLE ${dbTable} is '${tableData.tableComment}';`;
|
sql = `COMMENT ON TABLE ${dbTable} is '${QuoteEscape(tableData.tableComment)}';`;
|
||||||
}
|
}
|
||||||
if (tableData.tableName != tableData.oldTableName) {
|
if (tableData.tableName != tableData.oldTableName) {
|
||||||
let dbTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableData.oldTableName)}`;
|
let dbTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableData.oldTableName)}`;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
DuplicateStrategy,
|
DuplicateStrategy,
|
||||||
EditorCompletion,
|
EditorCompletion,
|
||||||
EditorCompletionItem,
|
EditorCompletionItem,
|
||||||
|
QuoteEscape,
|
||||||
IndexDefinition,
|
IndexDefinition,
|
||||||
RowDefinition,
|
RowDefinition,
|
||||||
sqlColumnType,
|
sqlColumnType,
|
||||||
@@ -283,7 +284,7 @@ class PostgresqlDialect 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)}'; `;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// 建表
|
// 建表
|
||||||
@@ -294,7 +295,7 @@ class PostgresqlDialect 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;
|
||||||
@@ -312,7 +313,7 @@ class PostgresqlDialect implements DbDialect {
|
|||||||
let colArr = a.columnNames.map((a: string) => `${this.quoteIdentifier(a)}`);
|
let colArr = a.columnNames.map((a: string) => `${this.quoteIdentifier(a)}`);
|
||||||
sql.push(`CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${this.quoteIdentifier(a.indexName)} on ${dbTable} (${colArr.join(',')})`);
|
sql.push(`CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${this.quoteIdentifier(a.indexName)} on ${dbTable} (${colArr.join(',')})`);
|
||||||
if (a.indexComment) {
|
if (a.indexComment) {
|
||||||
sql.push(`COMMENT ON INDEX ${schema}.${this.quoteIdentifier(a.indexName)} IS '${a.indexComment}'`);
|
sql.push(`COMMENT ON INDEX ${schema}.${this.quoteIdentifier(a.indexName)} IS '${QuoteEscape(a.indexComment)}'`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return sql.join(';');
|
return sql.join(';');
|
||||||
@@ -334,14 +335,14 @@ class PostgresqlDialect implements DbDialect {
|
|||||||
changeData.add.forEach((a) => {
|
changeData.add.forEach((a) => {
|
||||||
modifySql += `alter table ${dbTable} add ${this.genColumnBasicSql(a)};`;
|
modifySql += `alter table ${dbTable} add ${this.genColumnBasicSql(a)};`;
|
||||||
if (a.remark) {
|
if (a.remark) {
|
||||||
commentSql += `comment on column ${dbTable}.${this.quoteIdentifier(a.name)} is '${a.remark}';`;
|
commentSql += `comment on column ${dbTable}.${this.quoteIdentifier(a.name)} is '${QuoteEscape(a.remark)}';`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changeData.upd.length > 0) {
|
if (changeData.upd.length > 0) {
|
||||||
changeData.upd.forEach((a) => {
|
changeData.upd.forEach((a) => {
|
||||||
let cmtSql = `comment on column ${dbTable}.${this.quoteIdentifier(a.name)} is '${a.remark}';`;
|
let cmtSql = `comment on column ${dbTable}.${this.quoteIdentifier(a.name)} is '${QuoteEscape(a.remark)}';`;
|
||||||
if (a.remark && a.oldName === a.name) {
|
if (a.remark && a.oldName === a.name) {
|
||||||
commentSql += cmtSql;
|
commentSql += cmtSql;
|
||||||
}
|
}
|
||||||
@@ -412,7 +413,7 @@ class PostgresqlDialect implements DbDialect {
|
|||||||
let colArr = a.columnNames.map((a: string) => `${this.quoteIdentifier(a)}`);
|
let colArr = a.columnNames.map((a: string) => `${this.quoteIdentifier(a)}`);
|
||||||
sql.push(`CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${this.quoteIdentifier(a.indexName)} on ${dbTable} (${colArr.join(',')})`);
|
sql.push(`CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${this.quoteIdentifier(a.indexName)} on ${dbTable} (${colArr.join(',')})`);
|
||||||
if (a.indexComment) {
|
if (a.indexComment) {
|
||||||
sql.push(`COMMENT ON INDEX ${schema}.${this.quoteIdentifier(a.indexName)} IS '${a.indexComment}'`);
|
sql.push(`COMMENT ON INDEX ${schema}.${this.quoteIdentifier(a.indexName)} IS '${QuoteEscape(a.indexComment)}'`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -428,7 +429,7 @@ class PostgresqlDialect implements DbDialect {
|
|||||||
let sql = '';
|
let sql = '';
|
||||||
if (tableData.tableComment != tableData.oldTableComment) {
|
if (tableData.tableComment != tableData.oldTableComment) {
|
||||||
let dbTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableData.oldTableName)}`;
|
let dbTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableData.oldTableName)}`;
|
||||||
sql = `COMMENT ON TABLE ${dbTable} is '${tableData.tableComment}';`;
|
sql = `COMMENT ON TABLE ${dbTable} is '${QuoteEscape(tableData.tableComment)}';`;
|
||||||
}
|
}
|
||||||
if (tableData.tableName != tableData.oldTableName) {
|
if (tableData.tableName != tableData.oldTableName) {
|
||||||
let dbTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableData.oldTableName)}`;
|
let dbTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableData.oldTableName)}`;
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { EnumValue } from '@/common/Enum';
|
import { EnumValue } from '@/common/Enum';
|
||||||
|
|
||||||
|
export const DbGetDbNamesMode = {
|
||||||
|
Auto: EnumValue.of(-1, '实时获取').setTagType('warning'),
|
||||||
|
Assign: EnumValue.of(1, '指定库名').setTagType('primary'),
|
||||||
|
};
|
||||||
|
|
||||||
// 数据库sql执行类型
|
// 数据库sql执行类型
|
||||||
export const DbSqlExecTypeEnum = {
|
export const DbSqlExecTypeEnum = {
|
||||||
Update: EnumValue.of(1, 'UPDATE').setTagColor('#E4F5EB'),
|
Update: EnumValue.of(1, 'UPDATE').setTagColor('#E4F5EB'),
|
||||||
@@ -30,3 +35,10 @@ export const DbDataSyncRunningStateEnum = {
|
|||||||
Wait: EnumValue.of(2, '待运行').setTagType('primary'),
|
Wait: EnumValue.of(2, '待运行').setTagType('primary'),
|
||||||
Fail: EnumValue.of(3, '已停止').setTagType('danger'),
|
Fail: EnumValue.of(3, '已停止').setTagType('danger'),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const DbTransferRunningStateEnum = {
|
||||||
|
Success: EnumValue.of(2, '成功').setTagType('success'),
|
||||||
|
Wait: EnumValue.of(1, '执行中').setTagType('primary'),
|
||||||
|
Fail: EnumValue.of(-1, '失败').setTagType('danger'),
|
||||||
|
Stop: EnumValue.of(-2, '手动终止').setTagType('warning'),
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,90 +1,97 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<el-dialog :title="title" v-model="dialogVisible" :close-on-click-modal="false" :destroy-on-close="true" :before-close="cancel" width="650px">
|
<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="machineForm" :rules="rules" label-width="auto">
|
<el-form :model="form" ref="machineForm" :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 ref="tagSelectRef" prop="tagCodePaths" label="标签">
|
||||||
<el-form-item ref="tagSelectRef" prop="tagId" label="标签">
|
<tag-tree-select
|
||||||
<tag-tree-select
|
multiple
|
||||||
multiple
|
@change-tag="
|
||||||
@change-tag="
|
(paths) => {
|
||||||
(tagIds) => {
|
form.tagCodePaths = paths;
|
||||||
form.tagId = tagIds;
|
tagSelectRef.validate();
|
||||||
tagSelectRef.validate();
|
}
|
||||||
}
|
"
|
||||||
"
|
:select-tags="form.tagCodePaths"
|
||||||
:tag-path="form.tagPath"
|
style="width: 100%"
|
||||||
:select-tags="form.tagId"
|
/>
|
||||||
style="width: 100%"
|
</el-form-item>
|
||||||
/>
|
<el-form-item prop="code" label="编号" required>
|
||||||
</el-form-item>
|
<el-input
|
||||||
<el-form-item prop="name" label="名称" required>
|
:disabled="form.id"
|
||||||
<el-input v-model.trim="form.name" placeholder="请输入机器别名" auto-complete="off"></el-input>
|
v-model.trim="form.code"
|
||||||
</el-form-item>
|
placeholder="请输入编号 (大小写字母、数字、_-.:), 不可修改"
|
||||||
<el-form-item prop="ip" label="ip" required>
|
auto-complete="off"
|
||||||
<el-col :span="18">
|
></el-input>
|
||||||
<el-input :disabled="form.id" v-model.trim="form.ip" placeholder="主机ip" auto-complete="off"> </el-input>
|
</el-form-item>
|
||||||
</el-col>
|
<el-form-item prop="name" label="名称" required>
|
||||||
<el-col style="text-align: center" :span="1">:</el-col>
|
<el-input v-model.trim="form.name" placeholder="请输入机器别名" auto-complete="off"></el-input>
|
||||||
<el-col :span="5">
|
</el-form-item>
|
||||||
<el-input type="number" v-model.number="form.port" placeholder="端口"></el-input>
|
<el-form-item prop="protocol" label="协议" required>
|
||||||
</el-col>
|
<el-radio-group v-model="form.protocol" @change="handleChangeProtocol">
|
||||||
</el-form-item>
|
<el-radio v-for="item in MachineProtocolEnum" :key="item.value" :label="item.label" :value="item.value"></el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="ip" label="ip" required>
|
||||||
|
<el-col :span="18">
|
||||||
|
<el-input v-model.trim="form.ip" placeholder="主机ip" auto-complete="off"> </el-input>
|
||||||
|
</el-col>
|
||||||
|
<el-col style="text-align: center" :span="1">:</el-col>
|
||||||
|
<el-col :span="5">
|
||||||
|
<el-input type="number" v-model.number="form.port" placeholder="端口"></el-input>
|
||||||
|
</el-col>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item prop="username" label="用户名">
|
<el-form-item prop="remark" label="备注">
|
||||||
<el-input v-model.trim="form.username" placeholder="请输授权用户名" autocomplete="new-password"> </el-input>
|
<el-input type="textarea" v-model="form.remark"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="认证方式" required>
|
<el-divider content-position="left">账号</el-divider>
|
||||||
<el-select @change="changeAuthMethod" style="width: 100%" v-model="state.authType" placeholder="请选认证方式">
|
<div>
|
||||||
<el-option key="1" label="密码" :value="1"> </el-option>
|
<ResourceAuthCertTableEdit
|
||||||
<el-option key="2" label="授权凭证" :value="2"> </el-option>
|
v-model="form.authCerts"
|
||||||
</el-select>
|
:resource-code="form.code"
|
||||||
</el-form-item>
|
:resource-type="TagResourceTypeEnum.Machine.value"
|
||||||
<el-form-item v-if="state.authType == 1" prop="password" label="密码">
|
:test-conn-btn-loading="testConnBtnLoading"
|
||||||
<el-input type="password" show-password v-model.trim="form.password" placeholder="请输入密码" autocomplete="new-password">
|
@test-conn="testConn"
|
||||||
</el-input>
|
/>
|
||||||
</el-form-item>
|
</div>
|
||||||
|
|
||||||
<el-form-item v-if="state.authType == 2" prop="authCertId" label="授权凭证" required>
|
<el-divider content-position="left">其他</el-divider>
|
||||||
<auth-cert-select ref="authCertSelectRef" v-model="form.authCertId" />
|
<el-form-item prop="enableRecorder" label="终端回放">
|
||||||
</el-form-item>
|
<el-checkbox v-model="form.enableRecorder" :true-value="1" :false-value="-1"></el-checkbox>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item prop="remark" label="备注">
|
<el-form-item prop="sshTunnelMachineId" label="SSH隧道">
|
||||||
<el-input type="textarea" v-model="form.remark"></el-input>
|
<ssh-tunnel-select v-model="form.sshTunnelMachineId" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-tab-pane>
|
|
||||||
|
|
||||||
<el-tab-pane label="其他配置" name="other">
|
|
||||||
<el-form-item prop="enableRecorder" label="终端回放">
|
|
||||||
<el-checkbox v-model="form.enableRecorder" :true-value="1" :false-value="-1"></el-checkbox>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item prop="sshTunnelMachineId" label="SSH隧道">
|
|
||||||
<ssh-tunnel-select v-model="form.sshTunnelMachineId" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-tab-pane>
|
|
||||||
</el-tabs>
|
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div>
|
<div>
|
||||||
<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>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { toRefs, reactive, watch, ref } from 'vue';
|
import { reactive, ref, toRefs, watchEffect } from 'vue';
|
||||||
import { machineApi } from './api';
|
import { machineApi } 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 ResourceAuthCertTableEdit from '../component/ResourceAuthCertTableEdit.vue';
|
||||||
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
|
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
|
||||||
import AuthCertSelect from './authcert/AuthCertSelect.vue';
|
import { MachineProtocolEnum } from './enums';
|
||||||
|
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||||
|
import { ResourceCodePattern } from '@/common/pattern';
|
||||||
|
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
visible: {
|
visible: {
|
||||||
@@ -99,16 +106,30 @@ const props = defineProps({
|
|||||||
});
|
});
|
||||||
|
|
||||||
//定义事件
|
//定义事件
|
||||||
const emit = defineEmits(['update:visible', 'cancel', 'val-change']);
|
const emit = defineEmits(['cancel', 'val-change']);
|
||||||
|
|
||||||
|
const dialogVisible = defineModel<boolean>('visible', { default: false });
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
tagId: [
|
tagCodePaths: [
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: '请选择标签',
|
message: '请选择标签',
|
||||||
trigger: ['change'],
|
trigger: ['change'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
code: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请输入编码',
|
||||||
|
trigger: ['change', 'blur'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: ResourceCodePattern.pattern,
|
||||||
|
message: ResourceCodePattern.message,
|
||||||
|
trigger: ['blur'],
|
||||||
|
},
|
||||||
|
],
|
||||||
name: [
|
name: [
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
@@ -116,137 +137,121 @@ const rules = {
|
|||||||
trigger: ['change', 'blur'],
|
trigger: ['change', 'blur'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
protocol: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请选择机器类型',
|
||||||
|
trigger: ['change', 'blur'],
|
||||||
|
},
|
||||||
|
],
|
||||||
ip: [
|
ip: [
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: '请输入主机ip和端口',
|
message: '请输入主机ip和端口',
|
||||||
trigger: ['change', 'blur'],
|
trigger: ['blur'],
|
||||||
},
|
|
||||||
],
|
|
||||||
authCertId: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: '请选择授权凭证',
|
|
||||||
trigger: ['change', 'blur'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
username: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: '请输入授权用户名',
|
|
||||||
trigger: ['change', 'blur'],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const machineForm: any = ref(null);
|
const machineForm: any = ref(null);
|
||||||
const authCertSelectRef: any = ref(null);
|
|
||||||
const tagSelectRef: any = ref(null);
|
const tagSelectRef: any = ref(null);
|
||||||
|
|
||||||
|
const defaultForm = {
|
||||||
|
id: null,
|
||||||
|
code: '',
|
||||||
|
tagPath: '',
|
||||||
|
ip: null,
|
||||||
|
port: 22,
|
||||||
|
protocol: MachineProtocolEnum.Ssh.value,
|
||||||
|
name: null,
|
||||||
|
authCerts: [],
|
||||||
|
tagCodePaths: [],
|
||||||
|
remark: '',
|
||||||
|
sshTunnelMachineId: null as any,
|
||||||
|
enableRecorder: -1,
|
||||||
|
};
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
dialogVisible: false,
|
|
||||||
tabActiveName: 'basic',
|
|
||||||
sshTunnelMachineList: [] as any,
|
sshTunnelMachineList: [] as any,
|
||||||
authCerts: [] as any,
|
form: defaultForm,
|
||||||
authType: 1,
|
submitForm: {} as any,
|
||||||
form: {
|
|
||||||
id: null,
|
|
||||||
code: '',
|
|
||||||
tagPath: '',
|
|
||||||
ip: null,
|
|
||||||
port: 22,
|
|
||||||
name: null,
|
|
||||||
authCertId: null as any,
|
|
||||||
username: '',
|
|
||||||
password: '',
|
|
||||||
tagId: [],
|
|
||||||
remark: '',
|
|
||||||
sshTunnelMachineId: null as any,
|
|
||||||
enableRecorder: -1,
|
|
||||||
},
|
|
||||||
submitForm: {},
|
|
||||||
pwd: '',
|
pwd: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const { dialogVisible, tabActiveName, form, submitForm } = toRefs(state);
|
const { form, submitForm } = toRefs(state);
|
||||||
|
|
||||||
const { isFetching: testConnBtnLoading, execute: testConnExec } = machineApi.testConn.useApi(submitForm);
|
const { isFetching: testConnBtnLoading, execute: testConnExec } = machineApi.testConn.useApi(submitForm);
|
||||||
const { isFetching: saveBtnLoading, execute: saveMachineExec } = machineApi.saveMachine.useApi(submitForm);
|
const { isFetching: saveBtnLoading, execute: saveMachineExec } = machineApi.saveMachine.useApi(submitForm);
|
||||||
|
|
||||||
watch(props, async (newValue: any) => {
|
watchEffect(() => {
|
||||||
state.dialogVisible = newValue.visible;
|
if (!dialogVisible.value) {
|
||||||
if (!state.dialogVisible) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
state.tabActiveName = 'basic';
|
const machine: any = props.machine;
|
||||||
if (newValue.machine) {
|
if (machine) {
|
||||||
state.form = { ...newValue.machine };
|
state.form = { ...machine };
|
||||||
state.form.tagId = newValue.machine.tags.map((t: any) => t.tagId);
|
state.form.tagCodePaths = machine.tags.map((t: any) => t.codePath);
|
||||||
// 如果凭证类型为公共的,则表示使用授权凭证认证
|
state.form.authCerts = machine.authCerts || [];
|
||||||
const authCertId = (state.form as any).authCertId;
|
|
||||||
if (authCertId > 0) {
|
|
||||||
state.authType = 2;
|
|
||||||
} else {
|
|
||||||
state.authType = 1;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
state.form = { port: 22, tagId: [] } as any;
|
state.form = { ...defaultForm };
|
||||||
state.authType = 1;
|
state.form.authCerts = [];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const changeAuthMethod = (val: any) => {
|
const testConn = async (authCert: any) => {
|
||||||
if (state.form.id) {
|
try {
|
||||||
if (val == 2) {
|
await machineForm.value.validate();
|
||||||
state.form.authCertId = null;
|
} catch (e: any) {
|
||||||
} else {
|
ElMessage.error('请正确填写信息');
|
||||||
state.form.password = '';
|
return false;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const testConn = async () => {
|
state.submitForm = getReqForm();
|
||||||
machineForm.value.validate(async (valid: boolean) => {
|
state.submitForm.authCerts = [authCert];
|
||||||
if (!valid) {
|
await testConnExec();
|
||||||
ElMessage.error('请正确填写信息');
|
ElMessage.success('连接成功');
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.submitForm = getReqForm();
|
|
||||||
await testConnExec();
|
|
||||||
ElMessage.success('连接成功');
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const btnOk = async () => {
|
const btnOk = async () => {
|
||||||
machineForm.value.validate(async (valid: boolean) => {
|
try {
|
||||||
if (!valid) {
|
await machineForm.value.validate();
|
||||||
ElMessage.error('请正确填写信息');
|
} catch (e: any) {
|
||||||
return false;
|
ElMessage.error('请正确填写信息');
|
||||||
}
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
state.submitForm = getReqForm();
|
if (state.form.authCerts.length == 0) {
|
||||||
await saveMachineExec();
|
ElMessage.error('请完善授权凭证账号信息');
|
||||||
ElMessage.success('保存成功');
|
return false;
|
||||||
emit('val-change', submitForm);
|
}
|
||||||
cancel();
|
|
||||||
});
|
state.submitForm = getReqForm();
|
||||||
|
await saveMachineExec();
|
||||||
|
ElMessage.success('保存成功');
|
||||||
|
emit('val-change', submitForm);
|
||||||
|
cancel();
|
||||||
};
|
};
|
||||||
|
|
||||||
const getReqForm = () => {
|
const getReqForm = () => {
|
||||||
const reqForm: any = { ...state.form };
|
const reqForm: any = { ...state.form };
|
||||||
// 如果为密码认证,则置空授权凭证id
|
|
||||||
if (state.authType == 1) {
|
|
||||||
reqForm.authCertId = -1;
|
|
||||||
}
|
|
||||||
if (!state.form.sshTunnelMachineId || state.form.sshTunnelMachineId <= 0) {
|
if (!state.form.sshTunnelMachineId || state.form.sshTunnelMachineId <= 0) {
|
||||||
reqForm.sshTunnelMachineId = -1;
|
reqForm.sshTunnelMachineId = -1;
|
||||||
}
|
}
|
||||||
return reqForm;
|
return reqForm;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleChangeProtocol = (val: any) => {
|
||||||
|
if (val == MachineProtocolEnum.Ssh.value) {
|
||||||
|
state.form.port = 22;
|
||||||
|
} else if (val == MachineProtocolEnum.Rdp.value) {
|
||||||
|
state.form.port = 3389;
|
||||||
|
} else {
|
||||||
|
state.form.port = 5901;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const cancel = () => {
|
const cancel = () => {
|
||||||
emit('update:visible', false);
|
dialogVisible.value = false;
|
||||||
emit('cancel');
|
emit('cancel');
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -4,11 +4,13 @@
|
|||||||
ref="pageTableRef"
|
ref="pageTableRef"
|
||||||
:page-api="machineApi.list"
|
:page-api="machineApi.list"
|
||||||
:before-query-fn="checkRouteTagPath"
|
:before-query-fn="checkRouteTagPath"
|
||||||
|
:data-handler-fn="handleData"
|
||||||
:search-items="searchItems"
|
:search-items="searchItems"
|
||||||
v-model:query-form="params"
|
v-model:query-form="params"
|
||||||
:show-selection="true"
|
:show-selection="true"
|
||||||
v-model:selection-data="state.selectionData"
|
v-model:selection-data="state.selectionData"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
|
:lazy="true"
|
||||||
>
|
>
|
||||||
<template #tableHeader>
|
<template #tableHeader>
|
||||||
<el-button v-auth="perms.addMachine" type="primary" icon="plus" @click="openFormDialog(false)" plain>添加 </el-button>
|
<el-button v-auth="perms.addMachine" type="primary" icon="plus" @click="openFormDialog(false)" plain>添加 </el-button>
|
||||||
@@ -83,12 +85,19 @@
|
|||||||
<ResourceTags :tags="data.tags" />
|
<ResourceTags :tags="data.tags" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template #authCert="{ data }">
|
||||||
|
<ResourceAuthCert v-model:select-auth-cert="data.selectAuthCert" :auth-certs="data.authCerts" />
|
||||||
|
</template>
|
||||||
|
|
||||||
<template #action="{ data }">
|
<template #action="{ data }">
|
||||||
<span v-auth="'machine:terminal'">
|
<span v-auth="'machine:terminal'">
|
||||||
<el-tooltip :show-after="500" content="按住ctrl则为新标签打开" placement="top">
|
<el-tooltip v-if="data.protocol == MachineProtocolEnum.Ssh.value" :show-after="500" content="按住ctrl则为新标签打开" placement="top">
|
||||||
<el-button :disabled="data.status == -1" type="primary" @click="showTerminal(data, $event)" link>终端</el-button>
|
<el-button :disabled="data.status == -1" type="primary" @click="showTerminal(data, $event)" link>SSH</el-button>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
|
|
||||||
|
<el-button v-if="data.protocol == MachineProtocolEnum.Rdp.value" type="primary" @click="showRDP(data)" link>RDP</el-button>
|
||||||
|
<el-button v-if="data.protocol == MachineProtocolEnum.Vnc.value" type="primary" @click="showRDP(data)" link>VNC</el-button>
|
||||||
|
|
||||||
<el-divider direction="vertical" border-style="dashed" />
|
<el-divider direction="vertical" border-style="dashed" />
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@@ -97,7 +106,9 @@
|
|||||||
<el-divider direction="vertical" border-style="dashed" />
|
<el-divider direction="vertical" border-style="dashed" />
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<el-button :disabled="data.status == -1" type="warning" @click="serviceManager(data)" link>脚本</el-button>
|
<el-button v-if="data.protocol == MachineProtocolEnum.Ssh.value" :disabled="data.status == -1" type="warning" @click="serviceManager(data)" link
|
||||||
|
>脚本</el-button
|
||||||
|
>
|
||||||
<el-divider direction="vertical" border-style="dashed" />
|
<el-divider direction="vertical" border-style="dashed" />
|
||||||
|
|
||||||
<el-dropdown @command="handleCommand">
|
<el-dropdown @command="handleCommand">
|
||||||
@@ -111,9 +122,19 @@
|
|||||||
<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: 'rdp-blank', data }" v-if="data.protocol == MachineProtocolEnum.Rdp.value">
|
||||||
|
RDP(新窗口)
|
||||||
|
</el-dropdown-item>
|
||||||
|
|
||||||
<el-dropdown-item :command="{ type: 'edit', data }" v-if="actionBtns[perms.updateMachine]"> 编辑 </el-dropdown-item>
|
<el-dropdown-item :command="{ type: 'edit', data }" v-if="actionBtns[perms.updateMachine]"> 编辑 </el-dropdown-item>
|
||||||
|
|
||||||
<el-dropdown-item :command="{ type: 'process', data }" :disabled="data.status == -1"> 进程 </el-dropdown-item>
|
<el-dropdown-item
|
||||||
|
v-if="data.protocol == MachineProtocolEnum.Ssh.value"
|
||||||
|
:command="{ type: 'process', data }"
|
||||||
|
:disabled="data.status == -1"
|
||||||
|
>
|
||||||
|
进程
|
||||||
|
</el-dropdown-item>
|
||||||
|
|
||||||
<el-dropdown-item :command="{ type: 'terminalRec', data }" v-if="actionBtns[perms.updateMachine] && data.enableRecorder == 1">
|
<el-dropdown-item :command="{ type: 'terminalRec', data }" v-if="actionBtns[perms.updateMachine] && data.enableRecorder == 1">
|
||||||
终端回放
|
终端回放
|
||||||
@@ -134,20 +155,15 @@
|
|||||||
<el-descriptions-item :span="2" label="IP">{{ infoDialog.data.ip }}</el-descriptions-item>
|
<el-descriptions-item :span="2" label="IP">{{ infoDialog.data.ip }}</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.authCertId > 1 ? '授权凭证' : '密码' }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
|
|
||||||
<el-descriptions-item :span="3" label="备注">{{ infoDialog.data.remark }}</el-descriptions-item>
|
<el-descriptions-item :span="3" label="备注">{{ infoDialog.data.remark }}</el-descriptions-item>
|
||||||
|
|
||||||
<el-descriptions-item :span="1.5" label="SSH隧道">{{ infoDialog.data.sshTunnelMachineId > 0 ? '是' : '否' }} </el-descriptions-item>
|
<el-descriptions-item :span="1.5" label="SSH隧道">{{ infoDialog.data.sshTunnelMachineId > 0 ? '是' : '否' }} </el-descriptions-item>
|
||||||
<el-descriptions-item :span="1.5" label="终端回放">{{ infoDialog.data.enableRecorder == 1 ? '是' : '否' }} </el-descriptions-item>
|
<el-descriptions-item :span="1.5" label="终端回放">{{ infoDialog.data.enableRecorder == 1 ? '是' : '否' }} </el-descriptions-item>
|
||||||
|
|
||||||
<el-descriptions-item :span="2" label="创建时间">{{ dateFormat(infoDialog.data.createTime) }} </el-descriptions-item>
|
<el-descriptions-item :span="2" label="创建时间">{{ formatDate(infoDialog.data.createTime) }} </el-descriptions-item>
|
||||||
<el-descriptions-item :span="1" label="创建者">{{ infoDialog.data.creator }}</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="2" label="更新时间">{{ formatDate(infoDialog.data.updateTime) }} </el-descriptions-item>
|
||||||
<el-descriptions-item :span="1" label="修改者">{{ infoDialog.data.modifier }}</el-descriptions-item>
|
<el-descriptions-item :span="1" label="修改者">{{ infoDialog.data.modifier }}</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
@@ -156,7 +172,7 @@
|
|||||||
<template #headerTitle="{ terminalInfo }">
|
<template #headerTitle="{ terminalInfo }">
|
||||||
{{ `${(terminalInfo.terminalId + '').slice(-2)}` }}
|
{{ `${(terminalInfo.terminalId + '').slice(-2)}` }}
|
||||||
<el-divider direction="vertical" />
|
<el-divider direction="vertical" />
|
||||||
{{ `${terminalInfo.meta.username}@${terminalInfo.meta.ip}:${terminalInfo.meta.port}` }}
|
{{ `${terminalInfo.meta.selectAuthCert.username}@${terminalInfo.meta.ip}:${terminalInfo.meta.port}` }}
|
||||||
<el-divider direction="vertical" />
|
<el-divider direction="vertical" />
|
||||||
{{ terminalInfo.meta.name }}
|
{{ terminalInfo.meta.name }}
|
||||||
</template>
|
</template>
|
||||||
@@ -171,30 +187,67 @@
|
|||||||
|
|
||||||
<process-list v-model:visible="processDialog.visible" v-model:machineId="processDialog.machineId" />
|
<process-list v-model:visible="processDialog.visible" v-model:machineId="processDialog.machineId" />
|
||||||
|
|
||||||
<script-manage :title="serviceDialog.title" v-model:visible="serviceDialog.visible" v-model:machineId="serviceDialog.machineId" />
|
<script-manage
|
||||||
|
:title="serviceDialog.title"
|
||||||
|
v-model:visible="serviceDialog.visible"
|
||||||
|
v-model:machineId="serviceDialog.machineId"
|
||||||
|
:auth-cert-name="serviceDialog.authCertName"
|
||||||
|
/>
|
||||||
|
|
||||||
<file-conf-list :title="fileDialog.title" v-model:visible="fileDialog.visible" v-model:machineId="fileDialog.machineId" />
|
<file-conf-list
|
||||||
|
:title="fileDialog.title"
|
||||||
|
v-model:visible="fileDialog.visible"
|
||||||
|
v-model:machineId="fileDialog.machineId"
|
||||||
|
:auth-cert-name="fileDialog.authCertName"
|
||||||
|
/>
|
||||||
|
|
||||||
<machine-stats v-model:visible="machineStatsDialog.visible" :machineId="machineStatsDialog.machineId" :title="machineStatsDialog.title"></machine-stats>
|
<machine-stats v-model:visible="machineStatsDialog.visible" :machineId="machineStatsDialog.machineId" :title="machineStatsDialog.title"></machine-stats>
|
||||||
|
|
||||||
<machine-rec v-model:visible="machineRecDialog.visible" :machineId="machineRecDialog.machineId" :title="machineRecDialog.title"></machine-rec>
|
<machine-rec v-model:visible="machineRecDialog.visible" :machineId="machineRecDialog.machineId" :title="machineRecDialog.title"></machine-rec>
|
||||||
|
|
||||||
|
<machine-rdp-dialog-comp
|
||||||
|
:title="machineRdpDialog.title"
|
||||||
|
v-model:visible="machineRdpDialog.visible"
|
||||||
|
v-model:machine-id="machineRdpDialog.machineId"
|
||||||
|
v-model:auth-cert="machineRdpDialog.authCert"
|
||||||
|
>
|
||||||
|
<template #headerTitle="{ terminalInfo }">
|
||||||
|
{{ `${(terminalInfo.terminalId + '').slice(-2)}` }}
|
||||||
|
<el-divider direction="vertical" />
|
||||||
|
{{ `${terminalInfo.meta.username}@${terminalInfo.meta.ip}:${terminalInfo.meta.port}` }}
|
||||||
|
<el-divider direction="vertical" />
|
||||||
|
{{ terminalInfo.meta.name }}
|
||||||
|
</template>
|
||||||
|
</machine-rdp-dialog-comp>
|
||||||
|
|
||||||
|
<el-dialog destroy-on-close :title="filesystemDialog.title" v-model="filesystemDialog.visible" :close-on-click-modal="false" width="70%">
|
||||||
|
<machine-file
|
||||||
|
:machine-id="filesystemDialog.machineId"
|
||||||
|
:protocol="filesystemDialog.protocol"
|
||||||
|
:file-id="filesystemDialog.fileId"
|
||||||
|
:path="filesystemDialog.path"
|
||||||
|
/>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</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 { useRouter, useRoute } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
import { machineApi, getMachineTerminalSocketUrl } from './api';
|
import { getMachineTerminalSocketUrl, machineApi } from './api';
|
||||||
import { dateFormat } from '@/common/utils/date';
|
|
||||||
import ResourceTags from '../component/ResourceTags.vue';
|
import ResourceTags from '../component/ResourceTags.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';
|
||||||
import { formatByteSize } from '@/common/utils/format';
|
import { formatByteSize, formatDate } from '@/common/utils/format';
|
||||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||||
import { SearchItem } from '@/components/SearchForm';
|
import { SearchItem } from '@/components/SearchForm';
|
||||||
import { getTagPathSearchItem } from '../component/tag';
|
import { getTagPathSearchItem } from '../component/tag';
|
||||||
|
import MachineFile from '@/views/ops/machine/file/MachineFile.vue';
|
||||||
|
import ResourceAuthCert from '../component/ResourceAuthCert.vue';
|
||||||
|
import { MachineProtocolEnum } from './enums';
|
||||||
|
import MachineRdpDialogComp from '@/components/terminal-rdp/MachineRdpDialog.vue';
|
||||||
|
|
||||||
// 组件
|
// 组件
|
||||||
const TerminalDialog = defineAsyncComponent(() => import('@/components/terminal/TerminalDialog.vue'));
|
const TerminalDialog = defineAsyncComponent(() => import('@/components/terminal/TerminalDialog.vue'));
|
||||||
@@ -205,6 +258,13 @@ const MachineStats = defineAsyncComponent(() => import('./MachineStats.vue'));
|
|||||||
const MachineRec = defineAsyncComponent(() => import('./MachineRec.vue'));
|
const MachineRec = defineAsyncComponent(() => import('./MachineRec.vue'));
|
||||||
const ProcessList = defineAsyncComponent(() => import('./ProcessList.vue'));
|
const ProcessList = defineAsyncComponent(() => import('./ProcessList.vue'));
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
lazy: {
|
||||||
|
type: [Boolean],
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const terminalDialogRef: any = ref(null);
|
const terminalDialogRef: any = ref(null);
|
||||||
@@ -217,17 +277,23 @@ const perms = {
|
|||||||
terminal: 'machine:terminal',
|
terminal: 'machine:terminal',
|
||||||
};
|
};
|
||||||
|
|
||||||
const searchItems = [getTagPathSearchItem(TagResourceTypeEnum.Machine.value), SearchItem.input('ip', 'IP'), SearchItem.input('name', '名称')];
|
const searchItems = [
|
||||||
|
getTagPathSearchItem(TagResourceTypeEnum.MachineAuthCert.value),
|
||||||
|
SearchItem.input('code', '编号'),
|
||||||
|
SearchItem.input('ip', 'IP'),
|
||||||
|
SearchItem.input('name', '名称'),
|
||||||
|
];
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
TableColumn.new('tags[0].tagPath', '关联标签').isSlot('tagPath').setAddWidth(20),
|
TableColumn.new('tags[0].tagPath', '关联标签').isSlot('tagPath').setAddWidth(20),
|
||||||
TableColumn.new('name', '名称'),
|
TableColumn.new('name', '名称'),
|
||||||
TableColumn.new('ipPort', 'ip:port').isSlot().setAddWidth(50),
|
TableColumn.new('ipPort', 'ip:port').isSlot().setAddWidth(50),
|
||||||
|
TableColumn.new('authCerts[0].username', '授权凭证').isSlot('authCert').setAddWidth(10),
|
||||||
|
TableColumn.new('status', '状态').isSlot().setMinWidth(85),
|
||||||
TableColumn.new('stat', '运行状态').isSlot().setAddWidth(55),
|
TableColumn.new('stat', '运行状态').isSlot().setAddWidth(55),
|
||||||
TableColumn.new('fs', '磁盘(挂载点=>可用/总)').isSlot().setAddWidth(25),
|
TableColumn.new('fs', '磁盘(挂载点=>可用/总)').isSlot().setAddWidth(25),
|
||||||
TableColumn.new('username', '用户名'),
|
|
||||||
TableColumn.new('status', '状态').isSlot().setMinWidth(85),
|
|
||||||
TableColumn.new('remark', '备注'),
|
TableColumn.new('remark', '备注'),
|
||||||
|
TableColumn.new('code', '编号'),
|
||||||
TableColumn.new('action', '操作').isSlot().setMinWidth(238).fixedRight().alignCenter(),
|
TableColumn.new('action', '操作').isSlot().setMinWidth(238).fixedRight().alignCenter(),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -251,6 +317,7 @@ const state = reactive({
|
|||||||
serviceDialog: {
|
serviceDialog: {
|
||||||
visible: false,
|
visible: false,
|
||||||
machineId: 0,
|
machineId: 0,
|
||||||
|
authCertName: '',
|
||||||
title: '',
|
title: '',
|
||||||
},
|
},
|
||||||
processDialog: {
|
processDialog: {
|
||||||
@@ -260,8 +327,18 @@ const state = reactive({
|
|||||||
fileDialog: {
|
fileDialog: {
|
||||||
visible: false,
|
visible: false,
|
||||||
machineId: 0,
|
machineId: 0,
|
||||||
|
authCertName: '',
|
||||||
title: '',
|
title: '',
|
||||||
},
|
},
|
||||||
|
filesystemDialog: {
|
||||||
|
visible: false,
|
||||||
|
machineId: 0,
|
||||||
|
protocol: 1,
|
||||||
|
title: '',
|
||||||
|
fileId: 0,
|
||||||
|
authCertName: '',
|
||||||
|
path: '',
|
||||||
|
},
|
||||||
machineStatsDialog: {
|
machineStatsDialog: {
|
||||||
visible: false,
|
visible: false,
|
||||||
stats: null,
|
stats: null,
|
||||||
@@ -278,11 +355,33 @@ const state = reactive({
|
|||||||
machineId: 0,
|
machineId: 0,
|
||||||
title: '',
|
title: '',
|
||||||
},
|
},
|
||||||
|
machineRdpDialog: {
|
||||||
|
visible: false,
|
||||||
|
machineId: 0,
|
||||||
|
title: '',
|
||||||
|
authCert: '',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { params, infoDialog, selectionData, serviceDialog, processDialog, fileDialog, machineStatsDialog, machineEditDialog, machineRecDialog } = toRefs(state);
|
const {
|
||||||
|
params,
|
||||||
|
infoDialog,
|
||||||
|
selectionData,
|
||||||
|
serviceDialog,
|
||||||
|
processDialog,
|
||||||
|
fileDialog,
|
||||||
|
machineStatsDialog,
|
||||||
|
machineEditDialog,
|
||||||
|
machineRecDialog,
|
||||||
|
machineRdpDialog,
|
||||||
|
filesystemDialog,
|
||||||
|
} = toRefs(state);
|
||||||
|
|
||||||
onMounted(async () => {});
|
onMounted(async () => {
|
||||||
|
if (!props.lazy) {
|
||||||
|
search();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const checkRouteTagPath = (query: any) => {
|
const checkRouteTagPath = (query: any) => {
|
||||||
if (route.query.tagPath) {
|
if (route.query.tagPath) {
|
||||||
@@ -291,6 +390,15 @@ const checkRouteTagPath = (query: any) => {
|
|||||||
return query;
|
return query;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleData = (res: any) => {
|
||||||
|
const dataList = res.list;
|
||||||
|
// 赋值授权凭证
|
||||||
|
for (let x of dataList) {
|
||||||
|
x.selectAuthCert = x.authCerts[0];
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
const handleCommand = (commond: any) => {
|
const handleCommand = (commond: any) => {
|
||||||
const data = commond.data;
|
const data = commond.data;
|
||||||
const type = commond.type;
|
const type = commond.type;
|
||||||
@@ -311,16 +419,25 @@ const handleCommand = (commond: any) => {
|
|||||||
showRec(data);
|
showRec(data);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
case 'rdp': {
|
||||||
|
showRDP(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'rdp-blank': {
|
||||||
|
showRDP(data, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const showTerminal = (row: any, event: PointerEvent) => {
|
const showTerminal = (row: any, event: PointerEvent) => {
|
||||||
|
const ac = row.selectAuthCert.name;
|
||||||
// 按住ctrl点击,则新建标签页打开, metaKey对应mac command键
|
// 按住ctrl点击,则新建标签页打开, metaKey对应mac command键
|
||||||
if (event.ctrlKey || event.metaKey) {
|
if (event.ctrlKey || event.metaKey) {
|
||||||
const { href } = router.resolve({
|
const { href } = router.resolve({
|
||||||
path: `/machine/terminal`,
|
path: `/machine/terminal`,
|
||||||
query: {
|
query: {
|
||||||
id: row.id,
|
ac,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -331,9 +448,9 @@ const showTerminal = (row: any, event: PointerEvent) => {
|
|||||||
const terminalId = Date.now();
|
const terminalId = Date.now();
|
||||||
terminalDialogRef.value.open({
|
terminalDialogRef.value.open({
|
||||||
terminalId,
|
terminalId,
|
||||||
socketUrl: getMachineTerminalSocketUrl(row.id),
|
socketUrl: getMachineTerminalSocketUrl(ac),
|
||||||
minTitle: `${row.name} [${(terminalId + '').slice(-2)}]`, // 截取terminalId最后两位区分多个terminal
|
minTitle: `${row.name} [${(terminalId + '').slice(-2)}]`, // 截取terminalId最后两位区分多个terminal
|
||||||
minDesc: `${row.username}@${row.ip}:${row.port} (${row.name})`,
|
minDesc: `${row.selectAuthCert.username}@${row.ip}:${row.port} (${row.name})`,
|
||||||
meta: row,
|
meta: row,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -372,9 +489,11 @@ const deleteMachine = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const serviceManager = (row: any) => {
|
const serviceManager = (row: any) => {
|
||||||
|
const authCert = row.selectAuthCert;
|
||||||
state.serviceDialog.machineId = row.id;
|
state.serviceDialog.machineId = row.id;
|
||||||
|
state.serviceDialog.authCertName = authCert.name;
|
||||||
state.serviceDialog.visible = true;
|
state.serviceDialog.visible = true;
|
||||||
state.serviceDialog.title = `${row.name} => ${row.ip}`;
|
state.serviceDialog.title = `${row.name} => ${authCert.username}@${row.ip}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -396,7 +515,10 @@ const showMachineStats = async (machine: any) => {
|
|||||||
state.machineStatsDialog.visible = true;
|
state.machineStatsDialog.visible = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const search = async () => {
|
const search = async (tagPath: string = '') => {
|
||||||
|
if (tagPath) {
|
||||||
|
state.params.tagPath = tagPath;
|
||||||
|
}
|
||||||
pageTableRef.value.search();
|
pageTableRef.value.search();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -404,10 +526,23 @@ const submitSuccess = () => {
|
|||||||
search();
|
search();
|
||||||
};
|
};
|
||||||
|
|
||||||
const showFileManage = (selectionData: any) => {
|
const showFileManage = (data: any) => {
|
||||||
state.fileDialog.visible = true;
|
if (data.protocol === MachineProtocolEnum.Ssh.value) {
|
||||||
state.fileDialog.machineId = selectionData.id;
|
// ssh
|
||||||
state.fileDialog.title = `${selectionData.name} => ${selectionData.ip}`;
|
state.fileDialog.visible = true;
|
||||||
|
state.fileDialog.machineId = data.id;
|
||||||
|
state.fileDialog.authCertName = data.selectAuthCert.name;
|
||||||
|
state.fileDialog.title = `${data.name} => ${data.selectAuthCert.username}@${data.ip}`;
|
||||||
|
} else if (data.protocol === MachineProtocolEnum.Rdp.value) {
|
||||||
|
// rdp
|
||||||
|
state.filesystemDialog.protocol = 2;
|
||||||
|
state.filesystemDialog.machineId = data.id;
|
||||||
|
state.filesystemDialog.fileId = data.id;
|
||||||
|
state.filesystemDialog.authCertName = data.selectAuthCert.name;
|
||||||
|
state.filesystemDialog.path = '/';
|
||||||
|
state.filesystemDialog.title = `${data.name} => ${data.selectAuthCert.username}@远程桌面文件`;
|
||||||
|
state.filesystemDialog.visible = true;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatsFontClass = (availavle: number, total: number) => {
|
const getStatsFontClass = (availavle: number, total: number) => {
|
||||||
@@ -437,6 +572,26 @@ const showRec = (row: any) => {
|
|||||||
state.machineRecDialog.machineId = row.id;
|
state.machineRecDialog.machineId = row.id;
|
||||||
state.machineRecDialog.visible = true;
|
state.machineRecDialog.visible = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const showRDP = (row: any, blank = false) => {
|
||||||
|
if (blank) {
|
||||||
|
const { href } = router.resolve({
|
||||||
|
path: `/machine/terminal-rdp`,
|
||||||
|
query: {
|
||||||
|
ac: row.selectAuthCert.name,
|
||||||
|
name: row.name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
window.open(href, '_blank');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.machineRdpDialog.title = `${row.name}[${row.ip}]-远程桌面`;
|
||||||
|
state.machineRdpDialog.machineId = row.id;
|
||||||
|
state.machineRdpDialog.authCert = row.selectAuthCert.name;
|
||||||
|
state.machineRdpDialog.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({ search });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -6,17 +6,27 @@
|
|||||||
<tag-tree
|
<tag-tree
|
||||||
class="machine-terminal-tree"
|
class="machine-terminal-tree"
|
||||||
ref="tagTreeRef"
|
ref="tagTreeRef"
|
||||||
:resource-type="TagResourceTypeEnum.Machine.value"
|
:resource-type="TagResourceTypeEnum.MachineAuthCert.value"
|
||||||
:tag-path-node-type="NodeTypeTagPath"
|
:tag-path-node-type="NodeTypeTagPath"
|
||||||
|
:default-expanded-keys="state.defaultExpendKey"
|
||||||
>
|
>
|
||||||
<template #prefix="{ data }">
|
<template #prefix="{ data }">
|
||||||
<SvgIcon v-if="data.icon && data.params.status == 1" :name="data.icon.name" :color="data.icon.color" />
|
<SvgIcon
|
||||||
<SvgIcon v-if="data.icon && data.params.status == -1" :name="data.icon.name" color="var(--el-color-danger)" />
|
v-if="data.icon && data.params.status == 1 && data.params.protocol == MachineProtocolEnum.Ssh.value"
|
||||||
|
:name="data.icon.name"
|
||||||
|
:color="data.icon.color"
|
||||||
|
/>
|
||||||
|
<SvgIcon
|
||||||
|
v-if="data.icon && data.params.status == -1 && data.params.protocol == MachineProtocolEnum.Ssh.value"
|
||||||
|
:name="data.icon.name"
|
||||||
|
color="var(--el-color-danger)"
|
||||||
|
/>
|
||||||
|
<SvgIcon v-if="data.icon && data.params.protocol != MachineProtocolEnum.Ssh.value" :name="data.icon.name" :color="data.icon.color" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #suffix="{ data }">
|
<template #suffix="{ data }">
|
||||||
<span style="color: #c4c9c4; font-size: 9px" v-if="data.type.value == MachineNodeType.Machine">{{
|
<span v-if="data.type.value == MachineNodeType.AuthCert">{{
|
||||||
` ${data.params.username}@${data.params.ip}:${data.params.port}`
|
` ${data.params.selectAuthCert.username}@${data.params.ip}:${data.params.port}`
|
||||||
}}</span>
|
}}</span>
|
||||||
</template>
|
</template>
|
||||||
</tag-tree>
|
</tag-tree>
|
||||||
@@ -35,7 +45,7 @@
|
|||||||
>
|
>
|
||||||
<el-tab-pane class="h100" closable v-for="dt in state.tabs.values()" :label="dt.label" :name="dt.key" :key="dt.key">
|
<el-tab-pane class="h100" closable v-for="dt in state.tabs.values()" :label="dt.label" :name="dt.key" :key="dt.key">
|
||||||
<template #label>
|
<template #label>
|
||||||
<el-popconfirm @confirm="handleReconnect(dt.key)" title="确认重新连接?">
|
<el-popconfirm @confirm="handleReconnect(dt, true)" title="确认重新连接?">
|
||||||
<template #reference>
|
<template #reference>
|
||||||
<el-icon class="mr5" :color="dt.status == 1 ? '#67c23a' : '#f56c6c'" :title="dt.status == 1 ? '' : '点击重连'"
|
<el-icon class="mr5" :color="dt.status == 1 ? '#67c23a' : '#f56c6c'" :title="dt.status == 1 ? '' : '点击重连'"
|
||||||
><Connection />
|
><Connection />
|
||||||
@@ -59,13 +69,21 @@
|
|||||||
</el-popover>
|
</el-popover>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="terminal-wrapper" style="height: calc(100vh - 155px)">
|
<div :ref="(el: any) => setTerminalWrapperRef(el, dt.key)" class="terminal-wrapper" style="height: calc(100vh - 155px)">
|
||||||
<TerminalBody
|
<TerminalBody
|
||||||
|
v-if="dt.params.protocol == MachineProtocolEnum.Ssh.value"
|
||||||
:mount-init="false"
|
:mount-init="false"
|
||||||
@status-change="terminalStatusChange(dt.key, $event)"
|
@status-change="terminalStatusChange(dt.key, $event)"
|
||||||
:ref="(el) => setTerminalRef(el, dt.key)"
|
:ref="(el: any) => setTerminalRef(el, dt.key)"
|
||||||
:socket-url="dt.socketUrl"
|
:socket-url="dt.socketUrl"
|
||||||
/>
|
/>
|
||||||
|
<machine-rdp
|
||||||
|
v-if="dt.params.protocol != MachineProtocolEnum.Ssh.value"
|
||||||
|
:machine-id="dt.params.id"
|
||||||
|
:auth-cert="dt.authCert"
|
||||||
|
:ref="(el: any) => setTerminalRef(el, dt.key)"
|
||||||
|
@status-change="terminalStatusChange(dt.key, $event)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
@@ -75,34 +93,56 @@
|
|||||||
<el-descriptions-item :span="1.5" label="机器id">{{ infoDialog.data.id }}</el-descriptions-item>
|
<el-descriptions-item :span="1.5" label="机器id">{{ infoDialog.data.id }}</el-descriptions-item>
|
||||||
<el-descriptions-item :span="1.5" label="名称">{{ infoDialog.data.name }}</el-descriptions-item>
|
<el-descriptions-item :span="1.5" label="名称">{{ infoDialog.data.name }}</el-descriptions-item>
|
||||||
|
|
||||||
<el-descriptions-item :span="3" label="标签路径">{{ infoDialog.data.tagPath }}</el-descriptions-item>
|
<el-descriptions-item :span="3" label="关联标签"><ResourceTags :tags="infoDialog.data.tags" /></el-descriptions-item>
|
||||||
|
|
||||||
<el-descriptions-item :span="2" label="IP">{{ infoDialog.data.ip }}</el-descriptions-item>
|
<el-descriptions-item :span="2" label="IP">{{ infoDialog.data.ip }}</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.authCertId > 1 ? '授权凭证' : '密码' }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
|
|
||||||
<el-descriptions-item :span="3" label="备注">{{ infoDialog.data.remark }}</el-descriptions-item>
|
<el-descriptions-item :span="3" label="备注">{{ infoDialog.data.remark }}</el-descriptions-item>
|
||||||
|
|
||||||
<el-descriptions-item :span="1.5" label="SSH隧道">{{ infoDialog.data.sshTunnelMachineId > 0 ? '是' : '否' }} </el-descriptions-item>
|
<el-descriptions-item :span="1.5" label="SSH隧道">{{ infoDialog.data.sshTunnelMachineId > 0 ? '是' : '否' }} </el-descriptions-item>
|
||||||
<el-descriptions-item :span="1.5" label="终端回放">{{ infoDialog.data.enableRecorder == 1 ? '是' : '否' }} </el-descriptions-item>
|
<el-descriptions-item :span="1.5" label="终端回放">{{ infoDialog.data.enableRecorder == 1 ? '是' : '否' }} </el-descriptions-item>
|
||||||
|
|
||||||
<el-descriptions-item :span="2" label="创建时间">{{ dateFormat(infoDialog.data.createTime) }} </el-descriptions-item>
|
<el-descriptions-item :span="2" label="创建时间">{{ formatDate(infoDialog.data.createTime) }} </el-descriptions-item>
|
||||||
<el-descriptions-item :span="1" label="创建者">{{ infoDialog.data.creator }}</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="2" label="更新时间">{{ formatDate(infoDialog.data.updateTime) }} </el-descriptions-item>
|
||||||
<el-descriptions-item :span="1" label="修改者">{{ infoDialog.data.modifier }}</el-descriptions-item>
|
<el-descriptions-item :span="1" label="修改者">{{ infoDialog.data.modifier }}</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<process-list v-model:visible="processDialog.visible" v-model:machineId="processDialog.machineId" />
|
<process-list v-model:visible="processDialog.visible" v-model:machineId="processDialog.machineId" />
|
||||||
|
|
||||||
<script-manage :title="serviceDialog.title" v-model:visible="serviceDialog.visible" v-model:machineId="serviceDialog.machineId" />
|
<script-manage
|
||||||
|
:title="serviceDialog.title"
|
||||||
|
v-model:visible="serviceDialog.visible"
|
||||||
|
v-model:machineId="serviceDialog.machineId"
|
||||||
|
:auth-cert-name="serviceDialog.authCertName"
|
||||||
|
/>
|
||||||
|
|
||||||
<file-conf-list :title="fileDialog.title" v-model:visible="fileDialog.visible" v-model:machineId="fileDialog.machineId" />
|
<file-conf-list
|
||||||
|
:title="fileDialog.title"
|
||||||
|
:auth-cert-name="fileDialog.authCertName"
|
||||||
|
v-model:visible="fileDialog.visible"
|
||||||
|
v-model:machineId="fileDialog.machineId"
|
||||||
|
:protocol="fileDialog.protocol"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
<machine-stats v-model:visible="machineStatsDialog.visible" :machineId="machineStatsDialog.machineId" :title="machineStatsDialog.title" />
|
<machine-stats v-model:visible="machineStatsDialog.visible" :machineId="machineStatsDialog.machineId" :title="machineStatsDialog.title" />
|
||||||
|
|
||||||
@@ -114,24 +154,31 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, toRefs, reactive, defineAsyncComponent, nextTick } from 'vue';
|
import { defineAsyncComponent, nextTick, onMounted, reactive, ref, toRefs, watch } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { machineApi, getMachineTerminalSocketUrl } from './api';
|
import { getMachineTerminalSocketUrl, machineApi } from './api';
|
||||||
import { dateFormat } from '@/common/utils/date';
|
import { formatDate } from '@/common/utils/format';
|
||||||
import { hasPerms } from '@/components/auth/auth';
|
import { hasPerms } from '@/components/auth/auth';
|
||||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||||
import { NodeType, TagTreeNode } from '../component/tag';
|
import { NodeType, TagTreeNode, getTagTypeCodeByPath } from '../component/tag';
|
||||||
import TagTree from '../component/TagTree.vue';
|
import TagTree from '../component/TagTree.vue';
|
||||||
import { Splitpanes, Pane } from 'splitpanes';
|
import { Pane, Splitpanes } from 'splitpanes';
|
||||||
import { ContextmenuItem } from '@/components/contextmenu/index';
|
import { ContextmenuItem } from '@/components/contextmenu/index';
|
||||||
|
import TerminalBody from '@/components/terminal/TerminalBody.vue';
|
||||||
|
import { TerminalStatus } from '@/components/terminal/common';
|
||||||
|
import MachineRdp from '@/components/terminal-rdp/MachineRdp.vue';
|
||||||
|
import MachineFile from '@/views/ops/machine/file/MachineFile.vue';
|
||||||
|
import ResourceTags from '../component/ResourceTags.vue';
|
||||||
|
import { MachineProtocolEnum } from './enums';
|
||||||
|
import { useAutoOpenResource } from '@/store/autoOpenResource';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
|
||||||
// 组件
|
// 组件
|
||||||
const ScriptManage = defineAsyncComponent(() => import('./ScriptManage.vue'));
|
const ScriptManage = defineAsyncComponent(() => import('./ScriptManage.vue'));
|
||||||
const FileConfList = defineAsyncComponent(() => import('./file/FileConfList.vue'));
|
const FileConfList = defineAsyncComponent(() => import('./file/FileConfList.vue'));
|
||||||
const MachineStats = defineAsyncComponent(() => import('./MachineStats.vue'));
|
const MachineStats = defineAsyncComponent(() => import('./MachineStats.vue'));
|
||||||
const MachineRec = defineAsyncComponent(() => import('./MachineRec.vue'));
|
const MachineRec = defineAsyncComponent(() => import('./MachineRec.vue'));
|
||||||
const ProcessList = defineAsyncComponent(() => import('./ProcessList.vue'));
|
const ProcessList = defineAsyncComponent(() => import('./ProcessList.vue'));
|
||||||
import TerminalBody from '@/components/terminal/TerminalBody.vue';
|
|
||||||
import { TerminalStatus } from '@/components/terminal/common';
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@@ -148,9 +195,11 @@ const actionBtns = hasPerms([perms.updateMachine, perms.closeCli]);
|
|||||||
|
|
||||||
class MachineNodeType {
|
class MachineNodeType {
|
||||||
static Machine = 1;
|
static Machine = 1;
|
||||||
|
static AuthCert = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
|
defaultExpendKey: [] as any,
|
||||||
params: {
|
params: {
|
||||||
pageNum: 1,
|
pageNum: 1,
|
||||||
pageSize: 0,
|
pageSize: 0,
|
||||||
@@ -165,6 +214,7 @@ const state = reactive({
|
|||||||
serviceDialog: {
|
serviceDialog: {
|
||||||
visible: false,
|
visible: false,
|
||||||
machineId: 0,
|
machineId: 0,
|
||||||
|
authCertName: '',
|
||||||
title: '',
|
title: '',
|
||||||
},
|
},
|
||||||
processDialog: {
|
processDialog: {
|
||||||
@@ -174,7 +224,18 @@ const state = reactive({
|
|||||||
fileDialog: {
|
fileDialog: {
|
||||||
visible: false,
|
visible: false,
|
||||||
machineId: 0,
|
machineId: 0,
|
||||||
|
protocol: 1,
|
||||||
title: '',
|
title: '',
|
||||||
|
authCertName: '',
|
||||||
|
},
|
||||||
|
filesystemDialog: {
|
||||||
|
visible: false,
|
||||||
|
machineId: 0,
|
||||||
|
authCertName: '',
|
||||||
|
protocol: 1,
|
||||||
|
title: '',
|
||||||
|
fileId: 0,
|
||||||
|
path: '',
|
||||||
},
|
},
|
||||||
machineStatsDialog: {
|
machineStatsDialog: {
|
||||||
visible: false,
|
visible: false,
|
||||||
@@ -195,7 +256,12 @@ const { infoDialog, serviceDialog, processDialog, fileDialog, machineStatsDialog
|
|||||||
|
|
||||||
const tagTreeRef: any = ref(null);
|
const tagTreeRef: any = ref(null);
|
||||||
|
|
||||||
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (node: any) => {
|
const autoOpenResourceStore = useAutoOpenResource();
|
||||||
|
const { autoOpenResource } = storeToRefs(autoOpenResourceStore);
|
||||||
|
|
||||||
|
let openIds = {};
|
||||||
|
|
||||||
|
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (node: TagTreeNode) => {
|
||||||
// 加载标签树下的机器列表
|
// 加载标签树下的机器列表
|
||||||
state.params.tagPath = node.key;
|
state.params.tagPath = node.key;
|
||||||
state.params.pageNum = 1;
|
state.params.pageNum = 1;
|
||||||
@@ -204,87 +270,171 @@ const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(asyn
|
|||||||
// 把list 根据name字段排序
|
// 把list 根据name字段排序
|
||||||
res.list = res.list.sort((a: any, b: any) => a.name.localeCompare(b.name));
|
res.list = res.list.sort((a: any, b: any) => a.name.localeCompare(b.name));
|
||||||
return res.list.map((x: any) =>
|
return res.list.map((x: any) =>
|
||||||
new TagTreeNode(x.id, x.name, NodeTypeMachine(x))
|
new TagTreeNode(x.code, x.name, NodeTypeMachine)
|
||||||
.withParams(x)
|
.withParams(x)
|
||||||
.withDisabled(x.status == -1)
|
.withDisabled(x.status == -1 && x.protocol == MachineProtocolEnum.Ssh.value)
|
||||||
.withIcon({
|
.withIcon({
|
||||||
name: 'Monitor',
|
name: 'Monitor',
|
||||||
color: '#409eff',
|
color: '#409eff',
|
||||||
})
|
})
|
||||||
.withIsLeaf(true)
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
let openIds = {};
|
const NodeTypeMachine = new NodeType(MachineNodeType.Machine)
|
||||||
|
.withLoadNodesFunc((node: TagTreeNode) => {
|
||||||
|
const machine = node.params;
|
||||||
|
// 获取授权凭证列表
|
||||||
|
const authCerts = machine.authCerts;
|
||||||
|
return authCerts.map((x: any) =>
|
||||||
|
new TagTreeNode(x.name, x.username, NodeTypeAuthCert)
|
||||||
|
.withParams({ ...machine, selectAuthCert: x })
|
||||||
|
.withDisabled(machine.status == -1 && machine.protocol == MachineProtocolEnum.Ssh.value)
|
||||||
|
.withIcon({
|
||||||
|
name: 'Ticket',
|
||||||
|
color: '#409eff',
|
||||||
|
})
|
||||||
|
.withIsLeaf(true)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.withContextMenuItems([
|
||||||
|
new ContextmenuItem('detail', '详情').withIcon('More').withOnClick((node: any) => showInfo(node.params)),
|
||||||
|
|
||||||
const NodeTypeMachine = (machine: any) => {
|
new ContextmenuItem('status', '状态')
|
||||||
let contextMenuItems = [];
|
.withIcon('Compass')
|
||||||
contextMenuItems.push(new ContextmenuItem('term', '打开终端').withIcon('Monitor').withOnClick(() => openTerminal(machine)));
|
.withHideFunc((node: any) => node.params.protocol != MachineProtocolEnum.Ssh.value)
|
||||||
contextMenuItems.push(new ContextmenuItem('term-ex', '打开终端(新窗口)').withIcon('Monitor').withOnClick(() => openTerminal(machine, true)));
|
.withOnClick((node: any) => showMachineStats(node.params)),
|
||||||
contextMenuItems.push(new ContextmenuItem('files', '文件管理').withIcon('FolderOpened').withOnClick(() => showFileManage(machine)));
|
|
||||||
contextMenuItems.push(new ContextmenuItem('scripts', '脚本管理').withIcon('Files').withOnClick(() => serviceManager(machine)));
|
new ContextmenuItem('process', '进程')
|
||||||
contextMenuItems.push(new ContextmenuItem('detail', '详情').withIcon('More').withOnClick(() => showInfo(machine)));
|
.withIcon('DataLine')
|
||||||
contextMenuItems.push(new ContextmenuItem('status', '状态').withIcon('Compass').withOnClick(() => showMachineStats(machine)));
|
.withHideFunc((node: any) => node.params.protocol != MachineProtocolEnum.Ssh.value)
|
||||||
contextMenuItems.push(new ContextmenuItem('process', '进程').withIcon('DataLine').withOnClick(() => showProcess(machine)));
|
.withOnClick((node: any) => showProcess(node.params)),
|
||||||
if (actionBtns[perms.updateMachine] && machine.enableRecorder == 1) {
|
|
||||||
contextMenuItems.push(new ContextmenuItem('edit', '终端回放').withIcon('Compass').withOnClick(() => showRec(machine)));
|
new ContextmenuItem('edit', '终端回放')
|
||||||
|
.withIcon('Compass')
|
||||||
|
.withOnClick((node: any) => showRec(node.params))
|
||||||
|
.withHideFunc((node: any) => actionBtns[perms.updateMachine] && node.params.enableRecorder == 1),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const NodeTypeAuthCert = new NodeType(MachineNodeType.AuthCert)
|
||||||
|
.withNodeDblclickFunc((node: TagTreeNode) => {
|
||||||
|
openTerminal(node.params);
|
||||||
|
})
|
||||||
|
.withContextMenuItems([
|
||||||
|
new ContextmenuItem('term', '打开终端').withIcon('Monitor').withOnClick((node: any) => openTerminal(node.params)),
|
||||||
|
new ContextmenuItem('term-ex', '打开终端(新窗口)').withIcon('Monitor').withOnClick((node: any) => openTerminal(node.params, true)),
|
||||||
|
new ContextmenuItem('files', '文件管理').withIcon('FolderOpened').withOnClick((node: any) => showFileManage(node.params)),
|
||||||
|
|
||||||
|
new ContextmenuItem('scripts', '脚本管理')
|
||||||
|
.withIcon('Files')
|
||||||
|
.withHideFunc((node: any) => node.params.protocol != MachineProtocolEnum.Ssh.value)
|
||||||
|
.withOnClick((node: any) => serviceManager(node.params)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => autoOpenResource.value.machineCodePath,
|
||||||
|
(codePath: any) => {
|
||||||
|
autoOpenTerminal(codePath);
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return new NodeType(MachineNodeType.Machine).withContextMenuItems(contextMenuItems).withNodeDblclickFunc(() => {
|
watch(
|
||||||
// for (let k of state.tabs.keys()) {
|
() => state.activeTermName,
|
||||||
// // 存在该机器相关的终端tab,则直接激活该tab
|
(newValue, oldValue) => {
|
||||||
// if (k.startsWith(`${machine.id}_${machine.username}_`)) {
|
oldValue && terminalRefs[oldValue]?.blur && terminalRefs[oldValue]?.blur();
|
||||||
// state.activeTermName = k;
|
terminalRefs[newValue]?.focus && terminalRefs[newValue]?.focus();
|
||||||
// onTabChange();
|
}
|
||||||
// return;
|
);
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
openTerminal(machine);
|
onMounted(() => {
|
||||||
});
|
autoOpenTerminal(autoOpenResource.value.machineCodePath);
|
||||||
};
|
});
|
||||||
|
|
||||||
const openTerminal = (machine: any, ex?: boolean) => {
|
const autoOpenTerminal = (codePath: string) => {
|
||||||
// 新窗口打开
|
if (!codePath) {
|
||||||
if (ex) {
|
|
||||||
const { href } = router.resolve({
|
|
||||||
path: `/machine/terminal`,
|
|
||||||
query: {
|
|
||||||
id: machine.id,
|
|
||||||
name: machine.name,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
window.open(href, '_blank');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { name, id, username } = machine;
|
const typeAndCodes = getTagTypeCodeByPath(codePath);
|
||||||
|
const tagPath = typeAndCodes[TagResourceTypeEnum.Tag.value].join('/') + '/';
|
||||||
|
|
||||||
|
const machineCode = typeAndCodes[TagResourceTypeEnum.Machine.value][0];
|
||||||
|
state.defaultExpendKey = [tagPath, machineCode];
|
||||||
|
|
||||||
|
const authCertName = typeAndCodes[TagResourceTypeEnum.MachineAuthCert.value][0];
|
||||||
|
setTimeout(() => {
|
||||||
|
// 置空
|
||||||
|
autoOpenResourceStore.setMachineCodePath('');
|
||||||
|
tagTreeRef.value.setCurrentKey(authCertName);
|
||||||
|
|
||||||
|
const acNode = tagTreeRef.value.getNode(authCertName);
|
||||||
|
openTerminal(acNode.data.params);
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openTerminal = (machine: any, ex?: boolean) => {
|
||||||
|
// 授权凭证名
|
||||||
|
const ac = machine.selectAuthCert.name;
|
||||||
|
|
||||||
|
// 新窗口打开
|
||||||
|
if (ex) {
|
||||||
|
if (machine.protocol == MachineProtocolEnum.Ssh.value) {
|
||||||
|
const { href } = router.resolve({
|
||||||
|
path: `/machine/terminal`,
|
||||||
|
query: {
|
||||||
|
ac,
|
||||||
|
name: machine.name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
window.open(href, '_blank');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (machine.protocol == MachineProtocolEnum.Rdp.value) {
|
||||||
|
const { href } = router.resolve({
|
||||||
|
path: `/machine/terminal-rdp`,
|
||||||
|
query: {
|
||||||
|
machineId: machine.id,
|
||||||
|
ac: ac,
|
||||||
|
name: machine.name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
window.open(href, '_blank');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let { name } = machine;
|
||||||
|
const labelName = `${machine.selectAuthCert.username}@${name}`;
|
||||||
|
|
||||||
// 同一个机器的终端打开多次,key后添加下划线和数字区分
|
// 同一个机器的终端打开多次,key后添加下划线和数字区分
|
||||||
openIds[id] = openIds[id] ? ++openIds[id] : 1;
|
openIds[ac] = openIds[ac] ? ++openIds[ac] : 1;
|
||||||
let sameIndex = openIds[id];
|
let sameIndex = openIds[ac];
|
||||||
|
|
||||||
let key = `${id}_${username}_${sameIndex}`;
|
let key = `${ac}_${sameIndex}`;
|
||||||
// 只保留name的10个字,超出部分只保留前后4个字符,中间用省略号代替
|
// 只保留name的15个字,超出部分只保留前后10个字符,中间用省略号代替
|
||||||
let label = name.length > 10 ? name.slice(0, 4) + '...' + name.slice(-4) : name;
|
const label = labelName.length > 15 ? labelName.slice(0, 10) + '...' + labelName.slice(-10) : labelName;
|
||||||
|
|
||||||
state.tabs.set(key, {
|
let tab = {
|
||||||
key,
|
key,
|
||||||
label: `${label}${sameIndex === 1 ? '' : ':' + sameIndex}`, // label组成为:总打开term次数+name+同一个机器打开的次数
|
label: `${label}${sameIndex === 1 ? '' : ':' + sameIndex}`, // label组成为:总打开term次数+name+同一个机器打开的次数
|
||||||
params: machine,
|
params: machine,
|
||||||
socketUrl: getMachineTerminalSocketUrl(id),
|
authCert: ac,
|
||||||
});
|
socketUrl: getMachineTerminalSocketUrl(ac),
|
||||||
|
};
|
||||||
|
|
||||||
|
state.tabs.set(key, tab);
|
||||||
state.activeTermName = key;
|
state.activeTermName = key;
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
handleReconnect(key);
|
handleReconnect(tab);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const serviceManager = (row: any) => {
|
const serviceManager = (row: any) => {
|
||||||
|
const authCert = row.selectAuthCert;
|
||||||
state.serviceDialog.machineId = row.id;
|
state.serviceDialog.machineId = row.id;
|
||||||
state.serviceDialog.visible = true;
|
state.serviceDialog.visible = true;
|
||||||
state.serviceDialog.title = `${row.name} => ${row.ip}`;
|
state.serviceDialog.authCertName = authCert.name;
|
||||||
|
state.serviceDialog.title = `${row.name} => ${authCert.username}@${row.ip}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -302,9 +452,24 @@ const search = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const showFileManage = (selectionData: any) => {
|
const showFileManage = (selectionData: any) => {
|
||||||
state.fileDialog.visible = true;
|
const authCert = selectionData.selectAuthCert;
|
||||||
state.fileDialog.machineId = selectionData.id;
|
if (selectionData.protocol == 1) {
|
||||||
state.fileDialog.title = `${selectionData.name} => ${selectionData.ip}`;
|
state.fileDialog.visible = true;
|
||||||
|
state.fileDialog.protocol = selectionData.protocol;
|
||||||
|
state.fileDialog.machineId = selectionData.id;
|
||||||
|
state.fileDialog.authCertName = authCert.name;
|
||||||
|
state.fileDialog.title = `${selectionData.name} => ${authCert.username}@${selectionData.ip}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectionData.protocol == 2) {
|
||||||
|
state.filesystemDialog.protocol = 2;
|
||||||
|
state.filesystemDialog.machineId = selectionData.id;
|
||||||
|
state.filesystemDialog.authCertName = authCert.name;
|
||||||
|
state.filesystemDialog.fileId = selectionData.id;
|
||||||
|
state.filesystemDialog.path = '/';
|
||||||
|
state.filesystemDialog.title = `远程桌面文件管理`;
|
||||||
|
state.filesystemDialog.visible = true;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const showInfo = (info: any) => {
|
const showInfo = (info: any) => {
|
||||||
@@ -337,7 +502,6 @@ const onRemoveTab = (targetName: string) => {
|
|||||||
} else {
|
} else {
|
||||||
activeTermName = '';
|
activeTermName = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
let info = state.tabs.get(targetName);
|
let info = state.tabs.get(targetName);
|
||||||
if (info) {
|
if (info) {
|
||||||
terminalRefs[info.key]?.close();
|
terminalRefs[info.key]?.close();
|
||||||
@@ -360,26 +524,38 @@ const setTerminalRef = (el: any, key: any) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const terminalWrapperRefs: any = {};
|
||||||
|
const setTerminalWrapperRef = (el: any, key: any) => {
|
||||||
|
if (key) {
|
||||||
|
terminalWrapperRefs[key] = el;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onResizeTagTree = () => {
|
const onResizeTagTree = () => {
|
||||||
fitTerminal();
|
fitTerminal();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onTabChange = () => {
|
const onTabChange = () => {
|
||||||
fitTerminal();
|
fitTerminal();
|
||||||
|
|
||||||
|
const nowTab = state.tabs.get(state.activeTermName);
|
||||||
|
tagTreeRef.value.setCurrentKey(nowTab?.authCert);
|
||||||
};
|
};
|
||||||
|
|
||||||
const fitTerminal = () => {
|
const fitTerminal = () => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
let info = state.tabs.get(state.activeTermName);
|
let info = state.tabs.get(state.activeTermName);
|
||||||
if (info) {
|
if (info) {
|
||||||
terminalRefs[info.key]?.fitTerminal();
|
terminalRefs[info.key]?.fitTerminal && terminalRefs[info.key]?.fitTerminal();
|
||||||
terminalRefs[info.key]?.focus();
|
terminalRefs[info.key]?.focus && terminalRefs[info.key]?.focus();
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReconnect = (key: string) => {
|
const handleReconnect = (tab: any, force = false) => {
|
||||||
terminalRefs[key].init();
|
let width = terminalWrapperRefs[tab.key].offsetWidth;
|
||||||
|
let height = terminalWrapperRefs[tab.key].offsetHeight;
|
||||||
|
terminalRefs[tab.key]?.init(width, height, force);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,13 @@
|
|||||||
:before-close="handleClose"
|
:before-close="handleClose"
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
:destroy-on-close="true"
|
:destroy-on-close="true"
|
||||||
width="800"
|
width="1000"
|
||||||
@open="getTermOps()"
|
@open="getTermOps()"
|
||||||
>
|
>
|
||||||
<page-table ref="pageTableRef" :page-api="machineApi.termOpRecs" :lazy="true" height="100%" v-model:query-form="query" :columns="columns">
|
<page-table ref="pageTableRef" :page-api="machineApi.termOpRecs" :lazy="true" height="100%" v-model:query-form="query" :columns="columns">
|
||||||
<template #action="{ data }">
|
<template #action="{ data }">
|
||||||
<el-button @click="playRec(data)" loading-icon="loading" :loading="data.playRecLoding" type="primary" link>回放</el-button>
|
<el-button @click="playRec(data)" loading-icon="loading" :loading="data.playRecLoding" type="primary" link>回放</el-button>
|
||||||
|
<el-button @click="showExecCmds(data)" type="primary" link>命令</el-button>
|
||||||
</template>
|
</template>
|
||||||
</page-table>
|
</page-table>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
@@ -26,6 +27,17 @@
|
|||||||
>
|
>
|
||||||
<div ref="playerRef" id="rc-player"></div>
|
<div ref="playerRef" id="rc-player"></div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog title="执行命令记录" v-model="execCmdsDialogVisible" :destroy-on-close="true" width="500">
|
||||||
|
<el-table :data="state.execCmds" max-height="480" stripe size="small">
|
||||||
|
<el-table-column prop="cmd" label="命令" show-overflow-tooltip min-width="150px"> </el-table-column>
|
||||||
|
<el-table-column prop="time" label="执行时间" min-width="80" show-overflow-tooltip>
|
||||||
|
<template #default="scope">
|
||||||
|
{{ formatDate(new Date(scope.row.time * 1000).toString()) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -36,6 +48,7 @@ import * as AsciinemaPlayer from 'asciinema-player';
|
|||||||
import 'asciinema-player/dist/bundle/asciinema-player.css';
|
import 'asciinema-player/dist/bundle/asciinema-player.css';
|
||||||
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 { formatDate } from '@/common/utils/format';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
visible: { type: Boolean },
|
visible: { type: Boolean },
|
||||||
@@ -50,7 +63,7 @@ const columns = [
|
|||||||
TableColumn.new('createTime', '开始时间').isTime().setMinWidth(150),
|
TableColumn.new('createTime', '开始时间').isTime().setMinWidth(150),
|
||||||
TableColumn.new('endTime', '结束时间').isTime().setMinWidth(150),
|
TableColumn.new('endTime', '结束时间').isTime().setMinWidth(150),
|
||||||
TableColumn.new('recordFilePath', '文件路径').setMinWidth(200),
|
TableColumn.new('recordFilePath', '文件路径').setMinWidth(200),
|
||||||
TableColumn.new('action', '操作').isSlot().setMinWidth(60).fixedRight().alignCenter(),
|
TableColumn.new('action', '操作').isSlot().setMinWidth(120).fixedRight().alignCenter(),
|
||||||
];
|
];
|
||||||
|
|
||||||
const playerRef = ref(null);
|
const playerRef = ref(null);
|
||||||
@@ -63,11 +76,12 @@ const state = reactive({
|
|||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
machineId: 0,
|
machineId: 0,
|
||||||
},
|
},
|
||||||
|
|
||||||
playerDialogVisible: false,
|
playerDialogVisible: false,
|
||||||
|
execCmdsDialogVisible: false,
|
||||||
|
execCmds: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { dialogVisible, query, playerDialogVisible } = toRefs(state);
|
const { dialogVisible, query, playerDialogVisible, execCmdsDialogVisible } = toRefs(state);
|
||||||
|
|
||||||
watch(props, async (newValue: any) => {
|
watch(props, async (newValue: any) => {
|
||||||
const visible = newValue.visible;
|
const visible = newValue.visible;
|
||||||
@@ -82,6 +96,11 @@ const getTermOps = async () => {
|
|||||||
pageTableRef.value.search();
|
pageTableRef.value.search();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const showExecCmds = (data: any) => {
|
||||||
|
state.execCmds = JSON.parse(data.execCmds);
|
||||||
|
state.execCmdsDialogVisible = true;
|
||||||
|
};
|
||||||
|
|
||||||
let player: any = null;
|
let player: any = null;
|
||||||
|
|
||||||
const playRec = async (rec: any) => {
|
const playRec = async (rec: any) => {
|
||||||
@@ -103,8 +122,8 @@ const playRec = async (rec: any) => {
|
|||||||
idleTimeLimit: 2,
|
idleTimeLimit: 2,
|
||||||
// fit: false,
|
// fit: false,
|
||||||
// terminalFontSize: 'small',
|
// terminalFontSize: 'small',
|
||||||
cols: 144,
|
// cols: 144,
|
||||||
rows: 32,
|
// rows: 32,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
@@ -127,6 +146,12 @@ const handleClose = () => {
|
|||||||
</script>
|
</script>
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
#terminalRecDialog {
|
#terminalRecDialog {
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
#rc-player {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.el-overlay .el-overlay-dialog .el-dialog .el-dialog__body {
|
.el-overlay .el-overlay-dialog .el-dialog .el-dialog__body {
|
||||||
padding: 0px !important;
|
padding: 0px !important;
|
||||||
}
|
}
|
||||||
|
|||||||
34
mayfly_go_web/src/views/ops/machine/RdpTerminalPage.vue
Normal file
34
mayfly_go_web/src/views/ops/machine/RdpTerminalPage.vue
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<template>
|
||||||
|
<div class="terminal-wrapper" ref="terminalWrapperRef">
|
||||||
|
<machine-rdp ref="rdpRef" :auth-cert="state.authCert" :machine-id="state.machineId" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import MachineRdp from '@/components/terminal-rdp/MachineRdp.vue';
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
import { TerminalExpose } from '@/components/terminal-rdp';
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const rdpRef = ref({} as TerminalExpose);
|
||||||
|
const terminalWrapperRef = ref({} as any);
|
||||||
|
|
||||||
|
const state = computed(() => {
|
||||||
|
return {
|
||||||
|
authCert: route.query.ac as string,
|
||||||
|
machineId: Number(route.query.machineId),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
let width = terminalWrapperRef.value.clientWidth;
|
||||||
|
let height = terminalWrapperRef.value.clientHeight;
|
||||||
|
rdpRef.value?.init(width, height, false);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style lang="scss">
|
||||||
|
.terminal-wrapper {
|
||||||
|
height: calc(100vh);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
draggable
|
draggable
|
||||||
append-to-body
|
append-to-body
|
||||||
>
|
>
|
||||||
<TerminalBody ref="terminal" :cmd="terminalDialog.cmd" :socket-url="getMachineTerminalSocketUrl(terminalDialog.machineId)" height="560px" />
|
<TerminalBody ref="terminal" :cmd="terminalDialog.cmd" :socket-url="getMachineTerminalSocketUrl(props.authCertName)" height="560px" />
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<script-edit
|
<script-edit
|
||||||
@@ -100,6 +100,7 @@ import { SearchItem } from '@/components/SearchForm';
|
|||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
visible: { type: Boolean },
|
visible: { type: Boolean },
|
||||||
machineId: { type: Number },
|
machineId: { type: Number },
|
||||||
|
authCertName: { type: String },
|
||||||
title: { type: String },
|
title: { type: String },
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -143,7 +144,6 @@ const state = reactive({
|
|||||||
terminalDialog: {
|
terminalDialog: {
|
||||||
visible: false,
|
visible: false,
|
||||||
cmd: '',
|
cmd: '',
|
||||||
machineId: 0,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -195,6 +195,7 @@ const run = async (script: any) => {
|
|||||||
if (script.type == ScriptResultEnum.Result.value || noResult) {
|
if (script.type == ScriptResultEnum.Result.value || noResult) {
|
||||||
const res = await machineApi.runScript.request({
|
const res = await machineApi.runScript.request({
|
||||||
machineId: props.machineId,
|
machineId: props.machineId,
|
||||||
|
ac: props.authCertName,
|
||||||
scriptId: script.id,
|
scriptId: script.id,
|
||||||
params: JSON.stringify(state.scriptParamsDialog.params),
|
params: JSON.stringify(state.scriptParamsDialog.params),
|
||||||
});
|
});
|
||||||
@@ -215,7 +216,6 @@ const run = async (script: any) => {
|
|||||||
}
|
}
|
||||||
state.terminalDialog.cmd = script;
|
state.terminalDialog.cmd = script;
|
||||||
state.terminalDialog.visible = true;
|
state.terminalDialog.visible = true;
|
||||||
state.terminalDialog.machineId = props.machineId as any;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -236,7 +236,6 @@ function templateResolve(template: string, param: any) {
|
|||||||
|
|
||||||
const closeTermnial = () => {
|
const closeTermnial = () => {
|
||||||
state.terminalDialog.visible = false;
|
state.terminalDialog.visible = false;
|
||||||
state.terminalDialog.machineId = 0;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const editScript = (data: any) => {
|
const editScript = (data: any) => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="terminal-wrapper">
|
<div class="terminal-wrapper">
|
||||||
<TerminalBody :socket-url="getMachineTerminalSocketUrl(route.query.id)" />
|
<TerminalBody :socket-url="getMachineTerminalSocketUrl(route.query.ac)" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const machineApi = {
|
|||||||
// 删除机器
|
// 删除机器
|
||||||
del: Api.newDelete('/machines/{id}'),
|
del: Api.newDelete('/machines/{id}'),
|
||||||
scripts: Api.newGet('/machines/{machineId}/scripts'),
|
scripts: Api.newGet('/machines/{machineId}/scripts'),
|
||||||
runScript: Api.newGet('/machines/{machineId}/scripts/{scriptId}/run'),
|
runScript: Api.newGet('/machines/scripts/{scriptId}/{ac}/run'),
|
||||||
saveScript: Api.newPost('/machines/{machineId}/scripts'),
|
saveScript: Api.newPost('/machines/{machineId}/scripts'),
|
||||||
deleteScript: Api.newDelete('/machines/{machineId}/scripts/{scriptId}'),
|
deleteScript: Api.newDelete('/machines/{machineId}/scripts/{scriptId}'),
|
||||||
// 获取配置文件列表
|
// 获取配置文件列表
|
||||||
@@ -42,20 +42,12 @@ export const machineApi = {
|
|||||||
addConf: Api.newPost('/machines/{machineId}/files'),
|
addConf: Api.newPost('/machines/{machineId}/files'),
|
||||||
// 删除配置的文件or目录
|
// 删除配置的文件or目录
|
||||||
delConf: Api.newDelete('/machines/{machineId}/files/{id}'),
|
delConf: Api.newDelete('/machines/{machineId}/files/{id}'),
|
||||||
terminal: Api.newGet('/api/machines/{id}/terminal'),
|
|
||||||
// 机器终端操作记录列表
|
// 机器终端操作记录列表
|
||||||
termOpRecs: Api.newGet('/machines/{machineId}/term-recs'),
|
termOpRecs: Api.newGet('/machines/{machineId}/term-recs'),
|
||||||
// 机器终端操作记录详情
|
// 机器终端操作记录详情
|
||||||
termOpRec: Api.newGet('/machines/{id}/term-recs/{recId}'),
|
termOpRec: Api.newGet('/machines/{id}/term-recs/{recId}'),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const authCertApi = {
|
|
||||||
baseList: Api.newGet('/sys/authcerts/base'),
|
|
||||||
list: Api.newGet('/sys/authcerts'),
|
|
||||||
save: Api.newPost('/sys/authcerts'),
|
|
||||||
delete: Api.newDelete('/sys/authcerts/{id}'),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const cronJobApi = {
|
export const cronJobApi = {
|
||||||
list: Api.newGet('/machine-cronjobs'),
|
list: Api.newGet('/machine-cronjobs'),
|
||||||
relateMachineIds: Api.newGet('/machine-cronjobs/machine-ids'),
|
relateMachineIds: Api.newGet('/machine-cronjobs/machine-ids'),
|
||||||
@@ -66,6 +58,16 @@ export const cronJobApi = {
|
|||||||
execList: Api.newGet('/machine-cronjobs/execs'),
|
execList: Api.newGet('/machine-cronjobs/execs'),
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getMachineTerminalSocketUrl(machineId: any) {
|
export const cmdConfApi = {
|
||||||
return `${config.baseWsUrl}/machines/${machineId}/terminal?${joinClientParams()}`;
|
list: Api.newGet('/machine/security/cmd-confs'),
|
||||||
|
save: Api.newPost('/machine/security/cmd-confs'),
|
||||||
|
delete: Api.newDelete('/machine/security/cmd-confs/{id}'),
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getMachineTerminalSocketUrl(authCertName: any) {
|
||||||
|
return `${config.baseWsUrl}/machines/terminal/${authCertName}?${joinClientParams()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMachineRdpSocketUrl(authCertName: any) {
|
||||||
|
return `${config.baseWsUrl}/machines/rdp/${authCertName}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,118 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<el-dialog :title="title" v-model="dvisible" :show-close="false" :before-close="cancel" width="500px" :destroy-on-close="true">
|
|
||||||
<el-form ref="acForm" :rules="rules" :model="form" label-width="auto">
|
|
||||||
<el-form-item prop="name" label="名称" required>
|
|
||||||
<el-input v-model="form.name"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item prop="authMethod" label="认证方式" required>
|
|
||||||
<el-select style="width: 100%" v-model="form.authMethod" placeholder="请选择认证方式">
|
|
||||||
<el-option key="1" label="密码" :value="1"> </el-option>
|
|
||||||
<el-option key="2" label="密钥" :value="2"> </el-option>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item v-if="form.authMethod == 1" prop="password" label="密码">
|
|
||||||
<el-input type="password" show-password clearable v-model.trim="form.password" placeholder="请输入密码" autocomplete="new-password">
|
|
||||||
</el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item v-if="form.authMethod == 2" prop="password" label="秘钥">
|
|
||||||
<el-input type="textarea" :rows="5" v-model="form.password" placeholder="请将私钥文件内容拷贝至此"> </el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item v-if="form.authMethod == 2" prop="passphrase" label="秘钥密码">
|
|
||||||
<el-input type="password" v-model="form.passphrase"> </el-input>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<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="cancel()">取 消</el-button>
|
|
||||||
<el-button type="primary" :loading="btnLoading" @click="btnOk">确 定</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { ref, toRefs, reactive, watch } from 'vue';
|
|
||||||
import { authCertApi } from '../api';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
visible: {
|
|
||||||
type: Boolean,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
type: [Boolean, Object],
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
//定义事件
|
|
||||||
const emit = defineEmits(['update:visible', 'cancel', 'val-change']);
|
|
||||||
|
|
||||||
const acForm: any = ref(null);
|
|
||||||
|
|
||||||
const rules = {
|
|
||||||
name: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: '授权凭证名称不能为空',
|
|
||||||
trigger: ['change', 'blur'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const state = reactive({
|
|
||||||
dvisible: false,
|
|
||||||
params: [] as any,
|
|
||||||
form: {
|
|
||||||
id: null,
|
|
||||||
name: '',
|
|
||||||
authMethod: 1,
|
|
||||||
password: '',
|
|
||||||
passphrase: '',
|
|
||||||
remark: '',
|
|
||||||
},
|
|
||||||
btnLoading: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { dvisible, form, btnLoading } = toRefs(state);
|
|
||||||
|
|
||||||
watch(props, (newValue: any) => {
|
|
||||||
state.dvisible = newValue.visible;
|
|
||||||
if (newValue.data) {
|
|
||||||
state.form = { ...newValue.data };
|
|
||||||
} else {
|
|
||||||
state.form = { authMethod: 1 } as any;
|
|
||||||
state.params = [];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const cancel = () => {
|
|
||||||
// 更新父组件visible prop对应的值为false
|
|
||||||
emit('update:visible', false);
|
|
||||||
// 若父组件有取消事件,则调用
|
|
||||||
emit('cancel');
|
|
||||||
};
|
|
||||||
|
|
||||||
const btnOk = async () => {
|
|
||||||
acForm.value.validate(async (valid: boolean) => {
|
|
||||||
if (valid) {
|
|
||||||
state.btnLoading = true;
|
|
||||||
try {
|
|
||||||
await authCertApi.save.request(state.form);
|
|
||||||
emit('val-change', state.form);
|
|
||||||
cancel();
|
|
||||||
} finally {
|
|
||||||
state.btnLoading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
<style lang="scss"></style>
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<page-table
|
|
||||||
ref="pageTableRef"
|
|
||||||
:page-api="authCertApi.list"
|
|
||||||
:search-items="state.searchItems"
|
|
||||||
v-model:query-form="query"
|
|
||||||
:show-selection="true"
|
|
||||||
v-model:selection-data="selectionData"
|
|
||||||
:columns="state.columns"
|
|
||||||
>
|
|
||||||
<template #tableHeader>
|
|
||||||
<el-button type="primary" icon="plus" @click="edit(false)">添加</el-button>
|
|
||||||
<el-button :disabled="selectionData.length < 1" @click="deleteAc(selectionData)" type="danger" icon="delete">删除 </el-button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #action="{ data }">
|
|
||||||
<el-button @click="edit(data)" type="primary" link>编辑 </el-button>
|
|
||||||
</template>
|
|
||||||
</page-table>
|
|
||||||
|
|
||||||
<auth-cert-edit :title="editor.title" v-model:visible="editor.visible" :data="editor.authcert" @val-change="editChange" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { toRefs, reactive, onMounted, ref, Ref } from 'vue';
|
|
||||||
import AuthCertEdit from './AuthCertEdit.vue';
|
|
||||||
import { authCertApi } from '../api';
|
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
|
||||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
|
||||||
import { TableColumn } from '@/components/pagetable';
|
|
||||||
import { AuthMethodEnum } from '../enums';
|
|
||||||
import { SearchItem } from '@/components/SearchForm';
|
|
||||||
|
|
||||||
const pageTableRef: Ref<any> = ref(null);
|
|
||||||
const state = reactive({
|
|
||||||
query: {
|
|
||||||
pageNum: 1,
|
|
||||||
pageSize: 0,
|
|
||||||
name: null,
|
|
||||||
},
|
|
||||||
searchItems: [SearchItem.input('name', '凭证名称')],
|
|
||||||
columns: [
|
|
||||||
TableColumn.new('name', '名称'),
|
|
||||||
TableColumn.new('authMethod', '认证方式').typeTag(AuthMethodEnum),
|
|
||||||
TableColumn.new('remark', '备注'),
|
|
||||||
TableColumn.new('creator', '创建人'),
|
|
||||||
TableColumn.new('createTime', '创建时间').isTime(),
|
|
||||||
TableColumn.new('creator', '修改者'),
|
|
||||||
TableColumn.new('createTime', '修改时间').isTime(),
|
|
||||||
TableColumn.new('action', '操作').isSlot().fixedRight().setMinWidth(65).alignCenter(),
|
|
||||||
],
|
|
||||||
selectionData: [],
|
|
||||||
paramsDialog: {
|
|
||||||
visible: false,
|
|
||||||
config: null as any,
|
|
||||||
params: {},
|
|
||||||
paramsFormItem: [] as any,
|
|
||||||
},
|
|
||||||
editor: {
|
|
||||||
title: '授权凭证保存',
|
|
||||||
visible: false,
|
|
||||||
authcert: {},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { query, selectionData, editor } = toRefs(state);
|
|
||||||
|
|
||||||
onMounted(() => {});
|
|
||||||
|
|
||||||
const search = async () => {
|
|
||||||
pageTableRef.value.search();
|
|
||||||
};
|
|
||||||
|
|
||||||
const editChange = () => {
|
|
||||||
ElMessage.success('保存成功');
|
|
||||||
search();
|
|
||||||
};
|
|
||||||
|
|
||||||
const edit = (data: any) => {
|
|
||||||
if (data) {
|
|
||||||
state.editor.authcert = data;
|
|
||||||
} else {
|
|
||||||
state.editor.authcert = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.editor.visible = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteAc = async (data: any) => {
|
|
||||||
try {
|
|
||||||
await ElMessageBox.confirm(`确定删除该【${data.map((x: any) => x.name).join(', ')}授权凭证?`, '提示', {
|
|
||||||
confirmButtonText: '确定',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning',
|
|
||||||
});
|
|
||||||
await authCertApi.delete.request({ id: data.map((x: any) => x.id).join(',') });
|
|
||||||
ElMessage.success('删除成功');
|
|
||||||
search();
|
|
||||||
} catch (err) {
|
|
||||||
//
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
<style lang="scss"></style>
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div style="width: 100%">
|
|
||||||
<el-select @change="changeValue" v-model="id" filterable placeholder="请选择授权凭证,可前往[机器管理->授权凭证]添加" style="width: 100%">
|
|
||||||
<el-option v-for="ac in acs" :key="ac.id" :value="ac.id" :label="ac.name">
|
|
||||||
<el-tag v-if="ac.authMethod == 1" type="success" size="small">密码</el-tag>
|
|
||||||
<el-tag v-if="ac.authMethod == 2" size="small">密钥</el-tag>
|
|
||||||
|
|
||||||
<el-divider direction="vertical" border-style="dashed" />
|
|
||||||
{{ ac.name }}
|
|
||||||
|
|
||||||
<el-divider direction="vertical" border-style="dashed" />
|
|
||||||
{{ ac.remark }}
|
|
||||||
</el-option>
|
|
||||||
</el-select>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { reactive, toRefs, onMounted } from 'vue';
|
|
||||||
import { authCertApi } from '../api';
|
|
||||||
|
|
||||||
//定义事件
|
|
||||||
const emit = defineEmits(['update:modelValue', 'change']);
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
modelValue: {
|
|
||||||
type: [Number],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const state = reactive({
|
|
||||||
acs: [] as any,
|
|
||||||
id: null as any,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { acs, id } = toRefs(state);
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await getAcs();
|
|
||||||
if (props.modelValue) {
|
|
||||||
state.id = props.modelValue;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const changeValue = (val: any) => {
|
|
||||||
emit('update:modelValue', val);
|
|
||||||
emit('change', val);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAcs = async () => {
|
|
||||||
const acs = await authCertApi.baseList.request({ pageSize: 100, type: 2 });
|
|
||||||
state.acs = acs.list;
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss"></style>
|
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-popover placement="right" width="auto" title="机器详情" trigger="click">
|
||||||
|
<template #reference>
|
||||||
|
<el-link @click="getMachineDetail" type="primary">{{ props.code }}</el-link>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-descriptions v-loading="state.loading" :column="3" border>
|
||||||
|
<el-descriptions-item :span="1" label="机器id">{{ state.machineDetail.id }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item :span="1" label="编号">{{ state.machineDetail.code }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item :span="1" label="名称">{{ state.machineDetail.name }}</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item :span="3" label="关联标签"><ResourceTags :tags="state.machineDetail.tags" /></el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item :span="2" label="IP">{{ state.machineDetail.ip }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item :span="1" label="端口">{{ state.machineDetail.port }}</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item :span="3" label="备注">{{ state.machineDetail.remark }}</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item :span="1.5" label="SSH隧道">{{ state.machineDetail.sshTunnelMachineId > 0 ? '是' : '否' }} </el-descriptions-item>
|
||||||
|
<el-descriptions-item :span="1.5" label="终端回放">{{ state.machineDetail.enableRecorder == 1 ? '是' : '否' }} </el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item :span="2" label="创建时间">{{ formatDate(state.machineDetail.createTime) }} </el-descriptions-item>
|
||||||
|
<el-descriptions-item :span="1" label="创建者">{{ state.machineDetail.creator }}</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item :span="2" label="更新时间">{{ formatDate(state.machineDetail.updateTime) }} </el-descriptions-item>
|
||||||
|
<el-descriptions-item :span="1" label="修改者">{{ state.machineDetail.modifier }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-popover>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { reactive } from 'vue';
|
||||||
|
import { machineApi } from '../api';
|
||||||
|
import { formatDate } from '@/common/utils/format';
|
||||||
|
import ResourceTags from '../../component/ResourceTags.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
code: {
|
||||||
|
type: [String],
|
||||||
|
requierd: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
loading: false,
|
||||||
|
machineDetail: {} as any,
|
||||||
|
});
|
||||||
|
|
||||||
|
const getMachineDetail = async () => {
|
||||||
|
try {
|
||||||
|
state.machineDetail = {};
|
||||||
|
state.loading = true;
|
||||||
|
const res = await machineApi.list.request({
|
||||||
|
code: props.code,
|
||||||
|
});
|
||||||
|
if (res.total == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.machineDetail = res.list?.[0];
|
||||||
|
} finally {
|
||||||
|
state.loading = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
@@ -1,14 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mock-data-dialog">
|
<div class="mock-data-dialog">
|
||||||
<el-dialog
|
<el-drawer
|
||||||
:title="title"
|
:title="title"
|
||||||
v-model="dialogVisible"
|
v-model="dialogVisible"
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
:before-close="cancel"
|
:before-close="cancel"
|
||||||
:show-close="true"
|
:show-close="true"
|
||||||
:destroy-on-close="true"
|
:destroy-on-close="true"
|
||||||
width="900px"
|
size="40%"
|
||||||
>
|
>
|
||||||
|
<template #header>
|
||||||
|
<DrawerHeader :header="title" :back="cancel" />
|
||||||
|
</template>
|
||||||
|
|
||||||
<el-form :model="form" ref="formRef" :rules="rules" label-width="auto">
|
<el-form :model="form" ref="formRef" :rules="rules" label-width="auto">
|
||||||
<el-form-item prop="name" label="名称">
|
<el-form-item prop="name" label="名称">
|
||||||
<el-input v-model="form.name" placeholder="请输入名称"></el-input>
|
<el-input v-model="form.name" placeholder="请输入名称"></el-input>
|
||||||
@@ -34,19 +38,13 @@
|
|||||||
<el-input v-model="form.remark" placeholder="请输入备注"></el-input>
|
<el-input v-model="form.remark" placeholder="请输入备注"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item prop="machineIds" label="关联机器">
|
|
||||||
<el-select multiple v-model="form.machineIds" filterable placeholder="请选关联机器" style="width: 100%">
|
|
||||||
<el-option v-for="ac in state.machines" :key="ac.id" :value="ac.id" :label="ac.ip">
|
|
||||||
{{ ac.ip }}
|
|
||||||
<el-divider direction="vertical" border-style="dashed" />
|
|
||||||
{{ ac.tagPath }}{{ ac.name }}
|
|
||||||
</el-option>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item prop="script" label="执行脚本" required>
|
<el-form-item prop="script" label="执行脚本" required>
|
||||||
<monaco-editor style="width: 100%" v-model="form.script" language="shell" height="300px"
|
<monaco-editor style="width: 100%" v-model="form.script" language="shell" height="200px"
|
||||||
/></el-form-item>
|
/></el-form-item>
|
||||||
|
|
||||||
|
<el-form-item ref="tagSelectRef" prop="codePaths" label="关联机器">
|
||||||
|
<tag-tree-check height="200px" :tag-type="TagResourceTypeEnum.Machine.value" v-model="form.codePaths" />
|
||||||
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -55,7 +53,7 @@
|
|||||||
<el-button v-auth="'machine:script:save'" type="primary" :loading="btnLoading" @click="btnOk" :disabled="submitDisabled">保 存</el-button>
|
<el-button v-auth="'machine:script:save'" type="primary" :loading="btnLoading" @click="btnOk" :disabled="submitDisabled">保 存</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-drawer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -67,6 +65,9 @@ import { CronJobStatusEnum, CronJobSaveExecResTypeEnum } from '../enums';
|
|||||||
import { notEmpty } from '@/common/assert';
|
import { notEmpty } from '@/common/assert';
|
||||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||||
import CrontabInput from '@/components/crontab/CrontabInput.vue';
|
import CrontabInput from '@/components/crontab/CrontabInput.vue';
|
||||||
|
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||||
|
import TagTreeCheck from '../../component/TagTreeCheck.vue';
|
||||||
|
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
visible: {
|
visible: {
|
||||||
@@ -130,11 +131,11 @@ const state = reactive({
|
|||||||
id: null,
|
id: null,
|
||||||
name: '',
|
name: '',
|
||||||
cron: '',
|
cron: '',
|
||||||
machineIds: [],
|
|
||||||
remark: '',
|
remark: '',
|
||||||
script: '',
|
script: '',
|
||||||
status: 1,
|
status: 1,
|
||||||
saveExecResType: -1,
|
saveExecResType: -1,
|
||||||
|
codePaths: [],
|
||||||
},
|
},
|
||||||
machines: [] as any,
|
machines: [] as any,
|
||||||
btnLoading: false,
|
btnLoading: false,
|
||||||
@@ -154,7 +155,7 @@ watch(props, async (newValue: any) => {
|
|||||||
}
|
}
|
||||||
if (newValue.data) {
|
if (newValue.data) {
|
||||||
state.form = { ...newValue.data };
|
state.form = { ...newValue.data };
|
||||||
state.form.machineIds = await cronJobApi.relateMachineIds.request({ cronJobId: state.form.id });
|
state.form.codePaths = newValue.data.tags?.map((tag: any) => tag.codePath);
|
||||||
} else {
|
} else {
|
||||||
state.form = { script: '', status: 1 } as any;
|
state.form = { script: '', status: 1 } as any;
|
||||||
state.chooseMachines = [];
|
state.chooseMachines = [];
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user