mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-02 23:40:24 +08:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2118acf244 | ||
|
|
44a1bd626e | ||
|
|
ea3c70a8a8 | ||
|
|
6343173cf8 | ||
|
|
6837a9c867 | ||
|
|
a726927a28 | ||
|
|
e135e4ce64 | ||
|
|
43edef412c | ||
|
|
2deb3109c2 | ||
|
|
a80221a950 | ||
|
|
10630847df | ||
|
|
f43851698e | ||
|
|
73884bb693 | ||
|
|
1b5bb1de8b | ||
|
|
4814793546 | ||
|
|
d85bbff270 | ||
|
|
bb1522f4dc | ||
|
|
a7632fbf58 |
@@ -10,7 +10,7 @@ RUN yarn config set registry 'https://registry.npmmirror.com' && \
|
||||
yarn build
|
||||
|
||||
# 构建后端资源
|
||||
FROM golang:1.22 as be-builder
|
||||
FROM golang:1.23 as be-builder
|
||||
|
||||
ENV GOPROXY https://goproxy.cn
|
||||
WORKDIR /mayfly
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
### 介绍
|
||||
|
||||
web 版 **linux(终端[终端回放、命令过滤] 文件 脚本 进程 计划任务)、数据库(mysql postgres oracle sqlserver 达梦 高斯 sqlite)数据同步 数据迁移、redis(单机 哨兵 集群)、mongo 等集工单流程审批于一体的统一管理操作平台**
|
||||
web 版 **linux(终端[终端回放、命令过滤] 文件 脚本 进程 计划任务)、数据库(mysql postgres oracle sqlserver 达梦 高斯 sqlite)数据操作 数据同步 数据迁移、redis(单机 哨兵 集群)、mongo 等集工单流程审批于一体的统一管理操作平台**
|
||||
|
||||
### 开发语言与主要框架
|
||||
|
||||
|
||||
@@ -5,4 +5,6 @@ VITE_PORT = 8889
|
||||
VITE_OPEN = false
|
||||
|
||||
# public path 配置线上环境路径(打包)
|
||||
VITE_PUBLIC_PATH = ''
|
||||
VITE_PUBLIC_PATH = ''
|
||||
|
||||
VITE_EDITOR=idea
|
||||
@@ -1,68 +1,72 @@
|
||||
{
|
||||
"name": "mayfly",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"build-preview": "npm run build && npm run preview",
|
||||
"lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@vueuse/core": "^10.9.0",
|
||||
"asciinema-player": "^3.7.1",
|
||||
"axios": "^1.6.2",
|
||||
"clipboard": "^2.0.11",
|
||||
"cropperjs": "^1.6.1",
|
||||
"dayjs": "^1.11.11",
|
||||
"echarts": "^5.5.0",
|
||||
"element-plus": "^2.7.3",
|
||||
"js-base64": "^3.7.7",
|
||||
"jsencrypt": "^3.3.2",
|
||||
"lodash": "^4.17.21",
|
||||
"mitt": "^3.0.1",
|
||||
"monaco-editor": "^0.48.0",
|
||||
"monaco-sql-languages": "^0.11.0",
|
||||
"monaco-themes": "^0.4.4",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^2.1.7",
|
||||
"qrcode.vue": "^3.4.1",
|
||||
"screenfull": "^6.0.2",
|
||||
"sortablejs": "^1.15.2",
|
||||
"splitpanes": "^3.1.5",
|
||||
"sql-formatter": "^15.0.2",
|
||||
"trzsz": "^1.1.5",
|
||||
"uuid": "^9.0.1",
|
||||
"vue": "^3.4.27",
|
||||
"vue-router": "^4.3.2",
|
||||
"xterm": "^5.3.0",
|
||||
"xterm-addon-fit": "^0.8.0",
|
||||
"xterm-addon-search": "^0.13.0",
|
||||
"xterm-addon-web-links": "^0.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash": "^4.14.178",
|
||||
"@types/node": "^18.14.0",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/sortablejs": "^1.15.8",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
||||
"@typescript-eslint/parser": "^6.7.4",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vue/compiler-sfc": "^3.4.27",
|
||||
"code-inspector-plugin": "^0.4.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint": "^8.35.0",
|
||||
"eslint-plugin-vue": "^9.25.0",
|
||||
"prettier": "^3.2.5",
|
||||
"sass": "^1.77.1",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.2.11",
|
||||
"vue-eslint-parser": "^9.4.2"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead"
|
||||
]
|
||||
}
|
||||
"name": "mayfly",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"build-preview": "npm run build && npm run preview",
|
||||
"lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@vueuse/core": "^11.1.0",
|
||||
"asciinema-player": "^3.8.1",
|
||||
"axios": "^1.6.2",
|
||||
"clipboard": "^2.0.11",
|
||||
"cropperjs": "^1.6.1",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"echarts": "^5.5.1",
|
||||
"element-plus": "^2.8.6",
|
||||
"js-base64": "^3.7.7",
|
||||
"jsencrypt": "^3.3.2",
|
||||
"lodash": "^4.17.21",
|
||||
"mitt": "^3.0.1",
|
||||
"monaco-editor": "^0.52.0",
|
||||
"monaco-sql-languages": "^0.12.2",
|
||||
"monaco-themes": "^0.4.4",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^2.2.4",
|
||||
"qrcode.vue": "^3.5.0",
|
||||
"screenfull": "^6.0.2",
|
||||
"sortablejs": "^1.15.3",
|
||||
"splitpanes": "^3.1.5",
|
||||
"sql-formatter": "^15.4.5",
|
||||
"trzsz": "^1.1.5",
|
||||
"uuid": "^9.0.1",
|
||||
"vue": "^3.5.12",
|
||||
"vue-router": "^4.4.5",
|
||||
"xterm": "^5.3.0",
|
||||
"xterm-addon-fit": "^0.8.0",
|
||||
"xterm-addon-search": "^0.13.0",
|
||||
"xterm-addon-web-links": "^0.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/lodash": "^4.14.178",
|
||||
"@types/node": "^18.14.0",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/sortablejs": "^1.15.8",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
||||
"@typescript-eslint/parser": "^6.7.4",
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
"@vue/compiler-sfc": "^3.5.12",
|
||||
"code-inspector-plugin": "^0.4.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint": "^8.35.0",
|
||||
"eslint-plugin-vue": "^9.28.0",
|
||||
"prettier": "^3.2.5",
|
||||
"sass": "^1.80.3",
|
||||
"typescript": "^5.6.3",
|
||||
"vite": "^5.4.10",
|
||||
"vue-eslint-parser": "^9.4.3"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ class Api {
|
||||
*/
|
||||
async xhrReq(param: any = null, options: any = {}): Promise<any> {
|
||||
if (this.beforeHandler) {
|
||||
this.beforeHandler(param);
|
||||
await this.beforeHandler(param);
|
||||
}
|
||||
return request.xhrReq(this.method, this.url, param, options);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ const config = {
|
||||
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
|
||||
|
||||
// 系统版本
|
||||
version: 'v1.8.5',
|
||||
version: 'v1.9.0',
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
38
mayfly_go_web/src/common/crypto.ts
Normal file
38
mayfly_go_web/src/common/crypto.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import CryptoJS from 'crypto-js';
|
||||
import { getToken } from '@/common/utils/storage';
|
||||
|
||||
/**
|
||||
* AES 加密数据
|
||||
* @param word
|
||||
* @param key
|
||||
*/
|
||||
export function AesEncrypt(word: string, key?: string) {
|
||||
if (!key) {
|
||||
key = getToken().substring(0, 24);
|
||||
}
|
||||
|
||||
const sKey = CryptoJS.enc.Utf8.parse(key);
|
||||
const encrypted = CryptoJS.AES.encrypt(word, sKey, {
|
||||
iv: sKey,
|
||||
mode: CryptoJS.mode.CBC,
|
||||
padding: CryptoJS.pad.Pkcs7,
|
||||
});
|
||||
|
||||
return encrypted.ciphertext.toString(CryptoJS.enc.Base64);
|
||||
}
|
||||
|
||||
export function AesDecrypt(word: string, key?: string): string {
|
||||
if (!key) {
|
||||
key = getToken().substring(0, 24);
|
||||
}
|
||||
|
||||
const sKey = CryptoJS.enc.Utf8.parse(key);
|
||||
// key 和 iv 使用同一个值
|
||||
const decrypted = CryptoJS.AES.decrypt(word, sKey, {
|
||||
iv: sKey,
|
||||
mode: CryptoJS.mode.CBC, // CBC算法
|
||||
padding: CryptoJS.pad.Pkcs7, //使用pkcs7 进行padding 后端需要注意
|
||||
});
|
||||
|
||||
return decrypted.toString(CryptoJS.enc.Base64);
|
||||
}
|
||||
@@ -14,4 +14,5 @@ export default {
|
||||
oauth2Callback: (params: any) => request.get('/auth/oauth2/callback', params),
|
||||
getLdapEnabled: () => request.get('/auth/ldap/enabled'),
|
||||
ldapLogin: (param: any) => request.post('/auth/ldap/login', param),
|
||||
getFileDetail: (keys: string[]) => request.get(`/sys/files/detail/${keys.join(',')}`),
|
||||
};
|
||||
|
||||
@@ -209,6 +209,36 @@ export function joinClientParams(): string {
|
||||
return `token=${getToken()}&clientId=${getClientId()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件url地址
|
||||
* @param key 文件key
|
||||
* @returns 文件url
|
||||
*/
|
||||
export function getFileUrl(key: string) {
|
||||
return `${baseUrl}/sys/files/${key}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统文件上传url
|
||||
* @param key 文件key
|
||||
* @returns 文件上传url
|
||||
*/
|
||||
export function getUploadFileUrl(key: string = '') {
|
||||
return `${baseUrl}/sys/files/upload?token=${getToken()}&fileKey=${key}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文件
|
||||
* @param key 文件key
|
||||
*/
|
||||
export function downloadFile(key: string) {
|
||||
const a = document.createElement('a');
|
||||
a.setAttribute('href', `${getFileUrl(key)}`);
|
||||
a.setAttribute('target', '_blank');
|
||||
a.click();
|
||||
a.remove();
|
||||
}
|
||||
|
||||
function parseResult(result: Result) {
|
||||
if (result.code === ResultEnum.SUCCESS) {
|
||||
return result.data;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import Config from './config';
|
||||
import { ElNotification } from 'element-plus';
|
||||
import {ElNotification} from 'element-plus';
|
||||
import SocketBuilder from './SocketBuilder';
|
||||
import { getToken } from '@/common/utils/storage';
|
||||
import {getToken} from '@/common/utils/storage';
|
||||
|
||||
import { joinClientParams } from './request';
|
||||
import {joinClientParams} from './request';
|
||||
|
||||
class SysSocket {
|
||||
/**
|
||||
@@ -19,10 +19,11 @@ class SysSocket {
|
||||
/**
|
||||
* 消息类型
|
||||
*/
|
||||
messageTypes = {
|
||||
messageTypes: any = {
|
||||
0: 'error',
|
||||
1: 'success',
|
||||
2: 'info',
|
||||
22: 'info',
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -57,12 +58,20 @@ class SysSocket {
|
||||
}
|
||||
|
||||
const type = this.getMsgType(message.type);
|
||||
let msg = message.msg
|
||||
let duration = 0
|
||||
if (message.type == 22) {
|
||||
let obj = JSON.parse(msg);
|
||||
msg = `文件:${obj['title']} 执行成功: ${obj['executedStatements']} 条`
|
||||
duration = 2000
|
||||
}
|
||||
ElNotification({
|
||||
duration: 0,
|
||||
duration: duration,
|
||||
title: message.title,
|
||||
message: message.msg,
|
||||
message: msg,
|
||||
type: type,
|
||||
});
|
||||
console.log(message)
|
||||
})
|
||||
.open((event: any) => console.log(event))
|
||||
.close(() => {
|
||||
|
||||
@@ -9,7 +9,19 @@ export function getValueByPath(obj: any, path: string) {
|
||||
const keys = path.split('.');
|
||||
let result = obj;
|
||||
for (let key of keys) {
|
||||
if (!result || typeof result !== 'object') {
|
||||
if (!result) {
|
||||
return undefined;
|
||||
}
|
||||
// 如果是字符串,则尝试使用json解析
|
||||
if (typeof result == 'string') {
|
||||
try {
|
||||
result = JSON.parse(result);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
if (typeof result !== 'object') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -23,7 +35,18 @@ export function getValueByPath(obj: any, path: string) {
|
||||
}
|
||||
|
||||
const index = parseInt(matchIndex[1]);
|
||||
result = Array.isArray(result[arrayKey]) ? result[arrayKey][index] : undefined;
|
||||
|
||||
let arrValue = result[arrayKey];
|
||||
if (typeof arrValue == 'string') {
|
||||
try {
|
||||
arrValue = JSON.parse(arrValue);
|
||||
} catch (e) {
|
||||
result = undefined;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
result = Array.isArray(arrValue) ? arrValue[index] : undefined;
|
||||
} else {
|
||||
result = result[key];
|
||||
}
|
||||
|
||||
@@ -97,43 +97,6 @@ export function getTextWidth(str: string) {
|
||||
return width;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取内容所需要占用的宽度
|
||||
*/
|
||||
export function getContentWidth(content: any): number {
|
||||
if (!content) {
|
||||
return 50;
|
||||
}
|
||||
// 以下分配的单位长度可根据实际需求进行调整
|
||||
let flexWidth = 0;
|
||||
for (const char of content) {
|
||||
if (flexWidth > 500) {
|
||||
break;
|
||||
}
|
||||
if ((char >= '0' && char <= '9') || (char >= 'a' && char <= 'z')) {
|
||||
// 小写字母、数字字符
|
||||
flexWidth += 9.3;
|
||||
continue;
|
||||
}
|
||||
if (char >= 'A' && char <= 'Z') {
|
||||
flexWidth += 9;
|
||||
continue;
|
||||
}
|
||||
if (char >= '\u4e00' && char <= '\u9fa5') {
|
||||
// 如果是中文字符,为字符分配16个单位宽度
|
||||
flexWidth += 20;
|
||||
} else {
|
||||
// 其他种类字符
|
||||
flexWidth += 8;
|
||||
}
|
||||
}
|
||||
// if (flexWidth > 450) {
|
||||
// // 设置最大宽度
|
||||
// flexWidth = 450;
|
||||
// }
|
||||
return flexWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns uuid
|
||||
@@ -179,3 +142,38 @@ export async function copyToClipboard(txt: string, selector: string = '#copyValu
|
||||
clipboard.destroy();
|
||||
});
|
||||
}
|
||||
|
||||
export function fuzzyMatchField(keyword: string, fields: any[], ...valueExtractFuncs: Function[]) {
|
||||
keyword = keyword?.toLowerCase();
|
||||
return fields.filter((field) => {
|
||||
for (let valueExtractFunc of valueExtractFuncs) {
|
||||
const value = valueExtractFunc(field)?.toLowerCase();
|
||||
if (isPrefixSubsequence(keyword, value)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 匹配是否为前缀子序列 targetTemplate=username prefix=uname -> true,prefix=uname2 -> false
|
||||
* @param prefix 字符串前缀(不连续也可以,但不改变字符的相对顺序)
|
||||
* @param targetTemplate 目标模板
|
||||
* @returns 是否匹配
|
||||
*/
|
||||
export function isPrefixSubsequence(prefix: string, targetTemplate: string) {
|
||||
let i = 0; // 指向prefix的索引
|
||||
let j = 0; // 指向targetTemplate的索引
|
||||
|
||||
while (i < prefix.length && j < targetTemplate.length) {
|
||||
if (prefix[i] === targetTemplate[j]) {
|
||||
// 字符匹配,两个指针都向前移动
|
||||
i++;
|
||||
}
|
||||
j++; // 目标字符串指针始终向前移动
|
||||
}
|
||||
|
||||
// 如果prefix的所有字符都被找到,返回true
|
||||
return i === prefix.length;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ export interface ViteEnv {
|
||||
VITE_PORT: number;
|
||||
VITE_OPEN: boolean;
|
||||
VITE_PUBLIC_PATH: string;
|
||||
VITE_EDITOR: string;
|
||||
}
|
||||
|
||||
export function loadEnv(): ViteEnv {
|
||||
|
||||
@@ -83,7 +83,7 @@ const breakPoint = computed<BreakPoint>(() => gridRef.value?.breakPoint);
|
||||
// 判断是否显示 展开/合并 按钮
|
||||
const showCollapse = computed(() => {
|
||||
let show = false;
|
||||
props.items.reduce((prev, current) => {
|
||||
props.items.reduce((prev, current: any) => {
|
||||
prev += (current![breakPoint.value]?.span ?? current?.span ?? 1) + (current![breakPoint.value]?.offset ?? current?.offset ?? 0);
|
||||
if (typeof props.searchCol !== 'number') {
|
||||
if (prev >= props.searchCol[breakPoint.value]) show = true;
|
||||
|
||||
@@ -14,11 +14,11 @@ export function hasPerm(code: string) {
|
||||
|
||||
/**
|
||||
* 判断用户是否拥有权限对象里对应的code
|
||||
* @param perms { save: "xxx:save"}
|
||||
* @returns {"xxx:save": true} key->permission code
|
||||
* @param permCodes
|
||||
*/
|
||||
export function hasPerms(permCodes: any[]) {
|
||||
const res = {};
|
||||
const res = {} as { [key: string]: boolean };
|
||||
for (let permCode of permCodes) {
|
||||
if (hasPerm(permCode)) {
|
||||
res[permCode] = true;
|
||||
|
||||
@@ -35,38 +35,42 @@
|
||||
<p class="title">时间表达式</p>
|
||||
<table>
|
||||
<thead>
|
||||
<th v-for="item of tabTitles" width="40" :key="item">{{ item }}</th>
|
||||
<th>crontab完整表达式</th>
|
||||
<tr>
|
||||
<th v-for="item of tabTitles" width="40" :key="item">{{ item }}</th>
|
||||
<th>crontab完整表达式</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<td>
|
||||
<span>{{ crontabValueObj.second }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{ crontabValueObj.min }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{ crontabValueObj.hour }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{ crontabValueObj.day }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{ crontabValueObj.mouth }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{ crontabValueObj.week }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{ crontabValueObj.year }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{ contabValueString }}</span>
|
||||
</td>
|
||||
<tr>
|
||||
<td>
|
||||
<span>{{ crontabValueObj.second }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{ crontabValueObj.min }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{ crontabValueObj.hour }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{ crontabValueObj.day }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{ crontabValueObj.mouth }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{ crontabValueObj.week }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{ crontabValueObj.year }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{ crontabValueString }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<CrontabResult :ex="contabValueString"></CrontabResult>
|
||||
<CrontabResult :ex="crontabValueString"></CrontabResult>
|
||||
|
||||
<div class="pop_btn">
|
||||
<el-button size="small" @click="hidePopup">取消</el-button>
|
||||
@@ -202,7 +206,7 @@ function hidePopup() {
|
||||
|
||||
// 填充表达式
|
||||
const submitFill = () => {
|
||||
emit('fill', contabValueString.value);
|
||||
emit('fill', crontabValueString.value);
|
||||
hidePopup();
|
||||
};
|
||||
|
||||
@@ -220,7 +224,7 @@ const clearCron = () => {
|
||||
changeTab(state.activeName);
|
||||
};
|
||||
|
||||
const contabValueString = computed(() => {
|
||||
const crontabValueString = computed(() => {
|
||||
let obj = state.crontabValueObj;
|
||||
let str = obj.second + ' ' + obj.min + ' ' + obj.hour + ' ' + obj.day + ' ' + obj.mouth + ' ' + obj.week + (obj.year == '' ? '' : ' ' + obj.year);
|
||||
return str;
|
||||
|
||||
17
mayfly_go_web/src/components/enumselect/EnumSelect.vue
Normal file
17
mayfly_go_web/src/components/enumselect/EnumSelect.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<el-select v-bind="$attrs" v-model="modelValue">
|
||||
<el-option v-for="item in props.enums" :key="item.value" :label="item.label" :value="item.value"> </el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const props = defineProps({
|
||||
enums: {
|
||||
type: Object, // 需要为EnumValue类型
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const modelValue: any = defineModel('modelValue');
|
||||
</script>
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<el-tag v-bind="$attrs" :type="type" :color="color" effect="plain">{{ enumLabel }}</el-tag>
|
||||
<el-tag :disable-transitions="true" v-bind="$attrs" :type="type" :color="color" effect="plain">{{ enumLabel }}</el-tag>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
60
mayfly_go_web/src/components/file/FileInfo.vue
Normal file
60
mayfly_go_web/src/components/file/FileInfo.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<el-tooltip :content="formatByteSize(fileDetail?.size)" placement="left">
|
||||
<el-link v-if="props.canDownload" target="_blank" rel="noopener noreferrer" icon="Download" type="primary" :href="getFileUrl(props.fileKey)"></el-link>
|
||||
</el-tooltip>
|
||||
|
||||
{{ fileDetail?.filename }}
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import openApi from '@/common/openApi';
|
||||
import { getFileUrl } from '@/common/request';
|
||||
import { formatByteSize } from '@/common/utils/format';
|
||||
const props = defineProps({
|
||||
fileKey: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
files: {
|
||||
type: [Array],
|
||||
},
|
||||
canDownload: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
setFileInfo();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.fileKey,
|
||||
async (val) => {
|
||||
if (val) {
|
||||
setFileInfo();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const fileDetail: any = ref({});
|
||||
|
||||
const setFileInfo = async () => {
|
||||
if (!props.fileKey) {
|
||||
return;
|
||||
}
|
||||
if (props.files && props.files.length > 0) {
|
||||
const file: any = props.files.find((file: any) => {
|
||||
return file.fileKey === props.fileKey;
|
||||
});
|
||||
fileDetail.value = file;
|
||||
return;
|
||||
}
|
||||
|
||||
const files = await openApi.getFileDetail([props.fileKey]);
|
||||
fileDetail.value = files?.[0];
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
@@ -252,7 +252,9 @@ const changeLanguage = (value: any) => {
|
||||
};
|
||||
|
||||
const setEditorValue = (value: any) => {
|
||||
monacoEditorIns.getModel()?.setValue(value);
|
||||
if (value) {
|
||||
monacoEditorIns.getModel()?.setValue(value);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -156,8 +156,8 @@
|
||||
<el-row v-if="props.pageable" class="mt20" type="flex" justify="end">
|
||||
<el-pagination
|
||||
:small="props.size == 'small'"
|
||||
@current-change="handlePageNumChange"
|
||||
@size-change="handlePageSizeChange"
|
||||
@current-change="pageNumChange"
|
||||
@size-change="pageSizeChange"
|
||||
style="text-align: right"
|
||||
layout="prev, pager, next, total, sizes"
|
||||
:total="total"
|
||||
@@ -185,7 +185,7 @@ import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
import { usePageTable } from '@/hooks/usePageTable';
|
||||
import { ElTable } from 'element-plus';
|
||||
|
||||
const emit = defineEmits(['update:selectionData', 'pageChange']);
|
||||
const emit = defineEmits(['update:selectionData', 'pageSizeChange', 'pageNumChange']);
|
||||
|
||||
export interface PageTableProps {
|
||||
size?: string;
|
||||
@@ -257,6 +257,15 @@ const changeSimpleFormItem = (searchItem: SearchItem) => {
|
||||
nowSearchItem.value = searchItem;
|
||||
};
|
||||
|
||||
const pageSizeChange = (val: number) => {
|
||||
emit('pageSizeChange', val);
|
||||
handlePageSizeChange(val);
|
||||
};
|
||||
const pageNumChange = (val: number) => {
|
||||
emit('pageNumChange', val);
|
||||
handlePageNumChange(val);
|
||||
};
|
||||
|
||||
let { tableData, total, loading, search, reset, getTableData, handlePageNumChange, handlePageSizeChange } = usePageTable(
|
||||
props.pageable,
|
||||
props.pageApi,
|
||||
@@ -353,6 +362,7 @@ defineExpose({
|
||||
tableRef: tableRef,
|
||||
search: getTableData,
|
||||
getData,
|
||||
total,
|
||||
});
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -109,7 +109,7 @@ const state = reactive({
|
||||
mouse: null as any,
|
||||
touchpad: null as any,
|
||||
errorMessage: '',
|
||||
arguments: {},
|
||||
arguments: {} as any,
|
||||
status: TerminalStatus.NoConnected,
|
||||
size: {
|
||||
height: 710,
|
||||
|
||||
@@ -67,7 +67,7 @@ const state = reactive({
|
||||
search: null as any,
|
||||
weblinks: null as any,
|
||||
},
|
||||
status: TerminalStatus.NoConnected,
|
||||
status: -11,
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
@@ -96,6 +96,7 @@ onBeforeUnmount(() => {
|
||||
});
|
||||
|
||||
function init() {
|
||||
state.status = TerminalStatus.NoConnected;
|
||||
if (term) {
|
||||
console.log('重新连接...');
|
||||
close();
|
||||
@@ -105,7 +106,7 @@ function init() {
|
||||
});
|
||||
}
|
||||
|
||||
function initTerm() {
|
||||
async function initTerm() {
|
||||
term = new Terminal({
|
||||
fontSize: themeConfig.value.terminalFontSize || 15,
|
||||
fontWeight: themeConfig.value.terminalFontWeight || 'normal',
|
||||
@@ -155,6 +156,7 @@ function initSocket() {
|
||||
state.status = TerminalStatus.Connected;
|
||||
|
||||
focus();
|
||||
fitTerminal();
|
||||
|
||||
// 如果有初始要执行的命令,则发送执行命令
|
||||
if (props.cmd) {
|
||||
@@ -209,7 +211,6 @@ function loadAddon() {
|
||||
// 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) => {
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import EnumValue from '@/common/Enum';
|
||||
|
||||
export enum TerminalStatus {
|
||||
Error = -1,
|
||||
NoConnected = 0,
|
||||
Connected = 1,
|
||||
Disconnected = 2,
|
||||
}
|
||||
|
||||
export const TerminalStatusEnum = {
|
||||
Error: EnumValue.of(TerminalStatus.Error, '连接出错').setExtra({ iconColor: 'var(--el-color-error)' }),
|
||||
NoConnected: EnumValue.of(TerminalStatus.NoConnected, '未连接').setExtra({ iconColor: 'var(--el-color-primary)' }),
|
||||
Connected: EnumValue.of(TerminalStatus.Connected, '连接成功').setExtra({ iconColor: 'var(--el-color-success)' }),
|
||||
Disconnected: EnumValue.of(TerminalStatus.Disconnected, '连接失败').setExtra({ iconColor: 'var(--el-color-error)' }),
|
||||
};
|
||||
|
||||
@@ -42,7 +42,7 @@ const useCustomFetch = createFetch({
|
||||
|
||||
export function useApiFetch<T>(api: Api, params: any = null, reqOptions: RequestInit = {}) {
|
||||
const uaf = useCustomFetch<T>(api.url, {
|
||||
beforeFetch({ url, options }) {
|
||||
async beforeFetch({ url, options }) {
|
||||
options.method = api.method;
|
||||
if (!params) {
|
||||
return;
|
||||
@@ -57,7 +57,7 @@ export function useApiFetch<T>(api: Api, params: any = null, reqOptions: Request
|
||||
}
|
||||
|
||||
if (api.beforeHandler) {
|
||||
paramsValue = api.beforeHandler(paramsValue);
|
||||
paramsValue = await api.beforeHandler(paramsValue);
|
||||
}
|
||||
|
||||
if (paramsValue) {
|
||||
|
||||
@@ -1 +1 @@
|
||||
@import 'common/transition.scss';
|
||||
@use 'common/transition.scss';
|
||||
@@ -1,4 +1,4 @@
|
||||
@import 'mixins/index.scss';
|
||||
@use 'mixins/index' as mixins;
|
||||
|
||||
/* Button 按钮
|
||||
------------------------------- */
|
||||
@@ -97,7 +97,7 @@
|
||||
.el-sub-menu .iconfont,
|
||||
.el-menu-item .fa,
|
||||
.el-sub-menu .fa {
|
||||
@include generalIcon;
|
||||
@include mixins.generalIcon;
|
||||
}
|
||||
|
||||
// 水平菜单、横向菜单高亮 背景色,鼠标 hover 时,有子级菜单的背景色
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
@import './app.scss';
|
||||
@import './base.scss';
|
||||
@import './other.scss';
|
||||
@import './element.scss';
|
||||
@import './media/media.scss';
|
||||
@import './waves.scss';
|
||||
@import './dark.scss';
|
||||
@import './iconSelector.scss';
|
||||
@use './app.scss';
|
||||
@use './base.scss';
|
||||
@use './other.scss';
|
||||
@use './element.scss';
|
||||
@use './media/media.scss';
|
||||
@use './waves.scss';
|
||||
@use './dark.scss';
|
||||
@use './iconSelector.scss';
|
||||
@@ -1,94 +1,109 @@
|
||||
@import './index.scss';
|
||||
@use './index.scss' as index;
|
||||
|
||||
/* 页面宽度小于768px
|
||||
------------------------------- */
|
||||
@media screen and (max-width: $sm) {
|
||||
.big-data-down-left {
|
||||
width: 100% !important;
|
||||
flex-direction: unset !important;
|
||||
flex-wrap: wrap;
|
||||
.flex-warp-item {
|
||||
min-height: 196.24px;
|
||||
padding: 0 7.5px 15px 15px !important;
|
||||
.flex-warp-item-box {
|
||||
border: none !important;
|
||||
border-bottom: 1px solid #ebeef5 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.big-data-down-center {
|
||||
width: 100% !important;
|
||||
.big-data-down-center-one,
|
||||
.big-data-down-center-two {
|
||||
min-height: 196.24px;
|
||||
padding-left: 15px !important;
|
||||
.big-data-down-center-one-content {
|
||||
border: none !important;
|
||||
border-bottom: 1px solid #ebeef5 !important;
|
||||
}
|
||||
.flex-warp-item-box {
|
||||
@extend .big-data-down-center-one-content;
|
||||
}
|
||||
}
|
||||
}
|
||||
.big-data-down-right {
|
||||
.flex-warp-item {
|
||||
.flex-warp-item-box {
|
||||
border: none !important;
|
||||
border-bottom: 1px solid #ebeef5 !important;
|
||||
}
|
||||
&:nth-of-type(2) {
|
||||
padding-left: 15px !important;
|
||||
}
|
||||
&:last-of-type {
|
||||
.flex-warp-item-box {
|
||||
border: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: index.$sm) {
|
||||
.big-data-down-left {
|
||||
width: 100% !important;
|
||||
flex-direction: unset !important;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.flex-warp-item {
|
||||
min-height: 196.24px;
|
||||
padding: 0 7.5px 15px 15px !important;
|
||||
|
||||
.flex-warp-item-box {
|
||||
border: none !important;
|
||||
border-bottom: 1px solid #ebeef5 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.big-data-down-center {
|
||||
width: 100% !important;
|
||||
|
||||
.big-data-down-center-one,
|
||||
.big-data-down-center-two {
|
||||
min-height: 196.24px;
|
||||
padding-left: 15px !important;
|
||||
|
||||
.big-data-down-center-one-content {
|
||||
border: none !important;
|
||||
border-bottom: 1px solid #ebeef5 !important;
|
||||
}
|
||||
|
||||
.flex-warp-item-box {
|
||||
@extend .big-data-down-center-one-content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.big-data-down-right {
|
||||
.flex-warp-item {
|
||||
.flex-warp-item-box {
|
||||
border: none !important;
|
||||
border-bottom: 1px solid #ebeef5 !important;
|
||||
}
|
||||
|
||||
&:nth-of-type(2) {
|
||||
padding-left: 15px !important;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
.flex-warp-item-box {
|
||||
border: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 页面宽度大于768px小于1200px
|
||||
------------------------------- */
|
||||
@media screen and (min-width: $sm) and (max-width: $lg) {
|
||||
.chart-warp-bottom {
|
||||
.big-data-down-left {
|
||||
width: 50% !important;
|
||||
}
|
||||
.big-data-down-center {
|
||||
width: 50% !important;
|
||||
}
|
||||
.big-data-down-right {
|
||||
.flex-warp-item {
|
||||
width: 50% !important;
|
||||
&:nth-of-type(2) {
|
||||
padding-left: 7.5px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: index.$sm) and (max-width: index.$lg) {
|
||||
.chart-warp-bottom {
|
||||
.big-data-down-left {
|
||||
width: 50% !important;
|
||||
}
|
||||
|
||||
.big-data-down-center {
|
||||
width: 50% !important;
|
||||
}
|
||||
|
||||
.big-data-down-right {
|
||||
.flex-warp-item {
|
||||
width: 50% !important;
|
||||
|
||||
&:nth-of-type(2) {
|
||||
padding-left: 7.5px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 页面宽度小于1200px
|
||||
------------------------------- */
|
||||
@media screen and (max-width: $lg) {
|
||||
.chart-warp-top {
|
||||
.up-left {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.chart-warp-bottom {
|
||||
overflow-y: auto !important;
|
||||
flex-wrap: wrap;
|
||||
.big-data-down-right {
|
||||
width: 100% !important;
|
||||
flex-direction: unset !important;
|
||||
flex-wrap: wrap;
|
||||
.flex-warp-item {
|
||||
min-height: 196.24px;
|
||||
padding: 0 7.5px 15px 15px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: index.$lg) {
|
||||
.chart-warp-top {
|
||||
.up-left {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-warp-bottom {
|
||||
overflow-y: auto !important;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.big-data-down-right {
|
||||
width: 100% !important;
|
||||
flex-direction: unset !important;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.flex-warp-item {
|
||||
min-height: 196.24px;
|
||||
padding: 0 7.5px 15px 15px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
@import './index.scss';
|
||||
@use './index.scss' as index;
|
||||
|
||||
/* 页面宽度小于576px
|
||||
------------------------------- */
|
||||
@media screen and (max-width: $xs) {
|
||||
.el-cascader__dropdown.el-popper {
|
||||
overflow: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: index.$xs) {
|
||||
.el-cascader__dropdown.el-popper {
|
||||
overflow: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
@import './index.scss';
|
||||
@use './index.scss';
|
||||
|
||||
/* 页面宽度小于800px
|
||||
------------------------------- */
|
||||
@media screen and (max-width: 800px) {
|
||||
.el-dialog {
|
||||
width: 90% !important;
|
||||
}
|
||||
.el-dialog.is-fullscreen {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
.el-dialog {
|
||||
width: 90% !important;
|
||||
}
|
||||
|
||||
.el-dialog.is-fullscreen {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,38 @@
|
||||
@import './index.scss';
|
||||
@use './index.scss' as index;
|
||||
|
||||
/* 页面宽度小于768px
|
||||
------------------------------- */
|
||||
@media screen and (max-width: $sm) {
|
||||
.error {
|
||||
.error-flex {
|
||||
flex-direction: column-reverse !important;
|
||||
height: auto !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
.right,
|
||||
.left {
|
||||
flex: unset !important;
|
||||
display: flex !important;
|
||||
}
|
||||
.left-item {
|
||||
margin: auto !important;
|
||||
}
|
||||
.right img {
|
||||
max-width: 450px !important;
|
||||
@extend .left-item;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: index.$sm) {
|
||||
.error {
|
||||
.error-flex {
|
||||
flex-direction: column-reverse !important;
|
||||
height: auto !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.right,
|
||||
.left {
|
||||
flex: unset !important;
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.left-item {
|
||||
margin: auto !important;
|
||||
}
|
||||
|
||||
.right img {
|
||||
max-width: 450px !important;
|
||||
@extend .left-item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 页面宽度大于768px小于992px
|
||||
------------------------------- */
|
||||
@media screen and (min-width: $sm) and (max-width: $md) {
|
||||
.error {
|
||||
.error-flex {
|
||||
padding-left: 30px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: index.$sm) and (max-width: index.$md) {
|
||||
.error {
|
||||
.error-flex {
|
||||
padding-left: 30px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
@import './index.scss';
|
||||
@use './index.scss' as index;
|
||||
|
||||
/* 页面宽度小于576px
|
||||
------------------------------- */
|
||||
@media screen and (max-width: $xs) {
|
||||
.el-form-item__label {
|
||||
width: 100% !important;
|
||||
text-align: left !important;
|
||||
}
|
||||
.el-form-item__content {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: index.$xs) {
|
||||
.el-form-item__label {
|
||||
width: 100% !important;
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
.el-form-item__content {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
@import './index.scss';
|
||||
@use './index.scss' as index;
|
||||
|
||||
/* 页面宽度小于768px
|
||||
------------------------------- */
|
||||
@media screen and (max-width: $sm) {
|
||||
.home-warning-media,
|
||||
.home-dynamic-media {
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: index.$sm) {
|
||||
|
||||
.home-warning-media,
|
||||
.home-dynamic-media {
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
@@ -1,55 +1,61 @@
|
||||
@import './index.scss';
|
||||
@use './index.scss' as index;
|
||||
|
||||
/* 页面宽度小于576px
|
||||
------------------------------- */
|
||||
@media screen and (max-width: $xs) {
|
||||
// MessageBox 弹框
|
||||
.el-message-box {
|
||||
width: 80% !important;
|
||||
}
|
||||
@media screen and (max-width: index.$xs) {
|
||||
|
||||
// MessageBox 弹框
|
||||
.el-message-box {
|
||||
width: 80% !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 页面宽度小于768px
|
||||
------------------------------- */
|
||||
@media screen and (max-width: $sm) {
|
||||
// Breadcrumb 面包屑
|
||||
.layout-navbars-breadcrumb-hide {
|
||||
display: none;
|
||||
}
|
||||
// 外链视图
|
||||
.layout-view-link {
|
||||
a {
|
||||
max-width: 80%;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
// 菜单搜索
|
||||
.layout-search-dialog {
|
||||
.el-autocomplete {
|
||||
width: 80% !important;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: index.$sm) {
|
||||
|
||||
// Breadcrumb 面包屑
|
||||
.layout-navbars-breadcrumb-hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// 外链视图
|
||||
.layout-view-link {
|
||||
a {
|
||||
max-width: 80%;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
// 菜单搜索
|
||||
.layout-search-dialog {
|
||||
.el-autocomplete {
|
||||
width: 80% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 页面宽度小于1000px
|
||||
------------------------------- */
|
||||
@media screen and (max-width: 1000px) {
|
||||
// 布局配置
|
||||
.layout-drawer-content-flex {
|
||||
position: relative;
|
||||
&::after {
|
||||
content: '手机版不支持切换布局';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
text-align: center;
|
||||
height: 140px;
|
||||
line-height: 140px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 布局配置
|
||||
.layout-drawer-content-flex {
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: '手机版不支持切换布局';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
text-align: center;
|
||||
height: 140px;
|
||||
line-height: 140px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,23 @@
|
||||
@import './index.scss';
|
||||
@use './index.scss' as index;
|
||||
|
||||
/* 页面宽度小于576px
|
||||
------------------------------- */
|
||||
@media screen and (max-width: $xs) {
|
||||
.login-container {
|
||||
.login-content {
|
||||
width: 90% !important;
|
||||
padding: 20px 0 !important;
|
||||
}
|
||||
.login-content-form-btn {
|
||||
width: 100% !important;
|
||||
padding: 12px 0 !important;
|
||||
}
|
||||
.login-copyright {
|
||||
.login-copyright-msg {
|
||||
white-space: unset !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: index.$xs) {
|
||||
.login-container {
|
||||
.login-content {
|
||||
width: 90% !important;
|
||||
padding: 20px 0 !important;
|
||||
}
|
||||
|
||||
.login-content-form-btn {
|
||||
width: 100% !important;
|
||||
padding: 12px 0 !important;
|
||||
}
|
||||
|
||||
.login-copyright {
|
||||
.login-copyright-msg {
|
||||
white-space: unset !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
@import './login.scss';
|
||||
@import './error.scss';
|
||||
@import './layout.scss';
|
||||
@import './personal.scss';
|
||||
@import './tagsView.scss';
|
||||
@import './home.scss';
|
||||
@import './chart.scss';
|
||||
@import './form.scss';
|
||||
@import './scrollbar.scss';
|
||||
@import './pagination.scss';
|
||||
@import './dialog.scss';
|
||||
@import './cityLinkage.scss';
|
||||
@use './login.scss';
|
||||
@use './error.scss';
|
||||
@use './layout.scss';
|
||||
@use './personal.scss';
|
||||
@use './tagsView.scss';
|
||||
@use './home.scss';
|
||||
@use './chart.scss';
|
||||
@use './form.scss';
|
||||
@use './scrollbar.scss';
|
||||
@use './pagination.scss';
|
||||
@use './dialog.scss';
|
||||
@use './cityLinkage.scss';
|
||||
@@ -1,15 +1,16 @@
|
||||
@import './index.scss';
|
||||
@use './index.scss' as index;
|
||||
|
||||
/* 页面宽度小于576px
|
||||
------------------------------- */
|
||||
@media screen and (max-width: $xs) {
|
||||
.el-pager,
|
||||
.el-pagination__jump {
|
||||
display: none !important;
|
||||
}
|
||||
@media screen and (max-width: index.$xs) {
|
||||
|
||||
.el-pager,
|
||||
.el-pagination__jump {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 默认居中对齐
|
||||
.el-pagination {
|
||||
text-align: center !important;
|
||||
}
|
||||
text-align: center !important;
|
||||
}
|
||||
@@ -1,16 +1,18 @@
|
||||
@import './index.scss';
|
||||
@use './index.scss' as index;
|
||||
|
||||
/* 页面宽度小于768px
|
||||
------------------------------- */
|
||||
@media screen and (max-width: $sm) {
|
||||
.personal-info {
|
||||
padding-left: 0 !important;
|
||||
margin-top: 15px;
|
||||
}
|
||||
.personal-recommend-col {
|
||||
margin-bottom: 15px;
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: index.$sm) {
|
||||
.personal-info {
|
||||
padding-left: 0 !important;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.personal-recommend-col {
|
||||
margin-bottom: 15px;
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,56 +1,66 @@
|
||||
@import './index.scss';
|
||||
@use './index.scss' as index;
|
||||
|
||||
/* 页面宽度小于768px
|
||||
------------------------------- */
|
||||
@media screen and (max-width: $sm) {
|
||||
// 滚动条的宽度
|
||||
::-webkit-scrollbar {
|
||||
width: 3px !important;
|
||||
height: 3px !important;
|
||||
}
|
||||
::-webkit-scrollbar-track-piece {
|
||||
background-color: var(--bg-main-color);
|
||||
}
|
||||
// 滚动条的设置
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(144, 147, 153, 0.3);
|
||||
background-clip: padding-box;
|
||||
min-height: 28px;
|
||||
border-radius: 5px;
|
||||
transition: 0.3s background-color;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(144, 147, 153, 0.5);
|
||||
}
|
||||
// element plus scrollbar
|
||||
.el-scrollbar__bar.is-vertical {
|
||||
width: 2px !important;
|
||||
}
|
||||
.el-scrollbar__bar.is-horizontal {
|
||||
height: 2px !important;
|
||||
}
|
||||
@media screen and (max-width: index.$sm) {
|
||||
|
||||
// 滚动条的宽度
|
||||
::-webkit-scrollbar {
|
||||
width: 3px !important;
|
||||
height: 3px !important;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track-piece {
|
||||
background-color: var(--bg-main-color);
|
||||
}
|
||||
|
||||
// 滚动条的设置
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(144, 147, 153, 0.3);
|
||||
background-clip: padding-box;
|
||||
min-height: 28px;
|
||||
border-radius: 5px;
|
||||
transition: 0.3s background-color;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(144, 147, 153, 0.5);
|
||||
}
|
||||
|
||||
// element plus scrollbar
|
||||
.el-scrollbar__bar.is-vertical {
|
||||
width: 2px !important;
|
||||
}
|
||||
|
||||
.el-scrollbar__bar.is-horizontal {
|
||||
height: 2px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 页面宽度大于768px
|
||||
------------------------------- */
|
||||
@media screen and (min-width: 769px) {
|
||||
// 滚动条的宽度
|
||||
::-webkit-scrollbar {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
}
|
||||
::-webkit-scrollbar-track-piece {
|
||||
background-color: var(--bg-main-color);
|
||||
}
|
||||
// 滚动条的设置
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(144, 147, 153, 0.3);
|
||||
background-clip: padding-box;
|
||||
min-height: 28px;
|
||||
border-radius: 5px;
|
||||
transition: 0.3s background-color;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(144, 147, 153, 0.5);
|
||||
}
|
||||
|
||||
// 滚动条的宽度
|
||||
::-webkit-scrollbar {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track-piece {
|
||||
background-color: var(--bg-main-color);
|
||||
}
|
||||
|
||||
// 滚动条的设置
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(144, 147, 153, 0.3);
|
||||
background-clip: padding-box;
|
||||
min-height: 28px;
|
||||
border-radius: 5px;
|
||||
transition: 0.3s background-color;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(144, 147, 153, 0.5);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
@import './index.scss';
|
||||
@use './index.scss' as index;
|
||||
|
||||
/* 页面宽度小于768px
|
||||
------------------------------- */
|
||||
@media screen and (max-width: $sm) {
|
||||
.tags-view-form {
|
||||
.tags-view-form-col {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: index.$sm) {
|
||||
.tags-view-form {
|
||||
.tags-view-form-col {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,32 @@
|
||||
/* 第三方图标字体间距/大小设置
|
||||
------------------------------- */
|
||||
@mixin generalIcon {
|
||||
font-size: 14px !important;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: 5px;
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
font-size: 14px !important;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: 5px;
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 文本不换行
|
||||
------------------------------- */
|
||||
@mixin text-no-wrap() {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 多行文本溢出
|
||||
------------------------------- */
|
||||
@mixin text-ellipsis($line: 2) {
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: $line;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: $line;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
/* 滚动条(页面未使用) div 中使用:
|
||||
@@ -35,22 +35,26 @@
|
||||
// @include scrollBar;
|
||||
// }
|
||||
@mixin scrollBar {
|
||||
// 滚动条凹槽的颜色,还可以设置边框属性
|
||||
&::-webkit-scrollbar-track-piece {
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
// 滚动条的宽度
|
||||
&::-webkit-scrollbar {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
}
|
||||
// 滚动条的设置
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #dddddd;
|
||||
background-clip: padding-box;
|
||||
min-height: 28px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #bbb;
|
||||
}
|
||||
|
||||
// 滚动条凹槽的颜色,还可以设置边框属性
|
||||
&::-webkit-scrollbar-track-piece {
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
// 滚动条的宽度
|
||||
&::-webkit-scrollbar {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
}
|
||||
|
||||
// 滚动条的设置
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #dddddd;
|
||||
background-clip: padding-box;
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #bbb;
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,31 @@
|
||||
/* wangeditor富文本编辑器
|
||||
------------------------------- */
|
||||
.w-e-toolbar {
|
||||
border: 1px solid #ebeef5 !important;
|
||||
border-bottom: 1px solid #ebeef5 !important;
|
||||
border-top-left-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
z-index: 2 !important;
|
||||
border: 1px solid #ebeef5 !important;
|
||||
border-bottom: 1px solid #ebeef5 !important;
|
||||
border-top-left-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
z-index: 2 !important;
|
||||
}
|
||||
|
||||
.w-e-text-container {
|
||||
border: 1px solid #ebeef5 !important;
|
||||
border-top: none !important;
|
||||
border-bottom-left-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
z-index: 1 !important;
|
||||
border: 1px solid #ebeef5 !important;
|
||||
border-top: none !important;
|
||||
border-bottom-left-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
z-index: 1 !important;
|
||||
}
|
||||
|
||||
/* web端自定义截屏
|
||||
------------------------------- */
|
||||
#screenShotContainer {
|
||||
z-index: 9998 !important;
|
||||
z-index: 9998 !important;
|
||||
}
|
||||
|
||||
#toolPanel {
|
||||
height: 42px !important;
|
||||
height: 42px !important;
|
||||
}
|
||||
|
||||
#optionPanel {
|
||||
height: 37px !important;
|
||||
}
|
||||
height: 37px !important;
|
||||
}
|
||||
149
mayfly_go_web/src/views/flow/ProcInstEdit.vue
Executable file
149
mayfly_go_web/src/views/flow/ProcInstEdit.vue
Executable file
@@ -0,0 +1,149 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-drawer :title="props.title" v-model="visible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="50%">
|
||||
<template #header>
|
||||
<DrawerHeader :header="title" :back="cancel" />
|
||||
</template>
|
||||
|
||||
<el-form :model="form" ref="formRef" :rules="rules" label-width="auto">
|
||||
<el-form-item prop="bizType" label="业务类型">
|
||||
<EnumSelect v-model="form.bizType" :enums="FlowBizType" placeholder="请选择业务类型" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="remark" label="备注">
|
||||
<el-input v-model.trim="form.remark" type="textarea" placeholder="备注" auto-complete="off" clearable></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">业务信息</el-divider>
|
||||
<component
|
||||
ref="bizFormRef"
|
||||
v-if="form.bizType"
|
||||
:is="bizComponents[form.bizType]"
|
||||
v-model:bizForm="form.bizForm"
|
||||
@changeResourceCode="changeResourceCode"
|
||||
>
|
||||
</component>
|
||||
</el-form>
|
||||
|
||||
<span v-if="flowProcdef || !state.form.procdefId">
|
||||
<el-divider content-position="left">审批节点</el-divider>
|
||||
|
||||
<ProcdefTasks v-if="flowProcdef" :procdef="flowProcdef" />
|
||||
|
||||
<el-result v-if="!state.form.procdefId" icon="error" title="不存在审批节点" sub-title="该资源无需审批操作"> </el-result>
|
||||
</span>
|
||||
|
||||
<template #footer>
|
||||
<div>
|
||||
<el-button @click="cancel()">取 消</el-button>
|
||||
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk" :disabled="!state.form.procdefId">确 定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, reactive, defineAsyncComponent, shallowReactive, useTemplateRef } from 'vue';
|
||||
import { procdefApi, procinstApi } from './api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||
import { FlowBizType } from './enums';
|
||||
import EnumSelect from '@/components/enumselect/EnumSelect.vue';
|
||||
import ProcdefTasks from './components/ProcdefTasks.vue';
|
||||
import RedisRunCmdFlowBizForm from './flowbiz/redis/RedisRunCmdFlowBizForm.vue';
|
||||
|
||||
const DbSqlExecFlowBizForm = defineAsyncComponent(() => import('./flowbiz/dbms/DbSqlExecFlowBizForm.vue'));
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
|
||||
const visible = defineModel<boolean>('visible', { default: false });
|
||||
|
||||
//定义事件
|
||||
const emit = defineEmits(['cancel', 'val-change']);
|
||||
|
||||
const formRef: any = useTemplateRef('formRef');
|
||||
const bizFormRef: any = useTemplateRef('bizFormRef');
|
||||
|
||||
// 业务组件
|
||||
const bizComponents: any = shallowReactive({
|
||||
db_sql_exec_flow: DbSqlExecFlowBizForm,
|
||||
redis_run_cmd_flow: RedisRunCmdFlowBizForm,
|
||||
});
|
||||
|
||||
const rules = {
|
||||
bizType: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择流程业务类型',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
remark: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入申请备注',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const defaultForm = {
|
||||
bizType: FlowBizType.DbSqlExec.value,
|
||||
procdefId: -1,
|
||||
status: null,
|
||||
remark: '',
|
||||
bizForm: {},
|
||||
};
|
||||
|
||||
const state = reactive({
|
||||
tasks: [] as any,
|
||||
form: { ...defaultForm },
|
||||
flowProcdef: null as any,
|
||||
sortable: '' as any,
|
||||
});
|
||||
|
||||
const { form, flowProcdef } = toRefs(state);
|
||||
|
||||
const { isFetching: saveBtnLoading, execute: procinstStart } = procinstApi.start.useApi(form);
|
||||
|
||||
const changeResourceCode = async (resourceType: any, code: string) => {
|
||||
state.flowProcdef = await procdefApi.getByResource.request({ resourceType, resourceCode: code });
|
||||
if (!state.flowProcdef) {
|
||||
state.form.procdefId = 0;
|
||||
} else {
|
||||
state.form.procdefId = state.flowProcdef.id;
|
||||
}
|
||||
};
|
||||
|
||||
const btnOk = async () => {
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
await bizFormRef.value.validateBizForm();
|
||||
} catch (e: any) {
|
||||
ElMessage.error('请正确填写信息');
|
||||
return false;
|
||||
}
|
||||
|
||||
await procinstStart();
|
||||
ElMessage.success('流程发起成功');
|
||||
emit('val-change', state.form);
|
||||
//重置表单域
|
||||
cancel();
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
visible.value = false;
|
||||
emit('cancel');
|
||||
state.flowProcdef = null;
|
||||
formRef.value.resetFields();
|
||||
bizFormRef.value.resetBizForm();
|
||||
|
||||
state.form = { ...defaultForm };
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
@@ -17,6 +17,25 @@
|
||||
<el-option v-for="item in ProcdefStatus" :key="item.value" :label="item.label" :value="item.value"> </el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item prop="condition" label="触发条件">
|
||||
<template #label>
|
||||
触发条件
|
||||
<el-tooltip content="go template语法。若输出结果为1,则表示触发该审批流程" placement="top">
|
||||
<el-icon>
|
||||
<question-filled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
|
||||
<el-input
|
||||
v-model="form.condition"
|
||||
:rows="10"
|
||||
type="textarea"
|
||||
placeholder="触发条件, 返回值=1, 则表示触发该审批流程"
|
||||
auto-complete="off"
|
||||
clearable
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="remark" label="备注">
|
||||
<el-input v-model.trim="form.remark" placeholder="备注" auto-complete="off" clearable></el-input>
|
||||
</el-form-item>
|
||||
@@ -118,6 +137,7 @@ const state = reactive({
|
||||
name: null,
|
||||
defKey: null,
|
||||
status: null,
|
||||
condition: '',
|
||||
remark: null,
|
||||
// 流程的审批节点任务
|
||||
tasks: '',
|
||||
@@ -141,6 +161,23 @@ watch(props, (newValue: any) => {
|
||||
state.tasks = tasks;
|
||||
} else {
|
||||
state.form = { status: ProcdefStatus.Enable.value } as any;
|
||||
state.form.condition = `{{/* DBMS-执行sql规则; param参数描述如下 */}}
|
||||
{{/* stmtType: select / read / insert / update / delete / ddl ; */}}
|
||||
{{ if eq .bizType "db_sql_exec_flow"}}
|
||||
{{/* 不是select和read语句时,开启流程审批 */}}
|
||||
{{ if and (ne .param.stmtType "select") (ne .param.stmtType "read") }}
|
||||
1
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{/* Redis-执行命令规则; param参数描述如下 */}}
|
||||
{{/* cmdType: read(读命令) / write(写命令); */}}
|
||||
{{/* cmd: get/set/hset...等 */}}
|
||||
{{ if eq .bizType "redis_run_cmd_flow"}}
|
||||
{{ if eq .param.cmdType "write" }}
|
||||
1
|
||||
{{ end }}
|
||||
{{ end }}`;
|
||||
state.tasks = [];
|
||||
}
|
||||
});
|
||||
|
||||
@@ -66,7 +66,7 @@ const columns = [
|
||||
];
|
||||
|
||||
// 该用户拥有的的操作列按钮权限
|
||||
const actionBtns = hasPerms([perms.save, perms.del]);
|
||||
const actionBtns: any = hasPerms([perms.save, perms.del]);
|
||||
const actionColumn = TableColumn.new('action', '操作').isSlot().fixedRight().setMinWidth(160).noShowOverflowTooltip().alignCenter();
|
||||
|
||||
const pageTableRef: Ref<any> = ref(null);
|
||||
|
||||
@@ -1,28 +1,20 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-drawer :title="props.title" v-model="visible" :before-close="cancel" size="40%" :close-on-click-modal="!props.instTaskId">
|
||||
<el-drawer :title="props.title" v-model="visible" :before-close="cancel" size="50%" :close-on-click-modal="!props.instTaskId">
|
||||
<template #header>
|
||||
<DrawerHeader :header="title" :back="cancel" />
|
||||
</template>
|
||||
|
||||
<div>
|
||||
<el-divider content-position="left">流程信息</el-divider>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions :column="3" border>
|
||||
<el-descriptions-item label="流程名">{{ procinst.procdefName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="业务">
|
||||
<enum-tag :enums="FlowBizType" :value="procinst.bizType"></enum-tag>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="发起人">
|
||||
<AccountInfo :account-id="procinst.creatorId" :username="procinst.creator" />
|
||||
<!-- {{ procinst.creator }} -->
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="发起时间">{{ formatDate(procinst.createTime) }}</el-descriptions-item>
|
||||
|
||||
<div v-if="procinst.duration">
|
||||
<el-descriptions-item label="持续时间">{{ formatTime(procinst.duration) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="结束时间">{{ formatDate(procinst.endTime) }}</el-descriptions-item>
|
||||
</div>
|
||||
|
||||
<el-descriptions-item label="流程状态">
|
||||
<enum-tag :enums="ProcinstStatus" :value="procinst.status"></enum-tag>
|
||||
@@ -31,6 +23,13 @@
|
||||
<enum-tag :enums="ProcinstBizStatus" :value="procinst.bizStatus"></enum-tag>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="发起时间">{{ formatDate(procinst.createTime) }}</el-descriptions-item>
|
||||
|
||||
<div v-if="procinst.duration">
|
||||
<el-descriptions-item label="结束时间">{{ formatDate(procinst.endTime) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="持续时间">{{ formatTime(procinst.duration) }}</el-descriptions-item>
|
||||
</div>
|
||||
|
||||
<el-descriptions-item label="备注">
|
||||
{{ procinst.remark }}
|
||||
</el-descriptions-item>
|
||||
@@ -44,14 +43,7 @@
|
||||
|
||||
<div>
|
||||
<el-divider content-position="left">业务信息</el-divider>
|
||||
<component
|
||||
v-if="procinst.bizType"
|
||||
ref="keyValueRef"
|
||||
:is="bizComponents[procinst.bizType]"
|
||||
:biz-key="procinst.bizKey"
|
||||
:biz-form="procinst.bizForm"
|
||||
>
|
||||
</component>
|
||||
<component v-if="procinst.bizType" ref="keyValueRef" :is="bizComponents[procinst.bizType]" :procinst="procinst"> </component>
|
||||
</div>
|
||||
|
||||
<div v-if="props.instTaskId">
|
||||
@@ -92,8 +84,8 @@ import EnumTag from '@/components/enumtag/EnumTag.vue';
|
||||
import AccountInfo from '@/views/system/account/components/AccountInfo.vue';
|
||||
import { formatDate } from '@/common/utils/format';
|
||||
|
||||
const DbSqlExecBiz = defineAsyncComponent(() => import('./flowbiz/DbSqlExecBiz.vue'));
|
||||
const RedisRunWriteCmdBiz = defineAsyncComponent(() => import('./flowbiz/RedisRunWriteCmdBiz.vue'));
|
||||
const DbSqlExecBiz = defineAsyncComponent(() => import('./flowbiz/dbms/DbSqlExecBiz.vue'));
|
||||
const RedisRunCmdBiz = defineAsyncComponent(() => import('./flowbiz/redis/RedisRunCmdBiz.vue'));
|
||||
|
||||
const props = defineProps({
|
||||
procinstId: {
|
||||
@@ -114,9 +106,9 @@ const visible = defineModel<boolean>('visible', { default: false });
|
||||
const emit = defineEmits(['cancel', 'val-change']);
|
||||
|
||||
// 业务组件
|
||||
const bizComponents = shallowReactive({
|
||||
const bizComponents: any = shallowReactive({
|
||||
db_sql_exec_flow: DbSqlExecBiz,
|
||||
redis_run_write_cmd_flow: RedisRunWriteCmdBiz,
|
||||
redis_run_cmd_flow: RedisRunCmdBiz,
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
:columns="columns"
|
||||
>
|
||||
<template #tableHeader>
|
||||
<!-- <el-button v-auth="perms.addAccount" type="primary" icon="plus" @click="editFlowDef(false)">添加</el-button> -->
|
||||
<el-button type="primary" icon="plus" @click="startProcInst()">发起流程</el-button>
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
@@ -36,6 +36,8 @@
|
||||
@val-change="valChange()"
|
||||
@cancel="procinstDetail.procinstId = 0"
|
||||
/>
|
||||
|
||||
<ProcInstEdit v-model:visible="procinstEdit.visible" :title="procinstEdit.title" @val-change="search" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -49,6 +51,7 @@ import ProcinstDetail from './ProcinstDetail.vue';
|
||||
import { FlowBizType, ProcinstBizStatus, ProcinstStatus } from './enums';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { formatTime } from '@/common/utils/format';
|
||||
import ProcInstEdit from './ProcInstEdit.vue';
|
||||
|
||||
const searchItems = [
|
||||
SearchItem.select('status', '流程状态').withEnum(ProcinstStatus),
|
||||
@@ -73,7 +76,7 @@ const columns = [
|
||||
}
|
||||
return formatTime(duration);
|
||||
}),
|
||||
TableColumn.new('bizHandleRes', '业务处理结果'),
|
||||
// TableColumn.new('bizHandleRes', '业务处理结果'),
|
||||
TableColumn.new('action', '操作').isSlot().fixedRight().setMinWidth(160).noShowOverflowTooltip().alignCenter(),
|
||||
];
|
||||
|
||||
@@ -98,9 +101,13 @@ const state = reactive({
|
||||
procinstId: 0,
|
||||
instTaskId: 0,
|
||||
},
|
||||
procinstEdit: {
|
||||
title: '发起流程',
|
||||
visible: false,
|
||||
},
|
||||
});
|
||||
|
||||
const { selectionData, query, procinstDetail } = toRefs(state);
|
||||
const { selectionData, query, procinstDetail, procinstEdit } = toRefs(state);
|
||||
|
||||
const search = async () => {
|
||||
pageTableRef.value.search();
|
||||
@@ -118,6 +125,10 @@ const showProcinst = (data: any) => {
|
||||
state.procinstDetail.visible = true;
|
||||
};
|
||||
|
||||
const startProcInst = () => {
|
||||
state.procinstEdit.visible = true;
|
||||
};
|
||||
|
||||
const valChange = () => {
|
||||
state.procinstDetail.visible = false;
|
||||
search();
|
||||
|
||||
@@ -9,6 +9,7 @@ export const procdefApi = {
|
||||
|
||||
export const procinstApi = {
|
||||
list: Api.newGet('/flow/procinsts'),
|
||||
start: Api.newPost('/flow/procinsts/start'),
|
||||
detail: Api.newGet('/flow/procinsts/{id}'),
|
||||
cancel: Api.newPost('/flow/procinsts/{id}/cancel'),
|
||||
tasks: Api.newGet('/flow/procinsts/tasks'),
|
||||
|
||||
@@ -29,6 +29,6 @@ export const ProcinstTaskStatus = {
|
||||
};
|
||||
|
||||
export const FlowBizType = {
|
||||
DbSqlExec: EnumValue.of('db_sql_exec_flow', 'DBMS-执行SQL'),
|
||||
RedisRunWriteCmd: EnumValue.of('redis_run_write_cmd_flow', 'Redis-执行write命令'),
|
||||
DbSqlExec: EnumValue.of('db_sql_exec_flow', 'DBMS-执行SQL').setTagType('warning'),
|
||||
RedisRunWriteCmd: EnumValue.of('redis_run_cmd_flow', 'Redis-执行命令').setTagType('danger'),
|
||||
};
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-descriptions :column="3" border>
|
||||
<el-descriptions-item :span="2" label="名称">{{ db?.name }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="id">{{ db?.id }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="3" label="关联标签"><ResourceTags :tags="db.tags" /></el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="1" label="主机">{{ `${db?.host}:${db?.port}` }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="类型">
|
||||
<SvgIcon :name="getDbDialect(db?.type).getInfo().icon" :size="20" />{{ db?.type }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="用户名">{{ db?.username }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="数据库">{{ sqlExec.db }}</el-descriptions-item>
|
||||
<el-descriptions-item label="表">
|
||||
{{ sqlExec.table }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="类型">
|
||||
<el-tag size="small">{{ EnumValue.getLabelByValue(DbSqlExecTypeEnum, sqlExec.type) }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="执行SQL">
|
||||
<monaco-editor height="300px" language="sql" v-model="sqlExec.sql" :options="{ readOnly: true }" />
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, reactive, watch, onMounted } from 'vue';
|
||||
import EnumValue from '@/common/Enum';
|
||||
import { dbApi } from '@/views/ops/db/api';
|
||||
import { DbSqlExecTypeEnum } from '@/views/ops/db/enums';
|
||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||
import { getDbDialect } from '@/views/ops/db/dialect';
|
||||
import ResourceTags from '@/views/ops/component/ResourceTags.vue';
|
||||
|
||||
const props = defineProps({
|
||||
// 业务key
|
||||
bizKey: {
|
||||
type: [String],
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
sqlExec: {
|
||||
sql: '',
|
||||
} as any,
|
||||
db: {} as any,
|
||||
});
|
||||
|
||||
const { sqlExec, db } = toRefs(state);
|
||||
|
||||
onMounted(() => {
|
||||
getDbSqlExec(props.bizKey);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.bizKey,
|
||||
(newValue: any) => {
|
||||
getDbSqlExec(newValue);
|
||||
}
|
||||
);
|
||||
|
||||
const getDbSqlExec = async (bizKey: string) => {
|
||||
if (!bizKey) {
|
||||
return;
|
||||
}
|
||||
const res = await dbApi.getSqlExecs.request({ flowBizKey: bizKey });
|
||||
if (!res.list) {
|
||||
return;
|
||||
}
|
||||
state.sqlExec = res.list?.[0];
|
||||
const dbRes = await dbApi.dbs.request({ id: state.sqlExec.dbId });
|
||||
state.db = dbRes.list?.[0];
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
@@ -1,80 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-descriptions :column="3" border>
|
||||
<el-descriptions-item :span="1" label="名称">{{ redis?.name }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="id">{{ redis?.id }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="用户名">{{ redis?.username }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="3" label="关联标签"><ResourceTags :tags="redis.tags" /></el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="1" label="主机">{{ `${redis?.host}` }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="库">{{ state.db }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="mode">
|
||||
{{ redis.mode }}
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="3" label="执行Cmd">
|
||||
<el-input type="textarea" disabled v-model="cmd" rows="5" />
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, reactive, watch, onMounted } from 'vue';
|
||||
import ResourceTags from '@/views/ops/component/ResourceTags.vue';
|
||||
import { redisApi } from '@/views/ops/redis/api';
|
||||
|
||||
const props = defineProps({
|
||||
// 业务表单
|
||||
bizForm: {
|
||||
type: [String],
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
cmd: '',
|
||||
db: 0,
|
||||
redis: {} as any,
|
||||
});
|
||||
|
||||
const { cmd, redis } = toRefs(state);
|
||||
|
||||
onMounted(() => {
|
||||
parseRunCmdForm(props.bizForm);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.bizForm,
|
||||
(newValue: any) => {
|
||||
parseRunCmdForm(newValue);
|
||||
}
|
||||
);
|
||||
|
||||
const parseRunCmdForm = async (bizForm: string) => {
|
||||
if (!bizForm) {
|
||||
return;
|
||||
}
|
||||
const form = JSON.parse(bizForm);
|
||||
|
||||
const cmds = form.cmd.map((item: any, index: number) => {
|
||||
if (index === 0) {
|
||||
return item; // 第一个元素直接返回原值
|
||||
}
|
||||
if (typeof item === 'string') {
|
||||
return `'${item}'`; // 字符串加单引号
|
||||
}
|
||||
return item; // 其他类型直接返回
|
||||
});
|
||||
state.cmd = cmds.join(' ');
|
||||
state.db = form.db;
|
||||
|
||||
const res = await redisApi.redisList.request({ id: form.id });
|
||||
if (!res.list) {
|
||||
return;
|
||||
}
|
||||
state.redis = res.list?.[0];
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
103
mayfly_go_web/src/views/flow/flowbiz/dbms/DbSqlExecBiz.vue
Executable file
103
mayfly_go_web/src/views/flow/flowbiz/dbms/DbSqlExecBiz.vue
Executable file
@@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-descriptions :column="3" border>
|
||||
<el-descriptions-item :span="3" label="标签"><TagCodePath :path="db.codePaths" /></el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="1" label="名称">{{ db?.name }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="主机">
|
||||
<SvgIcon :name="getDbDialect(db?.type).getInfo().icon" :size="20" />
|
||||
{{ `${db?.host}:${db?.port}` }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="数据库">{{ dbName }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="执行SQL">
|
||||
<monaco-editor height="300px" language="sql" v-model="sql" :options="{ readOnly: true }" />
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div v-if="runRes && runRes.length > 0">
|
||||
<el-divider content-position="left">处理结果</el-divider>
|
||||
<el-table :data="runRes" :max-height="400">
|
||||
<el-table-column prop="sql" label="SQL" show-overflow-tooltip />
|
||||
|
||||
<el-table-column prop="res" label="执行结果" :min-width="30" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<el-popover placement="top" :width="400" trigger="hover">
|
||||
<template #reference>
|
||||
<el-link icon="view" :type="scope.row.errorMsg ? 'danger' : 'success'" :underline="false"> </el-link>
|
||||
</template>
|
||||
|
||||
<el-text v-if="scope.row.errorMsg">{{ scope.row.errorMsg }}</el-text>
|
||||
<el-table v-else :data="scope.row.res" size="small">
|
||||
<el-table-column v-for="col in scope.row.columns" :key="col.name" :label="col.name" :prop="col.name" />
|
||||
</el-table>
|
||||
</el-popover>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- <el-table-column prop="errorMsg" label="错误信息" :min-width="60" show-overflow-tooltip /> -->
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, reactive, watch, onMounted } from 'vue';
|
||||
import { dbApi } from '@/views/ops/db/api';
|
||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||
import { getDbDialect } from '@/views/ops/db/dialect';
|
||||
import { tagApi } from '@/views/ops/tag/api';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import TagCodePath from '@/views/ops/component/TagCodePath.vue';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
|
||||
const props = defineProps({
|
||||
procinst: {
|
||||
type: [Object],
|
||||
default: () => {},
|
||||
},
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
// sqlExec: {
|
||||
// sql: '',
|
||||
// } as any,
|
||||
db: {} as any,
|
||||
dbName: '',
|
||||
sql: '',
|
||||
runRes: [],
|
||||
});
|
||||
|
||||
const { db, dbName, sql, runRes } = toRefs(state);
|
||||
|
||||
onMounted(() => {
|
||||
parseBizForm(props.procinst.bizForm);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.procinst.bizForm,
|
||||
(newValue: any) => {
|
||||
parseBizForm(newValue);
|
||||
}
|
||||
);
|
||||
|
||||
const parseBizForm = async (bizFormStr: string) => {
|
||||
if (props.procinst.bizHandleRes) {
|
||||
state.runRes = JSON.parse(props.procinst.bizHandleRes);
|
||||
} else {
|
||||
state.runRes = [];
|
||||
}
|
||||
|
||||
const bizForm = JSON.parse(bizFormStr);
|
||||
state.sql = bizForm.sql;
|
||||
state.dbName = bizForm.dbName;
|
||||
|
||||
const dbRes = await dbApi.dbs.request({ id: bizForm.dbId });
|
||||
state.db = dbRes.list?.[0];
|
||||
|
||||
tagApi.listByQuery.request({ type: TagResourceTypeEnum.DbName.value, codes: state.db.code }).then((res) => {
|
||||
state.db.codePaths = res.map((item: any) => item.codePath);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
83
mayfly_go_web/src/views/flow/flowbiz/dbms/DbSqlExecFlowBizForm.vue
Executable file
83
mayfly_go_web/src/views/flow/flowbiz/dbms/DbSqlExecFlowBizForm.vue
Executable file
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<el-form :model="bizForm" ref="formRef" :rules="rules" label-width="auto">
|
||||
<el-form-item prop="dbId" label="数据库" required>
|
||||
<db-select-tree
|
||||
placeholder="请选择数据库"
|
||||
v-model:db-id="bizForm.dbId"
|
||||
v-model:db-name="bizForm.dbName"
|
||||
v-model:db-type="dbType"
|
||||
@select-db="changeResourceCode"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="sql" label="SQL" required>
|
||||
<div class="w100">
|
||||
<monaco-editor height="300px" language="sql" v-model="bizForm.sql" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
|
||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||
import { registerDbCompletionItemProvider } from '@/views/ops/db/db';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
|
||||
const rules = {
|
||||
dbId: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择数据库',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
sql: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入执行SQL',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const emit = defineEmits(['changeResourceCode']);
|
||||
|
||||
const formRef: any = ref(null);
|
||||
|
||||
const bizForm = defineModel<any>('bizForm', {
|
||||
default: {
|
||||
dbId: 0,
|
||||
dbName: '',
|
||||
sql: '',
|
||||
},
|
||||
});
|
||||
|
||||
const dbType = ref('');
|
||||
|
||||
watch(
|
||||
() => bizForm.value.dbId,
|
||||
() => {
|
||||
registerDbCompletionItemProvider(bizForm.value.dbId, bizForm.value.dbName, [bizForm.value.dbName], dbType.value);
|
||||
}
|
||||
);
|
||||
|
||||
const changeResourceCode = async (db: any) => {
|
||||
emit('changeResourceCode', TagResourceTypeEnum.Db.value, db.code);
|
||||
};
|
||||
|
||||
const validateBizForm = async () => {
|
||||
return formRef.value.validate();
|
||||
};
|
||||
|
||||
const resetBizForm = () => {
|
||||
//重置表单域
|
||||
formRef.value.resetFields();
|
||||
bizForm.value.dbId = 0;
|
||||
bizForm.value.dbName = '';
|
||||
};
|
||||
|
||||
defineExpose({ validateBizForm, resetBizForm });
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
89
mayfly_go_web/src/views/flow/flowbiz/redis/RedisRunCmdBiz.vue
Executable file
89
mayfly_go_web/src/views/flow/flowbiz/redis/RedisRunCmdBiz.vue
Executable file
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-descriptions :column="3" border>
|
||||
<el-descriptions-item :span="3" label="标签"><TagCodePath :path="redis.codePaths" /></el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="2" label="编号">{{ redis?.code }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="名称">{{ redis?.name }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="1" label="主机">{{ `${redis?.host}` }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="库">{{ state.db }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="mode">
|
||||
{{ redis.mode }}
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="3" label="执行Cmd">
|
||||
<el-input type="textarea" disabled v-model="cmd" rows="5" />
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div v-if="runRes && runRes.length > 0">
|
||||
<el-divider content-position="left">处理结果</el-divider>
|
||||
<el-table :data="runRes" :max-height="400">
|
||||
<el-table-column prop="cmd" label="命令" show-overflow-tooltip />
|
||||
<el-table-column prop="res" label="执行结果" :min-width="50" show-overflow-tooltip> </el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, reactive, watch, onMounted } from 'vue';
|
||||
import { redisApi } from '@/views/ops/redis/api';
|
||||
import TagCodePath from '@/views/ops/component/TagCodePath.vue';
|
||||
import { tagApi } from '@/views/ops/tag/api';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
|
||||
const props = defineProps({
|
||||
procinst: {
|
||||
type: [Object],
|
||||
default: () => {},
|
||||
},
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
cmd: '',
|
||||
runRes: [],
|
||||
db: 0,
|
||||
redis: {} as any,
|
||||
});
|
||||
|
||||
const { cmd, redis, runRes } = toRefs(state);
|
||||
|
||||
onMounted(() => {
|
||||
parseRunCmdForm(props.procinst.bizForm);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.procinst.bizForm,
|
||||
(newValue: any) => {
|
||||
parseRunCmdForm(newValue);
|
||||
}
|
||||
);
|
||||
|
||||
const parseRunCmdForm = async (bizFormStr: string) => {
|
||||
if (props.procinst.bizHandleRes) {
|
||||
state.runRes = JSON.parse(props.procinst.bizHandleRes);
|
||||
} else {
|
||||
state.runRes = [];
|
||||
}
|
||||
|
||||
if (!bizFormStr) {
|
||||
return;
|
||||
}
|
||||
const bizForm = JSON.parse(bizFormStr);
|
||||
state.cmd = bizForm.cmd;
|
||||
state.db = bizForm.db;
|
||||
|
||||
const res = await redisApi.redisList.request({ id: bizForm.id });
|
||||
if (!res.list) {
|
||||
return;
|
||||
}
|
||||
state.redis = res.list?.[0];
|
||||
|
||||
tagApi.listByQuery.request({ type: TagResourceTypeEnum.Redis.value, codes: state.redis.code }).then((res) => {
|
||||
state.redis.codePaths = res.map((item: any) => item.codePath);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
148
mayfly_go_web/src/views/flow/flowbiz/redis/RedisRunCmdFlowBizForm.vue
Executable file
148
mayfly_go_web/src/views/flow/flowbiz/redis/RedisRunCmdFlowBizForm.vue
Executable file
@@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<el-form :model="bizForm" ref="formRef" :rules="rules" label-width="auto">
|
||||
<el-form-item prop="id" label="库" required>
|
||||
<TagTreeResourceSelect
|
||||
v-bind="$attrs"
|
||||
v-model="selectRedis"
|
||||
@change="changeRedis"
|
||||
:resource-type="TagResourceTypeEnum.Redis.value"
|
||||
:tag-path-node-type="NodeTypeTagPath"
|
||||
placeholder="请选择Redis实例与库"
|
||||
>
|
||||
</TagTreeResourceSelect>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="cmd" label="CMD" required>
|
||||
<el-input type="textarea" v-model="bizForm.cmd" placeholder="如: SET 'key' 'value'; 多条命令;分割" :rows="5" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import TagTreeResourceSelect from '@/views/ops/component/TagTreeResourceSelect.vue';
|
||||
import { NodeType, TagTreeNode } from '@/views/ops/component/tag';
|
||||
import { redisApi } from '@/views/ops/redis/api';
|
||||
import { sleep } from '@/common/utils/loading';
|
||||
|
||||
const rules = {
|
||||
id: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择Redis实例',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
cmd: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入执行CMD',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// tagpath 节点类型
|
||||
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const res = await redisApi.redisList.request({ tagPath: parentNode.key });
|
||||
if (!res.total) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const redisInfos = res.list;
|
||||
await sleep(100);
|
||||
return redisInfos.map((x: any) => {
|
||||
x.tagPath = parentNode.key;
|
||||
return new TagTreeNode(`${x.code}`, x.name, NodeTypeRedis).withParams(x);
|
||||
});
|
||||
});
|
||||
|
||||
// redis实例节点类型
|
||||
const NodeTypeRedis = new NodeType(1).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const redisInfo = parentNode.params;
|
||||
|
||||
let dbs: TagTreeNode[] = redisInfo.db.split(',').map((x: string) => {
|
||||
return new TagTreeNode(x, `db${x}`, 2 as any).withIsLeaf(true).withParams({
|
||||
id: redisInfo.id,
|
||||
db: x,
|
||||
name: `db${x}`,
|
||||
keys: 0,
|
||||
tagPath: redisInfo.tagPath,
|
||||
redisName: redisInfo.name,
|
||||
code: redisInfo.code,
|
||||
});
|
||||
});
|
||||
|
||||
if (redisInfo.mode == 'cluster') {
|
||||
return dbs;
|
||||
}
|
||||
|
||||
const res = await redisApi.redisInfo.request({ id: redisInfo.id, host: redisInfo.host, section: 'Keyspace' });
|
||||
for (let db in res.Keyspace) {
|
||||
for (let d of dbs) {
|
||||
if (db == d.params.name) {
|
||||
d.params.keys = res.Keyspace[db]?.split(',')[0]?.split('=')[1] || 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 替换label
|
||||
dbs.forEach((e: any) => {
|
||||
e.label = `${e.params.name}`;
|
||||
});
|
||||
return dbs;
|
||||
});
|
||||
|
||||
const emit = defineEmits(['changeResourceCode']);
|
||||
|
||||
const formRef: any = ref(null);
|
||||
|
||||
const bizForm = defineModel<any>('bizForm', {
|
||||
default: {
|
||||
id: 0,
|
||||
db: 0,
|
||||
cmd: '',
|
||||
},
|
||||
});
|
||||
|
||||
const redisName = ref('');
|
||||
const tagPath = ref('');
|
||||
|
||||
const selectRedis = computed({
|
||||
get: () => {
|
||||
return redisName.value ? `${tagPath.value} > ${redisName.value} > db${bizForm.value.db}` : '';
|
||||
},
|
||||
set: () => {
|
||||
//
|
||||
},
|
||||
});
|
||||
|
||||
const changeRedis = (nodeData: TagTreeNode) => {
|
||||
const params = nodeData.params;
|
||||
tagPath.value = params.tagPath;
|
||||
redisName.value = params.redisName;
|
||||
bizForm.value.id = params.id;
|
||||
bizForm.value.db = parseInt(params.db);
|
||||
|
||||
changeResourceCode(params.code);
|
||||
};
|
||||
|
||||
const changeResourceCode = async (redisCode: any) => {
|
||||
emit('changeResourceCode', TagResourceTypeEnum.Redis.value, redisCode);
|
||||
};
|
||||
|
||||
const validateBizForm = async () => {
|
||||
return formRef.value.validate();
|
||||
};
|
||||
|
||||
const resetBizForm = () => {
|
||||
//重置表单域
|
||||
formRef.value.resetFields();
|
||||
bizForm.value.id = 0;
|
||||
bizForm.value.db = 0;
|
||||
bizForm.value.cmd = '';
|
||||
};
|
||||
|
||||
defineExpose({ validateBizForm, resetBizForm });
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
@@ -6,7 +6,15 @@
|
||||
<el-card shadow="hover" header="个人信息">
|
||||
<div class="personal-user">
|
||||
<div class="personal-user-left">
|
||||
<el-upload class="h100 personal-user-left-upload" action="" multiple :limit="1">
|
||||
<el-upload
|
||||
class="h100 personal-user-left-upload"
|
||||
:action="getUploadFileUrl(`avatar_${userInfo.username}`)"
|
||||
:limit="1"
|
||||
:show-file-list="false"
|
||||
:before-upload="beforeAvatarUpload"
|
||||
:on-success="handleAvatarSuccess"
|
||||
accept=".png,.jpg,.jpeg"
|
||||
>
|
||||
<img :src="userInfo.photo" />
|
||||
</el-upload>
|
||||
</div>
|
||||
@@ -89,7 +97,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column prop="codePath" min-width="400" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<TagCodePath :path="scope.row.codePath" />
|
||||
<TagCodePath :path="scope.row.codePath" :tagInfos="state.machine.tagInfos" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column width="30">
|
||||
@@ -123,7 +131,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column prop="codePath" min-width="380" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<TagCodePath :path="scope.row.codePath" />
|
||||
<TagCodePath :path="scope.row.codePath" :tagInfos="state.db.tagInfos" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column width="30">
|
||||
@@ -164,7 +172,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column prop="codePath" min-width="380" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<TagCodePath :path="scope.row.codePath" />
|
||||
<TagCodePath :path="scope.row.codePath" :tagInfos="state.redis.tagInfos" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column width="30">
|
||||
@@ -203,7 +211,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column prop="codePath" min-width="380" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<TagCodePath :path="scope.row.codePath" />
|
||||
<TagCodePath :path="scope.row.codePath" :tagInfos="state.mongo.tagInfos" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column width="30">
|
||||
@@ -249,20 +257,23 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, reactive, onMounted, computed } from 'vue';
|
||||
import { computed, onMounted, reactive, toRefs } from 'vue';
|
||||
// import * as echarts from 'echarts';
|
||||
import { formatAxis } from '@/common/utils/format';
|
||||
import { formatAxis, formatDate } from '@/common/utils/format';
|
||||
import { indexApi } from './api';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { storeToRefs } from 'pinia';
|
||||
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';
|
||||
import { getAllTagInfoByCodePaths } from '../ops/component/tag';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { getFileUrl, getUploadFileUrl } from '@/common/request';
|
||||
import { saveUser } from '@/common/utils/storage';
|
||||
|
||||
const router = useRouter();
|
||||
const { userInfo } = storeToRefs(useUserInfo());
|
||||
@@ -288,18 +299,22 @@ const state = reactive({
|
||||
machine: {
|
||||
num: 0,
|
||||
opLogs: [],
|
||||
tagInfos: {},
|
||||
},
|
||||
db: {
|
||||
num: 0,
|
||||
opLogs: [],
|
||||
tagInfos: {},
|
||||
},
|
||||
redis: {
|
||||
num: 0,
|
||||
opLogs: [],
|
||||
tagInfos: {},
|
||||
},
|
||||
mongo: {
|
||||
num: 0,
|
||||
opLogs: [],
|
||||
tagInfos: {},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -354,25 +369,56 @@ const getMsgs = async () => {
|
||||
return await personApi.getMsgs.request(state.msgDialog.query);
|
||||
};
|
||||
|
||||
const beforeAvatarUpload = (rawFile: any) => {
|
||||
if (rawFile.size >= 512 * 1024) {
|
||||
ElMessage.error('头像不能超过512KB!');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleAvatarSuccess = (response: any, uploadFile: any) => {
|
||||
userInfo.value.photo = URL.createObjectURL(uploadFile.raw);
|
||||
|
||||
const newUser = { ...userInfo.value };
|
||||
newUser.photo = getFileUrl(`avatar_${userInfo.value.username}`);
|
||||
// 存储用户信息到浏览器缓存
|
||||
saveUser(newUser);
|
||||
};
|
||||
|
||||
// 初始化数字滚动
|
||||
const initData = async () => {
|
||||
resourceOpLogApi.getAccountResourceOpLogs
|
||||
.request({ resourceType: TagResourceTypeEnum.MachineAuthCert.value, pageSize: state.defaultLogSize })
|
||||
.then((res: any) => {
|
||||
.then(async (res: any) => {
|
||||
const tagInfos = await getAllTagInfoByCodePaths(res.list?.map((item: any) => item.codePath));
|
||||
state.machine.tagInfos = tagInfos;
|
||||
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.DbName.value, pageSize: state.defaultLogSize })
|
||||
.then(async (res: any) => {
|
||||
const tagInfos = await getAllTagInfoByCodePaths(res.list?.map((item: any) => item.codePath));
|
||||
state.db.tagInfos = tagInfos;
|
||||
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.Redis.value, pageSize: state.defaultLogSize })
|
||||
.then(async (res: any) => {
|
||||
const tagInfos = await getAllTagInfoByCodePaths(res.list?.map((item: any) => item.codePath));
|
||||
state.redis.tagInfos = tagInfos;
|
||||
state.redis.opLogs = res.list;
|
||||
});
|
||||
|
||||
resourceOpLogApi.getAccountResourceOpLogs.request({ resourceType: TagResourceTypeEnum.Mongo.value, pageSize: state.defaultLogSize }).then((res: any) => {
|
||||
state.mongo.opLogs = res.list;
|
||||
});
|
||||
resourceOpLogApi.getAccountResourceOpLogs
|
||||
.request({ resourceType: TagResourceTypeEnum.Mongo.value, pageSize: state.defaultLogSize })
|
||||
.then(async (res: any) => {
|
||||
const tagInfos = await getAllTagInfoByCodePaths(res.list?.map((item: any) => item.codePath));
|
||||
state.mongo.tagInfos = tagInfos;
|
||||
state.mongo.opLogs = res.list;
|
||||
});
|
||||
|
||||
indexApi.machineDashbord.request().then((res: any) => {
|
||||
state.machine.num = res.machineNum;
|
||||
@@ -425,7 +471,7 @@ const toPage = (item: any, codePath = '') => {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/theme/mixins/index.scss';
|
||||
@use '@/theme/mixins/index.scss' as mixins;
|
||||
|
||||
.personal {
|
||||
.personal-user {
|
||||
@@ -463,7 +509,7 @@ const toPage = (item: any, codePath = '') => {
|
||||
|
||||
.personal-title {
|
||||
font-size: 18px;
|
||||
@include text-ellipsis(1);
|
||||
@include mixins.text-ellipsis(1);
|
||||
}
|
||||
|
||||
.personal-item {
|
||||
@@ -473,11 +519,11 @@ const toPage = (item: any, codePath = '') => {
|
||||
|
||||
.personal-item-label {
|
||||
color: gray;
|
||||
@include text-ellipsis(1);
|
||||
@include mixins.text-ellipsis(1);
|
||||
}
|
||||
|
||||
.personal-item-value {
|
||||
@include text-ellipsis(1);
|
||||
@include mixins.text-ellipsis(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -508,7 +554,7 @@ const toPage = (item: any, codePath = '') => {
|
||||
|
||||
.personal-info-li-title {
|
||||
display: inline-block;
|
||||
@include text-ellipsis(1);
|
||||
@include mixins.text-ellipsis(1);
|
||||
color: grey;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@@ -144,6 +144,7 @@ import { personApi } from '@/views/personal/api';
|
||||
import { AccountUsernamePattern } from '@/common/pattern';
|
||||
import { getToken } from '@/common/utils/storage';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
import { getFileUrl } from '@/common/request';
|
||||
|
||||
const rules = {
|
||||
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
||||
@@ -347,26 +348,34 @@ const updateUserInfo = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const loginResDeal = (loginRes: any) => {
|
||||
const loginResDeal = async (loginRes: any) => {
|
||||
state.loginRes = loginRes;
|
||||
// 用户信息
|
||||
const userInfos = {
|
||||
name: loginRes.name,
|
||||
username: loginRes.username,
|
||||
// 头像
|
||||
photo: letterAvatar(loginRes.username),
|
||||
time: new Date().getTime(),
|
||||
lastLoginTime: loginRes.lastLoginTime,
|
||||
lastLoginIp: loginRes.lastLoginIp,
|
||||
photo: '',
|
||||
};
|
||||
|
||||
const avatarFileKey = `avatar_${loginRes.username}`;
|
||||
const avatarFileDetail = await openApi.getFileDetail([avatarFileKey]);
|
||||
// 说明存在头像文件
|
||||
if (avatarFileDetail.length > 0) {
|
||||
userInfos.photo = getFileUrl(avatarFileKey);
|
||||
} else {
|
||||
userInfos.photo = letterAvatar(loginRes.username);
|
||||
}
|
||||
|
||||
// 存储用户信息到浏览器缓存
|
||||
saveUser(userInfos);
|
||||
// 1、请注意执行顺序(存储用户信息到vuex)
|
||||
useUserInfo().setUserInfo(userInfos);
|
||||
|
||||
const token = loginRes.token;
|
||||
// 如果不需要 otp校验,则该token即为accessToken,否则为otp校验token
|
||||
// 如果不需要otp校验,则该token即为accessToken,否则为otp校验token
|
||||
if (loginRes.otp == -1) {
|
||||
signInSuccess(token, loginRes.refresh_token);
|
||||
return;
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<el-form-item prop="name" label="名称" required>
|
||||
<el-form-item v-if="form.type == AuthCertTypeEnum.Public.value" prop="name" label="名称" required>
|
||||
<el-input :disabled="form.id" v-model="form.name" placeholder="请输入凭证名 (全局唯一)"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="name" label="名称" show-overflow-tooltip min-width="100px"> </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">
|
||||
@@ -117,17 +117,17 @@ const cancelEdit = () => {
|
||||
|
||||
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) {
|
||||
// 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;
|
||||
@@ -135,8 +135,8 @@ const btnOk = async (authCert: any) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (authCerts.value?.filter((x: any) => x.username == authCert.username || x.name == authCert.name).length > 0) {
|
||||
ElMessage.error('该名称或用户名已存在于该账号列表中');
|
||||
if (authCerts.value?.filter((x: any) => x.username == authCert.username).length > 0) {
|
||||
ElMessage.error('该用户名已存在于该账号列表中');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<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">
|
||||
<div v-if="codePaths">
|
||||
<el-row v-for="(path, idx) in codePaths?.slice(0, 1)" :key="idx">
|
||||
<span v-for="item in 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>
|
||||
<span> {{ item.name ? item.name : item.code }}</span>
|
||||
<SvgIcon v-if="!item.isEnd" class="mr5 ml5" name="arrow-right" />
|
||||
</span>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
:color="EnumValue.getEnumByValue(TagResourceTypeEnum, item.type)?.extra.iconColor"
|
||||
class="mr2"
|
||||
/>
|
||||
<span> {{ item.code }}</span>
|
||||
<span> {{ item.name ? item.name : item.code }}</span>
|
||||
<SvgIcon v-if="!item.isEnd" class="mr5 ml5" name="arrow-right" />
|
||||
</span>
|
||||
</el-row>
|
||||
@@ -36,14 +36,21 @@
|
||||
<script lang="ts" setup>
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import EnumValue from '@/common/Enum';
|
||||
import { computed } from 'vue';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { getAllTagInfoByCodePaths } from './tag';
|
||||
|
||||
const props = defineProps({
|
||||
path: {
|
||||
type: [String, Array<string>],
|
||||
},
|
||||
tagInfos: {
|
||||
type: Object, // key: code , value: code info
|
||||
},
|
||||
});
|
||||
|
||||
const codePaths: any = ref([]);
|
||||
let allTagInfos: any = {};
|
||||
|
||||
const paths = computed(() => {
|
||||
if (Array.isArray(props.path)) {
|
||||
return props.path;
|
||||
@@ -52,6 +59,32 @@ const paths = computed(() => {
|
||||
return [props.path];
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
setCodePaths();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.path,
|
||||
() => {
|
||||
setCodePaths();
|
||||
}
|
||||
);
|
||||
|
||||
const setCodePaths = async () => {
|
||||
if (!paths.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!props.tagInfos || Object.keys(props.tagInfos).length == 0) {
|
||||
const tagInfos = await getAllTagInfoByCodePaths(paths.value as any);
|
||||
allTagInfos = tagInfos;
|
||||
} else {
|
||||
allTagInfos = props.tagInfos;
|
||||
}
|
||||
|
||||
codePaths.value = paths.value.map((p) => parseTagPath(p));
|
||||
};
|
||||
|
||||
const parseTagPath = (tagPath: string = '') => {
|
||||
if (!tagPath) {
|
||||
return [];
|
||||
@@ -61,27 +94,52 @@ const parseTagPath = (tagPath: string = '') => {
|
||||
for (let code of codes) {
|
||||
const typeAndCode = code.split('|');
|
||||
|
||||
let tagInfo;
|
||||
if (typeAndCode.length == 1) {
|
||||
const tagCode = typeAndCode[0];
|
||||
if (!tagCode) {
|
||||
continue;
|
||||
}
|
||||
|
||||
res.push({
|
||||
tagInfo = {
|
||||
type: TagResourceTypeEnum.Tag.value,
|
||||
code: typeAndCode[0],
|
||||
});
|
||||
};
|
||||
res.push(tagInfo);
|
||||
continue;
|
||||
} else {
|
||||
tagInfo = {
|
||||
type: typeAndCode[0],
|
||||
code: typeAndCode[1],
|
||||
name: '',
|
||||
};
|
||||
}
|
||||
|
||||
res.push({
|
||||
type: typeAndCode[0],
|
||||
code: typeAndCode[1],
|
||||
});
|
||||
const ti = getTagInfo(tagInfo.type, tagInfo.code);
|
||||
if (ti) {
|
||||
tagInfo.name = ti.name;
|
||||
}
|
||||
|
||||
res.push(tagInfo);
|
||||
}
|
||||
|
||||
res[res.length - 1].isEnd = true;
|
||||
return res;
|
||||
};
|
||||
|
||||
const getTagInfo = (type: any, code: string) => {
|
||||
if (type == TagResourceTypeEnum.Tag.value) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (allTagInfos && Object.keys(allTagInfos).length > 0) {
|
||||
const key = `${type}|${code}`;
|
||||
if (allTagInfos[key]) {
|
||||
return allTagInfos[key];
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
:default-expanded-keys="props.defaultExpandedKeys"
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<span @dblclick="treeNodeDblclick(data)" :class="data.type.nodeDblclickFunc ? 'none-select' : ''">
|
||||
<span :id="node.key" @dblclick="treeNodeDblclick(data)" :class="data.type.nodeDblclickFunc ? 'none-select' : ''">
|
||||
<span v-if="data.type.value == TagTreeNode.TagPath">
|
||||
<tag-info :tag-path="data.label" />
|
||||
</span>
|
||||
@@ -48,11 +48,12 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, reactive, ref, watch, toRefs } from 'vue';
|
||||
import { nextTick, onMounted, reactive, ref, toRefs, watch } from 'vue';
|
||||
import { NodeType, TagTreeNode } from './tag';
|
||||
import TagInfo from './TagInfo.vue';
|
||||
import { Contextmenu } from '@/components/contextmenu';
|
||||
import { tagApi } from '../tag/api';
|
||||
import { isPrefixSubsequence } from '@/common/utils/string';
|
||||
|
||||
const props = defineProps({
|
||||
resourceType: {
|
||||
@@ -105,8 +106,7 @@ watch(filterText, (val) => {
|
||||
});
|
||||
|
||||
const filterNode = (value: string, data: any) => {
|
||||
if (!value) return true;
|
||||
return data.label.includes(value);
|
||||
return !value || isPrefixSubsequence(value, data.label);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -126,7 +126,7 @@ const loadTags = async () => {
|
||||
* @param { Object } node
|
||||
* @param { Object } resolve
|
||||
*/
|
||||
const loadNode = async (node: any, resolve: any) => {
|
||||
const loadNode = async (node: any, resolve: (data: any) => void, reject: () => void) => {
|
||||
if (typeof resolve !== 'function') {
|
||||
return;
|
||||
}
|
||||
@@ -141,14 +141,16 @@ const loadNode = async (node: any, resolve: any) => {
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
// 调用 reject 以保持节点状态,并允许远程加载继续。
|
||||
return reject();
|
||||
}
|
||||
return resolve(nodes);
|
||||
};
|
||||
|
||||
const treeNodeClick = (data: any) => {
|
||||
const treeNodeClick = async (data: any) => {
|
||||
if (!data.disabled && !data.type.nodeDblclickFunc && data.type.nodeClickFunc) {
|
||||
emit('nodeClick', data);
|
||||
data.type.nodeClickFunc(data);
|
||||
await data.type.nodeClickFunc(data);
|
||||
}
|
||||
// 关闭可能存在的右击菜单
|
||||
contextmenuRef.value.closeContextmenu();
|
||||
@@ -207,6 +209,17 @@ const getNode = (nodeKey: any) => {
|
||||
|
||||
const setCurrentKey = (nodeKey: any) => {
|
||||
treeRef.value.setCurrentKey(nodeKey);
|
||||
|
||||
// 通过Id获取到对应的dom元素
|
||||
const node = document.getElementById(nodeKey);
|
||||
if (node) {
|
||||
setTimeout(() => {
|
||||
nextTick(() => {
|
||||
// 通过scrollIntoView方法将对应的dom元素定位到可见区域 【block: 'center'】这个属性是在垂直方向居中显示
|
||||
node.scrollIntoView({ block: 'center' });
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
v-bind="$attrs"
|
||||
ref="tagTreeRef"
|
||||
:data="state.tags"
|
||||
:default-expanded-keys="checkedTags"
|
||||
:default-expanded-keys="state.defaultExpandedKeys"
|
||||
:default-checked-keys="checkedTags"
|
||||
multiple
|
||||
:render-after-expand="true"
|
||||
@@ -31,9 +31,9 @@
|
||||
/>
|
||||
|
||||
<span class="font13 ml5">
|
||||
{{ data.code }}
|
||||
<span style="color: #3c8dbc">【</span>
|
||||
{{ data.name }}
|
||||
<span style="color: #3c8dbc">【</span>
|
||||
{{ data.code }}
|
||||
<span style="color: #3c8dbc">】</span>
|
||||
<el-tag v-if="data.children !== null" size="small">{{ data.children.length }} </el-tag>
|
||||
</span>
|
||||
@@ -50,6 +50,7 @@ import { ref, reactive, onMounted } from 'vue';
|
||||
import { tagApi } from '../tag/api';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import EnumValue from '@/common/Enum';
|
||||
import { isPrefixSubsequence } from '@/common/utils/string';
|
||||
|
||||
const props = defineProps({
|
||||
height: {
|
||||
@@ -74,10 +75,12 @@ const tagTreeRef: any = ref(null);
|
||||
const filterTag = ref('');
|
||||
|
||||
const state = reactive({
|
||||
defaultExpandedKeys: [] as any,
|
||||
tags: [],
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
state.defaultExpandedKeys = checkedTags.value;
|
||||
search();
|
||||
});
|
||||
|
||||
@@ -100,10 +103,7 @@ const search = async () => {
|
||||
};
|
||||
|
||||
const filterNode = (value: string, data: any) => {
|
||||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
return data.codePath.toLowerCase().includes(value) || data.name.includes(value);
|
||||
return !value || isPrefixSubsequence(value, data.codePath) || isPrefixSubsequence(value, data.name);
|
||||
};
|
||||
|
||||
const onFilterValChanged = (val: string) => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { OptionsApi, SearchItem } from '@/components/SearchForm';
|
||||
import { ContextmenuItem } from '@/components/contextmenu';
|
||||
import { tagApi } from '../tag/api';
|
||||
import {OptionsApi, SearchItem} from '@/components/SearchForm';
|
||||
import {ContextmenuItem} from '@/components/contextmenu';
|
||||
import {TagResourceTypeEnum} from '@/common/commonEnum';
|
||||
import {tagApi} from '../tag/api';
|
||||
|
||||
export class TagTreeNode {
|
||||
/**
|
||||
@@ -161,7 +162,7 @@ export class NodeType {
|
||||
*/
|
||||
export function getTagPathSearchItem(resourceType: number) {
|
||||
return SearchItem.select('tagPath', '标签').withOptionsApi(
|
||||
OptionsApi.new(tagApi.getResourceTagPaths, { resourceType }).withConvertFn((res: any) => {
|
||||
OptionsApi.new(tagApi.getResourceTagPaths, {resourceType}).withConvertFn((res: any) => {
|
||||
return res.map((x: any) => {
|
||||
return {
|
||||
label: x,
|
||||
@@ -178,7 +179,8 @@ export function getTagPathSearchItem(resourceType: number) {
|
||||
* @returns {1: ['xxx'], 11: ['yyy']}
|
||||
*/
|
||||
export function getTagTypeCodeByPath(codePath: string) {
|
||||
const result = {};
|
||||
const result: any = {};
|
||||
if (!codePath) return result
|
||||
const parts = codePath.split('/'); // 切分字符串并保留数字和对应的值部分
|
||||
|
||||
for (let part of parts) {
|
||||
@@ -200,6 +202,40 @@ export function getTagTypeCodeByPath(codePath: string) {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 完善标签路径信息
|
||||
* @param codePaths 标签路径
|
||||
* @returns
|
||||
*/
|
||||
export async function getAllTagInfoByCodePaths(codePaths: string[]) {
|
||||
if (!codePaths) return
|
||||
const allTypeAndCode: any = {};
|
||||
|
||||
for (let codePath of codePaths) {
|
||||
const typeAndCode = getTagTypeCodeByPath(codePath);
|
||||
for (let type in typeAndCode) {
|
||||
allTypeAndCode[type] = [...new Set(typeAndCode[type].concat(allTypeAndCode[type] || []))];
|
||||
}
|
||||
}
|
||||
|
||||
for (let type in allTypeAndCode) {
|
||||
if (type == TagResourceTypeEnum.Tag.value) {
|
||||
continue;
|
||||
}
|
||||
const tagInfo = await tagApi.listByQuery.request({type: type, codes: allTypeAndCode[type]});
|
||||
allTypeAndCode[type] = tagInfo;
|
||||
}
|
||||
|
||||
const code2CodeInfo: any = {};
|
||||
for (let type in allTypeAndCode) {
|
||||
for (let code of allTypeAndCode[type]) {
|
||||
code2CodeInfo[`${type}|${code.code}`] = code;
|
||||
}
|
||||
}
|
||||
|
||||
return code2CodeInfo;
|
||||
}
|
||||
|
||||
export function expandCodePath(codePath: string) {
|
||||
const parts = codePath.split('/');
|
||||
const result = [];
|
||||
|
||||
@@ -10,14 +10,6 @@
|
||||
width="38%"
|
||||
>
|
||||
<el-form :model="form" ref="dbForm" :rules="rules" label-width="auto">
|
||||
<el-form-item prop="code" label="编号" required>
|
||||
<el-input
|
||||
:disabled="form.id"
|
||||
v-model.trim="form.code"
|
||||
placeholder="请输入编号 (大小写字母、数字、_-.:), 不可修改"
|
||||
auto-complete="off"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="name" label="名称" required>
|
||||
<el-input v-model.trim="form.name" placeholder="请输入数据库别名" auto-complete="off"></el-input>
|
||||
</el-form-item>
|
||||
@@ -88,7 +80,6 @@ import { dbApi } from './api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import type { CheckboxValueType } from 'element-plus';
|
||||
import { DbType } from '@/views/ops/db/dialect';
|
||||
import { ResourceCodePattern } from '@/common/pattern';
|
||||
|
||||
import EnumTag from '@/components/enumtag/EnumTag.vue';
|
||||
import { AuthCertCiphertextTypeEnum } from '../tag/enums';
|
||||
@@ -130,18 +121,6 @@ const rules = {
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
code: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入编码',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
{
|
||||
pattern: ResourceCodePattern.pattern,
|
||||
message: ResourceCodePattern.message,
|
||||
trigger: ['blur'],
|
||||
},
|
||||
],
|
||||
name: [
|
||||
{
|
||||
required: true,
|
||||
|
||||
@@ -1,93 +1,112 @@
|
||||
<template>
|
||||
<div class="db-list">
|
||||
<page-table
|
||||
ref="pageTableRef"
|
||||
:page-api="dbApi.dbs"
|
||||
:before-query-fn="checkRouteTagPath"
|
||||
:search-items="searchItems"
|
||||
v-model:query-form="query"
|
||||
:columns="columns"
|
||||
lazy
|
||||
<el-drawer
|
||||
:title="title"
|
||||
v-model="dialogVisible"
|
||||
@open="search"
|
||||
:before-close="cancel"
|
||||
:destroy-on-close="true"
|
||||
:close-on-click-modal="true"
|
||||
size="60%"
|
||||
>
|
||||
<template #instanceSelect>
|
||||
<el-select remote :remote-method="getInstances" v-model="query.instanceId" placeholder="输入并选择实例" filterable clearable>
|
||||
<el-option v-for="item in state.instances" :key="item.id" :label="`${item.name}`" :value="item.id">
|
||||
{{ item.name }}
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
{{ item.type }} / {{ item.host }}:{{ item.port }}
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
{{ item.username }}
|
||||
</el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<template #type="{ data }">
|
||||
<el-tooltip :content="data.type" placement="top">
|
||||
<SvgIcon :name="getDbDialect(data.type).getInfo().icon" :size="20" />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
|
||||
<template #host="{ data }">
|
||||
{{ `${data.host}:${data.port}` }}
|
||||
</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 #header>
|
||||
<DrawerHeader :header="title" :back="cancel">
|
||||
<template #extra>
|
||||
<div class="mr20">
|
||||
<span>{{ $props.instance?.tags?.[0]?.codePath }}</span>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
<SvgIcon :name="getDbDialect($props.instance?.type).getInfo()?.icon" :size="20" />
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
<span>{{ $props.instance?.host }}:{{ $props.instance?.port }}</span>
|
||||
</div>
|
||||
</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>
|
||||
</DrawerHeader>
|
||||
</template>
|
||||
|
||||
<template #tagPath="{ data }">
|
||||
<ResourceTags :tags="data.tags" />
|
||||
</template>
|
||||
<page-table
|
||||
ref="pageTableRef"
|
||||
:page-api="dbApi.dbs"
|
||||
v-model:query-form="query"
|
||||
:columns="columns"
|
||||
lazy
|
||||
show-selection
|
||||
v-model:selection-data="state.selectionData"
|
||||
>
|
||||
<template #tableHeader>
|
||||
<el-button v-auth="perms.saveDb" type="primary" circle icon="Plus" @click="editDb(null)"> </el-button>
|
||||
<el-button v-auth="perms.delDb" :disabled="state.selectionData.length < 1" @click="deleteDb" type="danger" circle icon="delete"></el-button>
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
<el-button type="primary" @click="onShowSqlExec(data)" link>SQL记录</el-button>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
<template #type="{ data }">
|
||||
<el-tooltip :content="data.type" placement="top">
|
||||
<SvgIcon :name="getDbDialect(data.type).getInfo().icon" :size="20" />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
|
||||
<el-dropdown @command="handleMoreActionCommand">
|
||||
<span class="el-dropdown-link-more">
|
||||
更多
|
||||
<el-icon class="el-icon--right">
|
||||
<arrow-down />
|
||||
</el-icon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item :command="{ type: 'detail', data }"> 详情 </el-dropdown-item>
|
||||
<el-dropdown-item :command="{ type: 'dumpDb', data }"> 导出 </el-dropdown-item>
|
||||
<el-dropdown-item :command="{ type: 'backupDb', data }" v-if="actionBtns[perms.backupDb] && supportAction('backupDb', data.type)">
|
||||
备份任务
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
:command="{ type: 'backupHistory', data }"
|
||||
v-if="actionBtns[perms.backupDb] && supportAction('backupDb', data.type)"
|
||||
>
|
||||
备份历史
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
:command="{ type: 'restoreDb', data }"
|
||||
v-if="actionBtns[perms.restoreDb] && supportAction('restoreDb', data.type)"
|
||||
>
|
||||
恢复任务
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
</page-table>
|
||||
<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>
|
||||
|
||||
<el-dialog width="750px" :title="`${db} 数据库导出`" v-model="exportDialog.visible">
|
||||
<template #tagPath="{ data }">
|
||||
<ResourceTags :tags="data.tags" />
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
<el-button v-auth="perms.saveDb" @click="editDb(data)" type="primary" link>编辑</el-button>
|
||||
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-button type="primary" @click="onShowSqlExec(data)" link>SQL记录</el-button>
|
||||
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-dropdown @command="handleMoreActionCommand">
|
||||
<span class="el-dropdown-link-more">
|
||||
更多
|
||||
<el-icon class="el-icon--right">
|
||||
<arrow-down />
|
||||
</el-icon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item :command="{ type: 'dumpDb', data }"> 导出 </el-dropdown-item>
|
||||
<!-- <el-dropdown-item
|
||||
:command="{ type: 'backupDb', data }"
|
||||
v-if="actionBtns[perms.backupDb] && supportAction('backupDb', data.type)"
|
||||
>
|
||||
备份任务
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
:command="{ type: 'backupHistory', data }"
|
||||
v-if="actionBtns[perms.backupDb] && supportAction('backupDb', data.type)"
|
||||
>
|
||||
备份历史
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
:command="{ type: 'restoreDb', data }"
|
||||
v-if="actionBtns[perms.restoreDb] && supportAction('restoreDb', data.type)"
|
||||
>
|
||||
恢复任务
|
||||
</el-dropdown-item> -->
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
</page-table>
|
||||
</el-drawer>
|
||||
|
||||
<el-dialog width="750px" :title="`${exportDialog.db} 数据库导出`" v-model="exportDialog.visible">
|
||||
<el-row justify="space-between">
|
||||
<el-col :span="9">
|
||||
<el-form-item label="导出内容: ">
|
||||
@@ -168,54 +187,29 @@
|
||||
<db-restore-list :dbId="dbRestoreDialog.dbId" :dbNames="dbRestoreDialog.dbs" />
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-if="infoDialog.visible" v-model="infoDialog.visible" :before-close="onBeforeCloseInfoDialog">
|
||||
<el-descriptions title="详情" :column="3" border>
|
||||
<el-descriptions-item :span="2" label="名称">{{ infoDialog.data?.name }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="id">{{ infoDialog.data?.id }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="3" label="关联标签"><ResourceTags :tags="infoDialog.data.tags" /></el-descriptions-item>
|
||||
<el-descriptions-item :span="3" label="数据库实例名称">{{ infoDialog.instance?.name }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="2" label="主机">{{ infoDialog.instance?.host }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="端口">{{ infoDialog.instance?.port }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="2" label="授权凭证">{{ infoDialog.instance.authCertName }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="类型">
|
||||
<SvgIcon :name="getDbDialect(infoDialog.instance?.type).getInfo().icon" :size="20" />{{ infoDialog.instance?.type }}
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="3" label="数据库">{{ infoDialog.data?.database }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="3" label="备注">{{ infoDialog.data?.remark }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="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="2" label="更新时间">{{ formatDate(infoDialog.data?.updateTime) }} </el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="修改者">{{ infoDialog.data?.modifier }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
|
||||
<db-edit @val-change="search()" :title="dbEditDialog.title" v-model:visible="dbEditDialog.visible" v-model:db="dbEditDialog.data"></db-edit>
|
||||
<db-edit
|
||||
@confirm="confirmEditDb"
|
||||
@cancel="cancelEditDb"
|
||||
:title="dbEditDialog.title"
|
||||
v-model:visible="dbEditDialog.visible"
|
||||
:instance="props.instance"
|
||||
v-model:db="dbEditDialog.data"
|
||||
></db-edit>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
|
||||
import { computed, defineAsyncComponent, reactive, ref, Ref, toRefs } from 'vue';
|
||||
import { dbApi } from './api';
|
||||
import config from '@/common/config';
|
||||
import { joinClientParams } from '@/common/request';
|
||||
import { isTrue } from '@/common/assert';
|
||||
import { formatDate } from '@/common/utils/format';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { hasPerms } from '@/components/auth/auth';
|
||||
import DbSqlExecLog from './DbSqlExecLog.vue';
|
||||
import { DbType } from './dialect';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { getDbDialect } from './dialect/index';
|
||||
import { getTagPathSearchItem } from '../component/tag';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import DbBackupList from './DbBackupList.vue';
|
||||
import DbBackupHistoryList from './DbBackupHistoryList.vue';
|
||||
import DbRestoreList from './DbRestoreList.vue';
|
||||
@@ -223,44 +217,47 @@ import ResourceTags from '../component/ResourceTags.vue';
|
||||
import { sleep } from '@/common/utils/loading';
|
||||
import { DbGetDbNamesMode } from './enums';
|
||||
import { DbInst } from './db';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||
|
||||
const DbEdit = defineAsyncComponent(() => import('./DbEdit.vue'));
|
||||
|
||||
const searchItems = [
|
||||
getTagPathSearchItem(TagResourceTypeEnum.DbName.value),
|
||||
SearchItem.slot('instanceId', '实例', 'instanceSelect'),
|
||||
SearchItem.input('code', '编号'),
|
||||
];
|
||||
const props = defineProps({
|
||||
instance: {
|
||||
type: [Object],
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
|
||||
const dialogVisible = defineModel<boolean>('visible');
|
||||
|
||||
const emit = defineEmits(['cancel']);
|
||||
|
||||
const columns = ref([
|
||||
TableColumn.new('tags[0].tagPath', '关联标签').isSlot('tagPath').setAddWidth(20),
|
||||
TableColumn.new('name', '名称'),
|
||||
TableColumn.new('type', '类型').isSlot().setAddWidth(-15).alignCenter(),
|
||||
TableColumn.new('instanceName', '实例名'),
|
||||
TableColumn.new('host', 'ip:port').isSlot().setAddWidth(40),
|
||||
TableColumn.new('authCertName', '授权凭证'),
|
||||
TableColumn.new('getDatabaseMode', '获库方式').typeTag(DbGetDbNamesMode),
|
||||
TableColumn.new('database', '库').isSlot().setMinWidth(80),
|
||||
TableColumn.new('remark', '备注'),
|
||||
TableColumn.new('code', '编号'),
|
||||
TableColumn.new('action', '操作').isSlot().setMinWidth(210).fixedRight().alignCenter(),
|
||||
]);
|
||||
|
||||
const perms = {
|
||||
base: 'db',
|
||||
saveDb: 'db:save',
|
||||
delDb: 'db:del',
|
||||
backupDb: 'db:backup',
|
||||
restoreDb: 'db:restore',
|
||||
};
|
||||
|
||||
// 该用户拥有的的操作列按钮权限
|
||||
// const actionBtns = hasPerms([perms.base, perms.saveDb]);
|
||||
const actionBtns = hasPerms(Object.values(perms));
|
||||
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(180).fixedRight().alignCenter();
|
||||
const actionBtns: any = hasPerms(Object.values(perms));
|
||||
|
||||
const route = useRoute();
|
||||
const pageTableRef: Ref<any> = ref(null);
|
||||
const state = reactive({
|
||||
row: {} as any,
|
||||
dbId: 0,
|
||||
db: '',
|
||||
loadingDbNames: false,
|
||||
currentDbNames: [],
|
||||
dbNameSearch: '',
|
||||
@@ -268,29 +265,20 @@ const state = reactive({
|
||||
/**
|
||||
* 选中的数据
|
||||
*/
|
||||
selectionData: [],
|
||||
selectionData: [] as any,
|
||||
/**
|
||||
* 查询条件
|
||||
*/
|
||||
query: {
|
||||
tagPath: '',
|
||||
instanceId: null,
|
||||
instanceId: 0,
|
||||
pageNum: 1,
|
||||
pageSize: 0,
|
||||
},
|
||||
infoDialog: {
|
||||
visible: false,
|
||||
data: null as any,
|
||||
instance: null as any,
|
||||
query: {
|
||||
instanceId: 0,
|
||||
},
|
||||
},
|
||||
// sql执行记录弹框
|
||||
sqlExecLogDialog: {
|
||||
title: '',
|
||||
visible: false,
|
||||
dbs: [],
|
||||
dbs: [] as any,
|
||||
dbId: 0,
|
||||
},
|
||||
// 数据库备份弹框
|
||||
@@ -321,6 +309,7 @@ const state = reactive({
|
||||
exportDialog: {
|
||||
visible: false,
|
||||
dbId: 0,
|
||||
db: '',
|
||||
type: 3,
|
||||
data: [] as any,
|
||||
value: [],
|
||||
@@ -339,14 +328,12 @@ const state = reactive({
|
||||
},
|
||||
});
|
||||
|
||||
const { db, query, infoDialog, sqlExecLogDialog, exportDialog, dbEditDialog, dbBackupDialog, dbBackupHistoryDialog, dbRestoreDialog } = toRefs(state);
|
||||
const { query, sqlExecLogDialog, exportDialog, dbEditDialog, dbBackupDialog, dbBackupHistoryDialog, dbRestoreDialog } = toRefs(state);
|
||||
|
||||
onMounted(async () => {
|
||||
if (Object.keys(actionBtns).length > 0) {
|
||||
columns.value.push(actionColumn);
|
||||
}
|
||||
search();
|
||||
});
|
||||
const search = async () => {
|
||||
state.query.instanceId = props.instance?.id;
|
||||
pageTableRef.value.search();
|
||||
};
|
||||
|
||||
const getDbNames = async (db: any) => {
|
||||
try {
|
||||
@@ -372,42 +359,46 @@ const filterDbs = computed(() => {
|
||||
});
|
||||
});
|
||||
|
||||
const checkRouteTagPath = (query: any) => {
|
||||
if (route.query.tagPath) {
|
||||
query.tagPath = route.query.tagPath as string;
|
||||
}
|
||||
return query;
|
||||
};
|
||||
|
||||
const search = async (tagPath: string = '') => {
|
||||
if (tagPath) {
|
||||
state.query.tagPath = tagPath;
|
||||
}
|
||||
pageTableRef.value.search();
|
||||
};
|
||||
|
||||
const showInfo = async (info: any) => {
|
||||
state.infoDialog.data = info;
|
||||
state.infoDialog.query.instanceId = info.instanceId;
|
||||
const res = await dbApi.getInstance.request(state.infoDialog.query);
|
||||
state.infoDialog.instance = res;
|
||||
state.infoDialog.visible = true;
|
||||
};
|
||||
|
||||
const onBeforeCloseInfoDialog = () => {
|
||||
state.infoDialog.visible = false;
|
||||
state.infoDialog.data = null;
|
||||
state.infoDialog.instance = null;
|
||||
};
|
||||
|
||||
const getInstances = async (instanceName = '') => {
|
||||
if (!instanceName) {
|
||||
state.instances = [];
|
||||
return;
|
||||
}
|
||||
const data = await dbApi.instances.request({ name: instanceName });
|
||||
const editDb = (data: any) => {
|
||||
if (data) {
|
||||
state.instances = data.list;
|
||||
state.dbEditDialog.data = { ...data };
|
||||
} else {
|
||||
state.dbEditDialog.data = {
|
||||
instanceId: props.instance.id,
|
||||
};
|
||||
}
|
||||
state.dbEditDialog.title = data ? '编辑数据库' : '新增数据库';
|
||||
state.dbEditDialog.visible = true;
|
||||
};
|
||||
|
||||
const confirmEditDb = async (db: any) => {
|
||||
db.instanceId = props.instance.id;
|
||||
await dbApi.saveDb.request(db);
|
||||
ElMessage.success('保存成功');
|
||||
search();
|
||||
cancelEditDb();
|
||||
};
|
||||
|
||||
const cancelEditDb = () => {
|
||||
state.dbEditDialog.visible = false;
|
||||
state.dbEditDialog.data = {};
|
||||
};
|
||||
|
||||
const deleteDb = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除【${state.selectionData.map((x: any) => x.name).join(', ')}】库?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
});
|
||||
for (let db of state.selectionData) {
|
||||
await dbApi.deleteDb.request({ id: db.id });
|
||||
}
|
||||
ElMessage.success('删除成功');
|
||||
} catch (err) {
|
||||
//
|
||||
} finally {
|
||||
search();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -415,10 +406,6 @@ const handleMoreActionCommand = (commond: any) => {
|
||||
const data = commond.data;
|
||||
const type = commond.type;
|
||||
switch (type) {
|
||||
case 'detail': {
|
||||
showInfo(data);
|
||||
return;
|
||||
}
|
||||
case 'dumpDb': {
|
||||
onDumpDbs(data);
|
||||
return;
|
||||
@@ -441,7 +428,9 @@ const handleMoreActionCommand = (commond: any) => {
|
||||
const onShowSqlExec = async (row: any) => {
|
||||
state.sqlExecLogDialog.title = `${row.name}`;
|
||||
state.sqlExecLogDialog.dbId = row.id;
|
||||
state.sqlExecLogDialog.dbs = row.database.split(' ');
|
||||
DbInst.getDbNames(row).then((res) => {
|
||||
state.sqlExecLogDialog.dbs = res;
|
||||
});
|
||||
state.sqlExecLogDialog.visible = true;
|
||||
};
|
||||
|
||||
@@ -454,26 +443,32 @@ const onBeforeCloseSqlExecDialog = () => {
|
||||
const onShowDbBackupDialog = async (row: any) => {
|
||||
state.dbBackupDialog.title = `${row.name}`;
|
||||
state.dbBackupDialog.dbId = row.id;
|
||||
state.dbBackupDialog.dbs = row.database.split(' ');
|
||||
DbInst.getDbNames(row).then((res) => {
|
||||
state.sqlExecLogDialog.dbs = res;
|
||||
});
|
||||
state.dbBackupDialog.visible = true;
|
||||
};
|
||||
|
||||
const onShowDbBackupHistoryDialog = async (row: any) => {
|
||||
state.dbBackupHistoryDialog.title = `${row.name}`;
|
||||
state.dbBackupHistoryDialog.dbId = row.id;
|
||||
state.dbBackupHistoryDialog.dbs = row.database.split(' ');
|
||||
DbInst.getDbNames(row).then((res) => {
|
||||
state.sqlExecLogDialog.dbs = res;
|
||||
});
|
||||
state.dbBackupHistoryDialog.visible = true;
|
||||
};
|
||||
|
||||
const onShowDbRestoreDialog = async (row: any) => {
|
||||
state.dbRestoreDialog.title = `${row.name}`;
|
||||
state.dbRestoreDialog.dbId = row.id;
|
||||
state.dbRestoreDialog.dbs = row.database.split(' ');
|
||||
DbInst.getDbNames(row).then((res) => {
|
||||
state.sqlExecLogDialog.dbs = res;
|
||||
});
|
||||
state.dbRestoreDialog.visible = true;
|
||||
};
|
||||
|
||||
const onDumpDbs = async (row: any) => {
|
||||
const dbs = row.database.split(' ');
|
||||
const dbs = await DbInst.getDbNames(row);
|
||||
const data = [];
|
||||
for (let name of dbs) {
|
||||
data.push({
|
||||
@@ -481,6 +476,7 @@ const onDumpDbs = async (row: any) => {
|
||||
label: name,
|
||||
});
|
||||
}
|
||||
state.exportDialog.db = row.name;
|
||||
state.exportDialog.value = [];
|
||||
state.exportDialog.data = data;
|
||||
state.exportDialog.dbId = row.id;
|
||||
@@ -524,7 +520,10 @@ const supportAction = (action: string, dbType: string): boolean => {
|
||||
return actions.includes(action);
|
||||
};
|
||||
|
||||
defineExpose({ search });
|
||||
const cancel = () => {
|
||||
dialogVisible.value = false;
|
||||
emit('cancel');
|
||||
};
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.db-list {
|
||||
|
||||
@@ -1,81 +1,137 @@
|
||||
<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-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-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-divider content-position="left">基本信息</el-divider>
|
||||
|
||||
<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="taskName" label="任务名" required>
|
||||
<el-input v-model.trim="form.taskName" placeholder="请输入任务名" auto-complete="off" />
|
||||
</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>
|
||||
<el-row class="w100">
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="status" label="启用状态">
|
||||
<el-switch v-model="form.status" inline-prompt active-text="启用" inactive-text="禁用" :active-value="1" :inactive-value="-1" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<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-col :span="12">
|
||||
<el-form-item prop="cronAble" label="定时迁移" required>
|
||||
<el-radio-group v-model="form.cronAble">
|
||||
<el-radio label="是" :value="1" />
|
||||
<el-radio label="否" :value="-1" />
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="cron" label="cron" :required="form.cronAble == 1">
|
||||
<CrontabInput v-model="form.cron" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="srcDbId" label="源数据库" class="w100" 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="mode" label="迁移方式" required>
|
||||
<el-radio-group v-model="form.mode">
|
||||
<el-radio label="迁移到数据库" :value="1" />
|
||||
<el-radio label="迁移到文件(自动命名)" :value="2" />
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="form.mode === 2">
|
||||
<el-row class="w100">
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="targetFileDbType" label="文件数据库类型" :required="form.mode === 2">
|
||||
<el-select v-model="form.targetFileDbType" placeholder="数据库类型" clearable filterable>
|
||||
<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>
|
||||
<SvgIcon :name="getDbDialect(form.targetFileDbType!).getInfo().icon" :size="20" />
|
||||
</template>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item label="文件保留天数">
|
||||
<el-input-number v-model="form.fileSaveDays" :min="-1" :max="1000">
|
||||
<template #suffix>
|
||||
<span>天</span>
|
||||
</template>
|
||||
</el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="strategy" label="迁移策略" required>
|
||||
<el-radio-group v-model="form.strategy">
|
||||
<el-radio label="全量" :value="1" />
|
||||
<el-radio label="增量(暂不可用)" :value="2" disabled />
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="form.mode == 1" prop="targetDbId" label="目标数据库" class="w100" :required="form.mode === 1">
|
||||
<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="nameCase" label="转换表、字段名" required>
|
||||
<el-radio-group v-model="form.nameCase">
|
||||
<el-radio label="无" :value="1" />
|
||||
<el-radio label="大写" :value="2" />
|
||||
<el-radio label="小写" :value="3" />
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">数据库对象</el-divider>
|
||||
<el-form-item>
|
||||
<el-input v-model="state.filterSrcTableText" placeholder="过滤表" size="small" />
|
||||
</el-form-item>
|
||||
<el-form-item class="w100">
|
||||
<el-tree
|
||||
ref="srcTreeRef"
|
||||
class="w100"
|
||||
style="max-height: 200px; 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-form>
|
||||
|
||||
<template #footer>
|
||||
@@ -84,15 +140,20 @@
|
||||
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk">确 定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, nextTick, reactive, ref, toRefs, watch } from 'vue';
|
||||
import { 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';
|
||||
import CrontabInput from '@/components/crontab/CrontabInput.vue';
|
||||
import { getDbDialect, getDbDialectMap } from '@/views/ops/db/dialect';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
import _ from 'lodash';
|
||||
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
@@ -108,15 +169,56 @@ const emit = defineEmits(['update:visible', 'cancel', 'val-change']);
|
||||
|
||||
const dialogVisible = defineModel<boolean>('visible', { default: false });
|
||||
|
||||
const rules = {};
|
||||
const rules = {
|
||||
taskName: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入任务名',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
srcDbId: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择源库',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
targetDbId: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择目标库',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
targetFileDbType: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择目标文件语言类型',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
cron: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择cron表达式',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const dbForm: any = ref(null);
|
||||
|
||||
const basicTab = 'basic';
|
||||
const tableTab = 'table';
|
||||
|
||||
type FormData = {
|
||||
id?: number;
|
||||
taskName: string;
|
||||
status: number;
|
||||
cronAble: 1 | -1;
|
||||
cron: string;
|
||||
mode: 1 | 2;
|
||||
targetFileDbType?: string;
|
||||
fileSaveDays?: number;
|
||||
dbType: 1 | 2;
|
||||
srcDbId?: number;
|
||||
srcDbName?: string;
|
||||
srcDbType?: string;
|
||||
@@ -136,6 +238,9 @@ type FormData = {
|
||||
};
|
||||
|
||||
const basicFormData = {
|
||||
mode: 1,
|
||||
status: 1,
|
||||
cronAble: -1,
|
||||
strategy: 1,
|
||||
nameCase: 1,
|
||||
deleteTable: 1,
|
||||
@@ -149,7 +254,6 @@ const srcTableListDisabled = ref(false);
|
||||
const defaultKeys = ['tab-check', 'all', 'table-list'];
|
||||
|
||||
const state = reactive({
|
||||
tabActiveName: 'basic',
|
||||
form: basicFormData,
|
||||
submitForm: {} as any,
|
||||
srcTableFields: [] as string[],
|
||||
@@ -172,20 +276,14 @@ const state = reactive({
|
||||
],
|
||||
});
|
||||
|
||||
const { tabActiveName, form, submitForm } = toRefs(state);
|
||||
const { 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;
|
||||
@@ -196,8 +294,7 @@ watch(dialogVisible, async (newValue: boolean) => {
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
state.form = props.data as FormData;
|
||||
state.form = _.cloneDeep(props.data) as FormData;
|
||||
let { srcDbId, targetDbId } = state.form;
|
||||
|
||||
// 初始化src数据源
|
||||
@@ -224,6 +321,10 @@ watch(dialogVisible, async (newValue: boolean) => {
|
||||
|
||||
// 初始化勾选迁移表
|
||||
srcTreeRef.value.setCheckedKeys(state.form.checkedKeys.split(','));
|
||||
|
||||
// 初始化默认值
|
||||
state.form.cronAble = state.form.cronAble || 0;
|
||||
state.form.mode = state.form.mode || 1;
|
||||
});
|
||||
|
||||
watch(
|
||||
@@ -323,10 +424,4 @@ const cancel = () => {
|
||||
emit('cancel');
|
||||
};
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.db-transfer-edit {
|
||||
.el-select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="scss"></style>
|
||||
|
||||
247
mayfly_go_web/src/views/ops/db/DbTransferFile.vue
Normal file
247
mayfly_go_web/src/views/ops/db/DbTransferFile.vue
Normal file
@@ -0,0 +1,247 @@
|
||||
<template>
|
||||
<div class="db-transfer-file">
|
||||
<el-dialog :title="title" v-model="dialogVisible" :close-on-click-modal="false" :destroy-on-close="true" width="1000px">
|
||||
<page-table
|
||||
ref="pageTableRef"
|
||||
:data="state.tableData"
|
||||
v-model:query-form="state.query"
|
||||
:show-selection="true"
|
||||
v-model:selection-data="state.selectionData"
|
||||
:columns="columns"
|
||||
@page-num-change="
|
||||
(args) => {
|
||||
state.query.pageNum = args.pageNum;
|
||||
search();
|
||||
}
|
||||
"
|
||||
@page-size-change="
|
||||
(args) => {
|
||||
state.query.pageSize = args.pageNum;
|
||||
search();
|
||||
}
|
||||
"
|
||||
>
|
||||
<template #tableHeader>
|
||||
<el-button v-auth="perms.del" :disabled="state.selectionData.length < 1" @click="del()" type="danger" icon="delete">删除</el-button>
|
||||
</template>
|
||||
|
||||
<template #fileKey="{ data }">
|
||||
<FileInfo :fileKey="data.fileKey" :canDownload="actionBtns[perms.down] && data.status === 2" />
|
||||
</template>
|
||||
|
||||
<template #fileDbType="{ data }">
|
||||
<span>
|
||||
<SvgIcon :name="getDbDialect(data.fileDbType).getInfo().icon" :size="18" />
|
||||
{{ data.fileDbType }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
<el-button v-if="actionBtns[perms.run] && data.status === DbTransferFileStatusEnum.Success.value" @click="openRun(data)" type="primary" link
|
||||
>执行</el-button
|
||||
>
|
||||
<el-button v-if="data.logId" @click="openLog(data)" type="success" link>日志</el-button>
|
||||
</template>
|
||||
</page-table>
|
||||
<TerminalLog v-model:log-id="state.logsDialog.logId" v-model:visible="state.logsDialog.visible" :title="state.logsDialog.title" />
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog :title="state.runDialog.title" v-model="state.runDialog.visible" :destroy-on-close="true" width="600px">
|
||||
<el-form :model="state.runDialog.runForm" ref="runFormRef" label-width="auto" :rules="state.runDialog.formRules">
|
||||
<el-form-item label="文件数据库类型" prop="dbType">
|
||||
<SvgIcon :name="getDbDialect(state.runDialog.runForm.dbType).getInfo().icon" :size="18" /> {{ state.runDialog.runForm.dbType }}
|
||||
</el-form-item>
|
||||
<el-form-item label="选择目标数据库" prop="targetDbId" required>
|
||||
<db-select-tree
|
||||
placeholder="请选择目标数据库"
|
||||
v-model:db-id="state.runDialog.runForm.targetDbId"
|
||||
v-model:inst-name="state.runDialog.runForm.targetInstName"
|
||||
v-model:db-name="state.runDialog.runForm.targetDbName"
|
||||
v-model:tag-path="state.runDialog.runForm.targetTagPath"
|
||||
v-model:db-type="state.runDialog.runForm.targetDbType"
|
||||
@select-db="state.runDialog.onSelectRunTargetDb"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div>
|
||||
<el-button @click="state.runDialog.cancel()">取 消</el-button>
|
||||
<el-button type="primary" :loading="state.runDialog.loading" @click="state.runDialog.btnOk">确 定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, reactive, Ref, ref, watch } from 'vue';
|
||||
import { dbApi } from '@/views/ops/db/api';
|
||||
import { getDbDialect } from '@/views/ops/db/dialect';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { hasPerms } from '@/components/auth/auth';
|
||||
import TerminalLog from '@/components/terminal/TerminalLog.vue';
|
||||
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
|
||||
import { getClientId } from '@/common/utils/storage';
|
||||
import FileInfo from '@/components/file/FileInfo.vue';
|
||||
import { DbTransferFileStatusEnum } from './enums';
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: [Object],
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
|
||||
const dialogVisible = defineModel<boolean>('visible', { default: false });
|
||||
|
||||
const columns = ref([
|
||||
TableColumn.new('fileKey', '文件').setMinWidth(280).isSlot(),
|
||||
TableColumn.new('createTime', '执行时间').setMinWidth(180).isTime(),
|
||||
TableColumn.new('fileDbType', 'sql语言').setMinWidth(90).isSlot(),
|
||||
TableColumn.new('status', '状态').typeTag(DbTransferFileStatusEnum),
|
||||
]);
|
||||
|
||||
const perms = {
|
||||
del: 'db:transfer:files:del',
|
||||
down: 'db:transfer:files:down',
|
||||
run: 'db:transfer:files:run',
|
||||
};
|
||||
|
||||
const actionBtns = hasPerms([perms.del, perms.down, perms.run]);
|
||||
|
||||
const actionWidth = ((actionBtns[perms.run] ? 1 : 0) + 1) * 55;
|
||||
|
||||
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(actionWidth).fixedRight().alignCenter();
|
||||
|
||||
onMounted(async () => {
|
||||
if (Object.keys(actionBtns).length > 0) {
|
||||
columns.value.push(actionColumn);
|
||||
}
|
||||
});
|
||||
|
||||
const runFormRef: any = ref(null);
|
||||
|
||||
const state = reactive({
|
||||
query: {
|
||||
taskId: props.data?.id,
|
||||
name: null,
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
},
|
||||
logsDialog: {
|
||||
logId: 0,
|
||||
title: '数据库迁移日志',
|
||||
visible: false,
|
||||
data: null as any,
|
||||
running: false,
|
||||
},
|
||||
runDialog: {
|
||||
title: '指定数据库执行sql文件',
|
||||
visible: false,
|
||||
data: null as any,
|
||||
formRules: {
|
||||
targetDbId: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择目标数据库',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
},
|
||||
runForm: {
|
||||
id: 0,
|
||||
dbType: '',
|
||||
clientId: '',
|
||||
targetDbId: 0,
|
||||
targetDbName: '',
|
||||
targetTagPath: '',
|
||||
targetInstName: '',
|
||||
targetDbType: '',
|
||||
},
|
||||
loading: false,
|
||||
cancel: function () {
|
||||
state.runDialog.visible = false;
|
||||
state.runDialog.runForm = {} as any;
|
||||
},
|
||||
btnOk: function () {
|
||||
runFormRef.value.validate(async (valid: boolean) => {
|
||||
if (!valid) {
|
||||
ElMessage.error('请正确填写信息');
|
||||
return false;
|
||||
}
|
||||
console.log(state.runDialog.runForm);
|
||||
if (state.runDialog.runForm.targetDbType !== state.runDialog.runForm.dbType) {
|
||||
ElMessage.warning(`请选择[${state.runDialog.runForm.dbType}]数据库`);
|
||||
return false;
|
||||
}
|
||||
state.runDialog.runForm.clientId = getClientId();
|
||||
await dbApi.dbTransferFileRun.request(state.runDialog.runForm);
|
||||
ElMessage.success('保存成功');
|
||||
state.runDialog.cancel();
|
||||
await search();
|
||||
});
|
||||
},
|
||||
onSelectRunTargetDb: function (param: any) {
|
||||
if (param.type !== state.runDialog.runForm.dbType) {
|
||||
ElMessage.warning(`请选择[${state.runDialog.runForm.dbType}]数据库`);
|
||||
}
|
||||
},
|
||||
},
|
||||
selectionData: [], // 选中的数据
|
||||
tableData: [],
|
||||
});
|
||||
|
||||
const search = async () => {
|
||||
const { total, list } = await dbApi.dbTransferFileList.request(state.query);
|
||||
state.tableData = list;
|
||||
pageTableRef.value.total = total;
|
||||
};
|
||||
|
||||
const pageTableRef: Ref<any> = ref(null);
|
||||
|
||||
const del = async function () {
|
||||
try {
|
||||
await ElMessageBox.confirm(`将会删除sql文件,确定删除?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
});
|
||||
await dbApi.dbTransferFileDel.request({ fileId: state.selectionData.map((x: any) => x.id).join(',') });
|
||||
ElMessage.success('删除成功');
|
||||
await search();
|
||||
} catch (err) {
|
||||
//
|
||||
}
|
||||
};
|
||||
|
||||
const openLog = function (data: any) {
|
||||
state.logsDialog.logId = data.logId;
|
||||
state.logsDialog.visible = true;
|
||||
state.logsDialog.title = '数据库迁移日志';
|
||||
state.logsDialog.running = data.state === 1;
|
||||
};
|
||||
|
||||
// 运行sql,弹出选择需要运行的库,默认运行当前数据库,需要保证数据库类型与sql文件一致
|
||||
const openRun = function (data: any) {
|
||||
console.log(data);
|
||||
state.runDialog.runForm = { id: data.id, dbType: data.fileDbType } as any;
|
||||
console.log(state.runDialog.runForm);
|
||||
state.runDialog.visible = true;
|
||||
};
|
||||
|
||||
watch(dialogVisible, async (newValue: boolean) => {
|
||||
if (!newValue) {
|
||||
return;
|
||||
}
|
||||
state.query.taskId = props.data?.id;
|
||||
state.query.pageNum = 1;
|
||||
state.query.pageSize = 10;
|
||||
|
||||
await search();
|
||||
});
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
@@ -14,11 +14,16 @@
|
||||
<el-button v-auth="perms.del" :disabled="selectionData.length < 1" @click="del()" type="danger" icon="delete">删除</el-button>
|
||||
</template>
|
||||
|
||||
<template #taskName="{ data }">
|
||||
<span :style="`${data.taskName ? '' : 'color:red'}`">
|
||||
{{ data.taskName || '请设置' }}
|
||||
</span>
|
||||
</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 }}
|
||||
{{ data.srcDbName }}
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
@@ -26,21 +31,43 @@
|
||||
<el-tooltip :content="`${data.targetTagPath} > ${data.targetInstName} > ${data.targetDbName}`">
|
||||
<span>
|
||||
<SvgIcon :name="getDbDialect(data.targetDbType).getInfo().icon" :size="18" />
|
||||
{{ data.targetInstName }}
|
||||
{{ data.targetDbName }}
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
|
||||
<template #status="{ data }">
|
||||
<span v-if="actionBtns[perms.status]">
|
||||
<el-switch
|
||||
v-model="data.status"
|
||||
@click="updStatus(data.id, data.status)"
|
||||
inline-prompt
|
||||
active-text="启用"
|
||||
inactive-text="禁用"
|
||||
:active-value="1"
|
||||
:inactive-value="-1"
|
||||
/>
|
||||
</span>
|
||||
<span v-else>
|
||||
<el-tag v-if="data.status == 1" class="ml-2" type="success">启用</el-tag>
|
||||
<el-tag v-else class="ml-2" type="danger">禁用</el-tag>
|
||||
</span>
|
||||
</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="actionBtns[perms.log]" type="warning" 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>
|
||||
<el-button v-if="actionBtns[perms.run] && data.runningState !== 1 && data.status === 1" type="success" link @click="reRun(data)"
|
||||
>运行</el-button
|
||||
>
|
||||
<el-button v-if="actionBtns[perms.files] && data.mode === 2" type="success" link @click="openFiles(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" />
|
||||
<db-transfer-file :title="filesDialog.title" v-model:visible="filesDialog.visible" v-model:data="filesDialog.data" />
|
||||
|
||||
<TerminalLog v-model:log-id="logsDialog.logId" v-model:visible="logsDialog.visible" :title="logsDialog.title" />
|
||||
</div>
|
||||
@@ -57,6 +84,7 @@ import { SearchItem } from '@/components/SearchForm';
|
||||
import { getDbDialect } from '@/views/ops/db/dialect';
|
||||
import { DbTransferRunningStateEnum } from './enums';
|
||||
import TerminalLog from '@/components/terminal/TerminalLog.vue';
|
||||
import DbTransferFile from './DbTransferFile.vue';
|
||||
|
||||
const DbTransferEdit = defineAsyncComponent(() => import('./DbTransferEdit.vue'));
|
||||
|
||||
@@ -66,23 +94,25 @@ const perms = {
|
||||
status: 'db:transfer:status',
|
||||
log: 'db:transfer:log',
|
||||
run: 'db:transfer:run',
|
||||
files: 'db:transfer:files',
|
||||
};
|
||||
|
||||
const searchItems = [SearchItem.input('name', '名称')];
|
||||
|
||||
const columns = ref([
|
||||
TableColumn.new('srcDb', '源库').setMinWidth(200).isSlot(),
|
||||
TableColumn.new('targetDb', '目标库').setMinWidth(200).isSlot(),
|
||||
TableColumn.new('taskName', '任务名').setMinWidth(150).isSlot(),
|
||||
TableColumn.new('srcDb', '源库').setMinWidth(150).isSlot(),
|
||||
// TableColumn.new('targetDb', '目标库').setMinWidth(150).isSlot(),
|
||||
TableColumn.new('runningState', '执行状态').typeTag(DbTransferRunningStateEnum),
|
||||
TableColumn.new('creator', '创建人'),
|
||||
TableColumn.new('createTime', '创建时间').isTime(),
|
||||
TableColumn.new('status', '状态').isSlot(),
|
||||
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 actionBtns = hasPerms([perms.save, perms.del, perms.status, perms.log, perms.run, perms.files]);
|
||||
const actionWidth =
|
||||
((actionBtns[perms.save] ? 1 : 0) + (actionBtns[perms.log] ? 1 : 0) + (actionBtns[perms.run] ? 1 : 0) + (actionBtns[perms.files] ? 1 : 0)) * 55;
|
||||
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(actionWidth).fixedRight().alignCenter();
|
||||
const pageTableRef: Ref<any> = ref(null);
|
||||
|
||||
@@ -114,9 +144,15 @@ const state = reactive({
|
||||
data: null as any,
|
||||
running: false,
|
||||
},
|
||||
filesDialog: {
|
||||
taskId: 0,
|
||||
title: '迁移文件列表',
|
||||
visible: false,
|
||||
data: null as any,
|
||||
},
|
||||
});
|
||||
|
||||
const { selectionData, query, editDialog, logsDialog } = toRefs(state);
|
||||
const { selectionData, query, editDialog, logsDialog, filesDialog } = toRefs(state);
|
||||
|
||||
onMounted(async () => {
|
||||
if (Object.keys(actionBtns).length > 0) {
|
||||
@@ -131,10 +167,10 @@ const search = () => {
|
||||
const edit = async (data: any) => {
|
||||
if (!data) {
|
||||
state.editDialog.data = null;
|
||||
state.editDialog.title = '新增数据库迁移任务';
|
||||
state.editDialog.title = '新增数据库迁移任务(迁移不会对源库造成修改)';
|
||||
} else {
|
||||
state.editDialog.data = data;
|
||||
state.editDialog.title = '修改数据库迁移任务';
|
||||
state.editDialog.title = '修改数据库迁移任务(迁移不会对源库造成修改)';
|
||||
}
|
||||
state.editDialog.visible = true;
|
||||
};
|
||||
@@ -178,6 +214,22 @@ const reRun = async (data: any) => {
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
const openFiles = async (data: any) => {
|
||||
state.filesDialog.visible = true;
|
||||
state.filesDialog.title = '迁移文件管理';
|
||||
state.filesDialog.taskId = data.id;
|
||||
state.filesDialog.data = data;
|
||||
};
|
||||
const updStatus = async (id: any, status: 1 | -1) => {
|
||||
try {
|
||||
await dbApi.updateDbTransferTaskStatus.request({ taskId: id, status });
|
||||
ElMessage.success(`${status === 1 ? '启用' : '禁用'}成功`);
|
||||
search();
|
||||
} catch (err) {
|
||||
//
|
||||
}
|
||||
};
|
||||
|
||||
const del = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除任务?`, '提示', {
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
<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>
|
||||
@@ -22,15 +22,6 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="code" label="编号" required>
|
||||
<el-input
|
||||
:disabled="form.id"
|
||||
v-model.trim="form.code"
|
||||
placeholder="请输入编号 (大小写字母、数字、_-.:), 不可修改"
|
||||
auto-complete="off"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="name" label="名称" required>
|
||||
<el-input v-model.trim="form.name" placeholder="请输入数据库别名" auto-complete="off"></el-input>
|
||||
</el-form-item>
|
||||
@@ -132,7 +123,6 @@ import SshTunnelSelect from '../component/SshTunnelSelect.vue';
|
||||
import { DbType, getDbDialect, getDbDialectMap } from './dialect';
|
||||
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';
|
||||
@@ -161,18 +151,6 @@ const rules = {
|
||||
trigger: ['change'],
|
||||
},
|
||||
],
|
||||
code: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入编码',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
{
|
||||
pattern: ResourceCodePattern.pattern,
|
||||
message: ResourceCodePattern.message,
|
||||
trigger: ['blur'],
|
||||
},
|
||||
],
|
||||
name: [
|
||||
{
|
||||
required: true,
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
<template #action="{ data }">
|
||||
<el-button @click="showInfo(data)" link>详情</el-button>
|
||||
<el-button v-if="actionBtns[perms.saveInstance]" @click="editInstance(data)" type="primary" link>编辑</el-button>
|
||||
<el-button v-if="actionBtns[perms.saveDb]" @click="editDb(data)" type="primary" link>库配置</el-button>
|
||||
<el-button v-if="actionBtns[perms.saveDb]" @click="editDb(data)" type="primary" link>库管理</el-button>
|
||||
</template>
|
||||
</page-table>
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
v-model:data="instanceEditDialog.data"
|
||||
></instance-edit>
|
||||
|
||||
<instance-db-conf :title="dbEditDialog.title" v-model:visible="dbEditDialog.visible" :instance="dbEditDialog.instance" />
|
||||
<DbList :title="dbEditDialog.title" v-model:visible="dbEditDialog.visible" :instance="dbEditDialog.instance" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -89,7 +89,7 @@ import { getTagPathSearchItem } from '../component/tag';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
|
||||
const InstanceEdit = defineAsyncComponent(() => import('./InstanceEdit.vue'));
|
||||
const InstanceDbConf = defineAsyncComponent(() => import('./InstanceDbConf.vue'));
|
||||
const DbList = defineAsyncComponent(() => import('./DbList.vue'));
|
||||
|
||||
const props = defineProps({
|
||||
lazy: {
|
||||
@@ -104,7 +104,7 @@ const perms = {
|
||||
saveDb: 'db:save',
|
||||
};
|
||||
|
||||
const searchItems = [getTagPathSearchItem(TagResourceTypeEnum.Db.value), SearchItem.input('code', '编号'), SearchItem.input('name', '名称')];
|
||||
const searchItems = [SearchItem.input('keyword', '关键字').withPlaceholder('host / 名称 / 编号'), getTagPathSearchItem(TagResourceTypeEnum.Db.value)];
|
||||
|
||||
const columns = ref([
|
||||
TableColumn.new('tags[0].tagPath', '关联标签').isSlot('tagPath').setAddWidth(20),
|
||||
@@ -118,7 +118,7 @@ const columns = ref([
|
||||
]);
|
||||
|
||||
// 该用户拥有的的操作列按钮权限
|
||||
const actionBtns = hasPerms(Object.values(perms));
|
||||
const actionBtns: any = hasPerms(Object.values(perms));
|
||||
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(180).fixedRight().alignCenter();
|
||||
const pageTableRef: Ref<any> = ref(null);
|
||||
|
||||
@@ -215,7 +215,7 @@ const deleteInstance = async () => {
|
||||
|
||||
const editDb = (data: any) => {
|
||||
state.dbEditDialog.instance = data;
|
||||
state.dbEditDialog.title = `配置 "${data.name}" 数据库`;
|
||||
state.dbEditDialog.title = `管理 "${data.name}" 数据库`;
|
||||
state.dbEditDialog.visible = true;
|
||||
};
|
||||
|
||||
|
||||
@@ -58,16 +58,71 @@
|
||||
<el-row>
|
||||
<el-col :span="24" v-if="state.db">
|
||||
<el-descriptions :column="4" size="small" border>
|
||||
<el-descriptions-item label-align="right" label="操作"
|
||||
><el-button
|
||||
<el-descriptions-item label-align="right" label="操作">
|
||||
<el-button
|
||||
:disabled="!state.db || !nowDbInst.id"
|
||||
type="primary"
|
||||
icon="Search"
|
||||
@click="addQueryTab({ id: nowDbInst.id, dbs: nowDbInst.databases }, state.db)"
|
||||
size="small"
|
||||
>新建查询</el-button
|
||||
></el-descriptions-item
|
||||
>
|
||||
link
|
||||
@click="
|
||||
addQueryTab(
|
||||
{ id: nowDbInst.id, dbs: nowDbInst.databases, nodeKey: getSqlMenuNodeKey(nowDbInst.id, state.db) },
|
||||
state.db
|
||||
)
|
||||
"
|
||||
title="新建查询"
|
||||
>
|
||||
</el-button>
|
||||
|
||||
<template v-if="!dbConfig.locationTreeNode">
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
<el-button @click="locationNowTreeNode(null)" title="定位至左侧树的指定位置" icon="Location" link></el-button>
|
||||
</template>
|
||||
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
<!-- 数据库展示配置 -->
|
||||
<el-popover
|
||||
popper-style="max-height: 550px; overflow: auto; max-width: 450px"
|
||||
placement="bottom"
|
||||
width="auto"
|
||||
title="数据库展示配置"
|
||||
trigger="click"
|
||||
>
|
||||
<el-row>
|
||||
<el-checkbox
|
||||
v-model="dbConfig.showColumnComment"
|
||||
label="显示字段备注"
|
||||
:true-value="1"
|
||||
:false-value="0"
|
||||
size="small"
|
||||
/>
|
||||
</el-row>
|
||||
|
||||
<el-row>
|
||||
<el-checkbox
|
||||
v-model="dbConfig.locationTreeNode"
|
||||
label="自动定位树节点"
|
||||
:true-value="1"
|
||||
:false-value="0"
|
||||
size="small"
|
||||
/>
|
||||
</el-row>
|
||||
|
||||
<el-row>
|
||||
<el-checkbox
|
||||
v-model="dbConfig.cacheTable"
|
||||
label="缓存表信息-[不开启则实时获取表信息]"
|
||||
:true-value="1"
|
||||
:false-value="0"
|
||||
size="small"
|
||||
/>
|
||||
</el-row>
|
||||
|
||||
<template #reference>
|
||||
<el-link type="primary" icon="setting" :underline="false"></el-link>
|
||||
</template>
|
||||
</el-popover>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label-align="right" label="tag">{{ nowDbInst.tagPath }}</el-descriptions-item>
|
||||
|
||||
@@ -103,7 +158,9 @@
|
||||
<el-tab-pane class="h100" closable v-for="dt in state.tabs.values()" :label="dt.label" :name="dt.key" :key="dt.key">
|
||||
<template #label>
|
||||
<el-popover :show-after="1000" placement="bottom-start" trigger="hover" :width="250">
|
||||
<template #reference> {{ dt.label }} </template>
|
||||
<template #reference>
|
||||
<span @contextmenu.prevent="onTabContextmenu(dt, $event)" class="font12">{{ dt.label }}</span>
|
||||
</template>
|
||||
<template #default>
|
||||
<el-descriptions :column="1" size="small">
|
||||
<el-descriptions-item label="tagPath">
|
||||
@@ -130,6 +187,7 @@
|
||||
:db-name="dt.db"
|
||||
:table-name="dt.params.table"
|
||||
:table-height="state.dataTabsTableHeight"
|
||||
:ref="(el: any) => (dt.componentRef = el)"
|
||||
></db-table-data-op>
|
||||
|
||||
<db-sql-editor
|
||||
@@ -138,6 +196,7 @@
|
||||
:db-name="dt.db"
|
||||
:sql-name="dt.params.sqlName"
|
||||
@save-sql-success="reloadSqls"
|
||||
:ref="(el: any) => (dt.componentRef = el)"
|
||||
>
|
||||
</db-sql-editor>
|
||||
|
||||
@@ -146,7 +205,6 @@
|
||||
:db-id="dt.params.id"
|
||||
:db="dt.params.db"
|
||||
:db-type="dt.params.type"
|
||||
:flow-procdef="dt.params.flowProcdef"
|
||||
:height="state.tablesOpHeight"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
@@ -161,34 +219,41 @@
|
||||
:dbId="tableCreateDialog.dbId"
|
||||
:db="tableCreateDialog.db"
|
||||
:dbType="tableCreateDialog.dbType"
|
||||
:flow-procdef="tableCreateDialog.flowProcdef"
|
||||
:version="tableCreateDialog.version"
|
||||
:data="tableCreateDialog.data"
|
||||
v-model:visible="tableCreateDialog.visible"
|
||||
@submit-sql="onSubmitEditTableSql"
|
||||
/>
|
||||
|
||||
<el-dialog width="55%" :title="`'${state.chooseTableName}' DDL`" v-model="state.ddlDialog.visible">
|
||||
<monaco-editor height="400px" language="sql" v-model="state.ddlDialog.ddl" :options="{ readOnly: true }" />
|
||||
</el-dialog>
|
||||
|
||||
<contextmenu ref="tabContextmenuRef" :dropdown="state.tabContextmenu.dropdown" :items="state.tabContextmenu.items" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, h, onBeforeUnmount, onMounted, reactive, ref, toRefs, watch } from 'vue';
|
||||
import { defineAsyncComponent, h, onBeforeUnmount, onMounted, reactive, ref, toRefs, useTemplateRef, watch } from 'vue';
|
||||
import { ElCheckbox, ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { formatByteSize } from '@/common/utils/format';
|
||||
import { DbInst, registerDbCompletionItemProvider, TabInfo, TabType } from './db';
|
||||
import { NodeType, TagTreeNode, getTagTypeCodeByPath } from '../component/tag';
|
||||
import { DbInst, DbThemeConfig, registerDbCompletionItemProvider, TabInfo, TabType } from './db';
|
||||
import { getTagTypeCodeByPath, NodeType, TagTreeNode } from '../component/tag';
|
||||
import TagTree from '../component/TagTree.vue';
|
||||
import { dbApi } from './api';
|
||||
import { dispposeCompletionItemProvider } from '@/components/monaco/completionItemProvider';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
import { ContextmenuItem } from '@/components/contextmenu';
|
||||
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
|
||||
import { getDbDialect, schemaDbTypes } from './dialect/index';
|
||||
import { sleep } from '@/common/utils/loading';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import { Pane, Splitpanes } from 'splitpanes';
|
||||
import { useEventListener } from '@vueuse/core';
|
||||
import { useEventListener, useStorage } from '@vueuse/core';
|
||||
import SqlExecBox from '@/views/ops/db/component/sqleditor/SqlExecBox';
|
||||
import { useAutoOpenResource } from '@/store/autoOpenResource';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { procdefApi } from '@/views/flow/api';
|
||||
import { format as sqlFormatter } from 'sql-formatter';
|
||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||
|
||||
const DbTableOp = defineAsyncComponent(() => import('./component/table/DbTableOp.vue'));
|
||||
const DbSqlEditor = defineAsyncComponent(() => import('./component/sqleditor/DbSqlEditor.vue'));
|
||||
@@ -231,10 +296,10 @@ const SqlIcon = {
|
||||
};
|
||||
|
||||
// node节点点击时,触发改变db事件
|
||||
const nodeClickChangeDb = (nodeData: TagTreeNode) => {
|
||||
const nodeClickChangeDb = async (nodeData: TagTreeNode) => {
|
||||
const params = nodeData.params;
|
||||
if (params.db) {
|
||||
changeDb(
|
||||
await changeDb(
|
||||
{
|
||||
id: params.id,
|
||||
host: `${params.host}`,
|
||||
@@ -242,7 +307,6 @@ const nodeClickChangeDb = (nodeData: TagTreeNode) => {
|
||||
type: params.type,
|
||||
tagPath: params.tagPath,
|
||||
databases: params.dbs,
|
||||
flowProcdef: params.flowProcdef,
|
||||
},
|
||||
params.db
|
||||
);
|
||||
@@ -274,7 +338,9 @@ const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc(as
|
||||
const params = parentNode.params;
|
||||
const dbs = (await DbInst.getDbNames(params))?.sort();
|
||||
|
||||
const flowProcdef = await procdefApi.getByResource.request({ resourceType: TagResourceTypeEnum.DbName.value, resourceCode: params.code });
|
||||
// 查询数据库版本信息
|
||||
const version = await dbApi.getCompatibleDbVersion.request({ id: params.id, db: dbs[0] });
|
||||
|
||||
return dbs.map((x: any) => {
|
||||
return new TagTreeNode(`${parentNode.key}.${x}`, x, NodeTypeDb)
|
||||
.withParams({
|
||||
@@ -282,10 +348,10 @@ const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc(as
|
||||
id: params.id,
|
||||
name: params.name,
|
||||
type: params.type,
|
||||
version: version || 'unset',
|
||||
host: `${params.host}:${params.port}`,
|
||||
dbs: dbs,
|
||||
db: x,
|
||||
flowProcdef: flowProcdef,
|
||||
})
|
||||
.withIcon(DbIcon);
|
||||
});
|
||||
@@ -319,7 +385,12 @@ const getNodeTypeTables = (params: any) => {
|
||||
let tableKey = `${params.id}.${params.db}.table-menu`;
|
||||
let sqlKey = getSqlMenuNodeKey(params.id, params.db);
|
||||
return [
|
||||
new TagTreeNode(`${params.id}.${params.db}.table-menu`, '表', NodeTypeTableMenu).withParams({ ...params, key: tableKey }).withIcon(TableIcon),
|
||||
new TagTreeNode(`${params.id}.${params.db}.table-menu`, '表', NodeTypeTableMenu)
|
||||
.withParams({
|
||||
...params,
|
||||
key: tableKey,
|
||||
})
|
||||
.withIcon(TableIcon),
|
||||
new TagTreeNode(sqlKey, 'SQL', NodeTypeSqlMenu).withParams({ ...params, key: sqlKey }).withIcon(SqlIcon),
|
||||
];
|
||||
};
|
||||
@@ -346,10 +417,10 @@ const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu)
|
||||
])
|
||||
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const params = parentNode.params;
|
||||
let { id, db, type, flowProcdef, schema } = params;
|
||||
let { id, db, type, schema, version } = params;
|
||||
// 获取当前库的所有表信息
|
||||
let tables = await DbInst.getInst(id).loadTables(db, state.reloadStatus);
|
||||
state.reloadStatus = false;
|
||||
state.reloadStatus = !dbConfig.value.cacheTable;
|
||||
let dbTableSize = 0;
|
||||
const tablesNode = tables.map((x: any) => {
|
||||
const tableSize = x.dataLength + x.indexLength;
|
||||
@@ -362,7 +433,7 @@ const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu)
|
||||
db,
|
||||
type,
|
||||
schema,
|
||||
flowProcdef: flowProcdef,
|
||||
version,
|
||||
key: key,
|
||||
parentKey: parentNode.key,
|
||||
tableName: x.tableName,
|
||||
@@ -378,7 +449,7 @@ const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu)
|
||||
})
|
||||
.withNodeDblclickFunc((node: TagTreeNode) => {
|
||||
const params = node.params;
|
||||
addTablesOpTab({ id: params.id, db: params.db, type: params.type, nodeKey: node.key });
|
||||
addTablesOpTab({ id: params.id, db: params.db, type: params.type, version: params.version, nodeKey: node.key });
|
||||
});
|
||||
|
||||
// 数据库sql模板菜单节点
|
||||
@@ -406,6 +477,7 @@ const NodeTypeTable = new NodeType(SqlExecNodeType.Table)
|
||||
new ContextmenuItem('renameTable', '重命名').withIcon('edit').withOnClick((data: any) => onRenameTable(data)),
|
||||
new ContextmenuItem('editTable', '编辑表').withIcon('edit').withOnClick((data: any) => onEditTable(data)),
|
||||
new ContextmenuItem('delTable', '删除表').withIcon('Delete').withOnClick((data: any) => onDeleteTable(data)),
|
||||
new ContextmenuItem('ddl', 'DDL').withIcon('Document').withOnClick((data: any) => onGenDdl(data)),
|
||||
])
|
||||
.withNodeClickFunc((nodeData: TagTreeNode) => {
|
||||
const params = nodeData.params;
|
||||
@@ -422,7 +494,24 @@ const NodeTypeSql = new NodeType(SqlExecNodeType.Sql)
|
||||
new ContextmenuItem('delSql', '删除').withIcon('delete').withOnClick((data: any) => deleteSql(data.params.id, data.params.db, data.params.sqlName)),
|
||||
]);
|
||||
|
||||
const tabContextmenuItems = [
|
||||
new ContextmenuItem(1, '关闭').withIcon('Close').withOnClick((data: any) => {
|
||||
onRemoveTab(data.key);
|
||||
}),
|
||||
|
||||
new ContextmenuItem(2, '关闭其他').withIcon('CircleClose').withOnClick((data: any) => {
|
||||
const tabName = data.key;
|
||||
const tabNames = [...state.tabs.keys()];
|
||||
for (let tab of tabNames) {
|
||||
if (tab !== tabName) {
|
||||
onRemoveTab(tab);
|
||||
}
|
||||
}
|
||||
}),
|
||||
];
|
||||
|
||||
const tagTreeRef: any = ref(null);
|
||||
const tabContextmenuRef: any = useTemplateRef('tabContextmenuRef');
|
||||
|
||||
const tabs: Map<string, TabInfo> = new Map();
|
||||
const state = reactive({
|
||||
@@ -435,6 +524,10 @@ const state = reactive({
|
||||
activeName: '',
|
||||
reloadStatus: false,
|
||||
tabs,
|
||||
tabContextmenu: {
|
||||
dropdown: { x: 0, y: 0 },
|
||||
items: tabContextmenuItems,
|
||||
},
|
||||
dataTabsTableHeight: '600px',
|
||||
tablesOpHeight: '600',
|
||||
dbServerInfo: {
|
||||
@@ -446,16 +539,23 @@ const state = reactive({
|
||||
title: '',
|
||||
activeName: '',
|
||||
dbId: 0,
|
||||
version: '',
|
||||
db: '',
|
||||
dbType: '',
|
||||
flowProcdef: null as any,
|
||||
data: {},
|
||||
parentKey: '',
|
||||
},
|
||||
chooseTableName: '',
|
||||
ddlDialog: {
|
||||
visible: false,
|
||||
ddl: '',
|
||||
},
|
||||
});
|
||||
|
||||
const { nowDbInst, tableCreateDialog } = toRefs(state);
|
||||
|
||||
const dbConfig = useStorage('dbConfig', DbThemeConfig);
|
||||
|
||||
const serverInfoReqParam = ref({
|
||||
instanceId: 0,
|
||||
});
|
||||
@@ -465,6 +565,7 @@ const autoOpenResourceStore = useAutoOpenResource();
|
||||
const { autoOpenResource } = storeToRefs(autoOpenResourceStore);
|
||||
|
||||
onMounted(() => {
|
||||
state.reloadStatus = !dbConfig.value.cacheTable;
|
||||
autoOpenDb(autoOpenResource.value.dbCodePath);
|
||||
setHeight();
|
||||
// 监听浏览器窗口大小变化,更新对应组件高度
|
||||
@@ -487,7 +588,7 @@ const autoOpenDb = (codePath: string) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const typeAndCodes = getTagTypeCodeByPath(codePath);
|
||||
const typeAndCodes: any = getTagTypeCodeByPath(codePath);
|
||||
const tagPath = typeAndCodes[TagResourceTypeEnum.Tag.value].join('/') + '/';
|
||||
|
||||
const dbCode = typeAndCodes[TagResourceTypeEnum.DbName.value][0];
|
||||
@@ -517,8 +618,8 @@ const showDbInfo = async (db: any) => {
|
||||
};
|
||||
|
||||
// 选择数据库,改变当前正在操作的数据库信息
|
||||
const changeDb = (db: any, dbName: string) => {
|
||||
state.nowDbInst = DbInst.getOrNewInst(db);
|
||||
const changeDb = async (db: any, dbName: string) => {
|
||||
state.nowDbInst = await DbInst.getOrNewInst(db);
|
||||
state.nowDbInst.databases = db.databases;
|
||||
state.db = dbName;
|
||||
};
|
||||
@@ -528,9 +629,9 @@ const loadTableData = async (db: any, dbName: string, tableName: string) => {
|
||||
if (tableName == '') {
|
||||
return;
|
||||
}
|
||||
changeDb(db, dbName);
|
||||
await changeDb(db, dbName);
|
||||
|
||||
const key = `${db.id}:\`${dbName}\`.${tableName}`;
|
||||
const key = `tableData:${db.id}.${dbName}.${tableName}`;
|
||||
let tab = state.tabs.get(key);
|
||||
state.activeName = key;
|
||||
// 如果存在该表tab,则直接返回
|
||||
@@ -557,7 +658,7 @@ const addQueryTab = async (db: any, dbName: string, sqlName: string = '') => {
|
||||
ElMessage.warning('请选择数据库实例及对应的schema');
|
||||
return;
|
||||
}
|
||||
changeDb(db, dbName);
|
||||
await changeDb(db, dbName);
|
||||
|
||||
const dbId = db.id;
|
||||
let label;
|
||||
@@ -565,7 +666,7 @@ const addQueryTab = async (db: any, dbName: string, sqlName: string = '') => {
|
||||
// 存在sql模板名,则该模板名只允许一个tab
|
||||
if (sqlName) {
|
||||
label = `查询-${sqlName}`;
|
||||
key = `查询:${dbId}:${dbName}.${sqlName}`;
|
||||
key = `query:${dbId}.${dbName}.${sqlName}`;
|
||||
} else {
|
||||
let count = 1;
|
||||
state.tabs.forEach((v) => {
|
||||
@@ -574,7 +675,7 @@ const addQueryTab = async (db: any, dbName: string, sqlName: string = '') => {
|
||||
}
|
||||
});
|
||||
label = `新查询-${count}`;
|
||||
key = `新查询${count}:${dbId}:${dbName}`;
|
||||
key = `query:${count}.${dbId}.${dbName}`;
|
||||
}
|
||||
state.activeName = key;
|
||||
let tab = state.tabs.get(key);
|
||||
@@ -608,10 +709,10 @@ const addTablesOpTab = async (db: any) => {
|
||||
ElMessage.warning('请选择数据库实例及对应的schema');
|
||||
return;
|
||||
}
|
||||
changeDb(db, dbName);
|
||||
await changeDb(db, dbName);
|
||||
|
||||
const dbId = db.id;
|
||||
let key = `表操作:${dbId}:${dbName}.tablesOp`;
|
||||
let key = `tablesOp:${dbId}.${dbName}`;
|
||||
state.activeName = key;
|
||||
|
||||
let tab = state.tabs.get(key);
|
||||
@@ -642,15 +743,22 @@ const onRemoveTab = (targetName: string) => {
|
||||
if (tabName !== targetName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
state.tabs.delete(targetName);
|
||||
if (activeName != targetName) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 如果删除的tab是当前激活的tab,则切换到前一个或后一个tab
|
||||
const nextTab = tabNames[i + 1] || tabNames[i - 1];
|
||||
if (nextTab) {
|
||||
activeName = nextTab;
|
||||
} else {
|
||||
activeName = '';
|
||||
}
|
||||
state.tabs.delete(targetName);
|
||||
state.activeName = activeName;
|
||||
onTabChange();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -670,6 +778,30 @@ const onTabChange = () => {
|
||||
registerDbCompletionItemProvider(nowTab.dbId, nowTab.db, nowTab.params.dbs, nowDbInst.value.type);
|
||||
}
|
||||
|
||||
// 激活当前tab(需要调用DbTableData组件的active,否则表头与数据会出现错位,暂不知为啥,先这样处理)
|
||||
nowTab?.componentRef?.active();
|
||||
|
||||
if (dbConfig.value.locationTreeNode) {
|
||||
locationNowTreeNode(nowTab);
|
||||
}
|
||||
};
|
||||
|
||||
// 右键点击时:传 x,y 坐标值到子组件中(props)
|
||||
const onTabContextmenu = (v: any, e: any) => {
|
||||
console.log('on tab cm');
|
||||
const { clientX, clientY } = e;
|
||||
state.tabContextmenu.dropdown.x = clientX;
|
||||
state.tabContextmenu.dropdown.y = clientY;
|
||||
tabContextmenuRef.value.openContextmenu(v);
|
||||
};
|
||||
|
||||
/**
|
||||
* 定位至当前树节点
|
||||
*/
|
||||
const locationNowTreeNode = (nowTab: any = null) => {
|
||||
if (!nowTab) {
|
||||
nowTab = state.tabs.get(state.activeName);
|
||||
}
|
||||
tagTreeRef.value.setCurrentKey(nowTab?.treeNodeKey);
|
||||
};
|
||||
|
||||
@@ -702,7 +834,7 @@ const reloadNode = (nodeKey: string) => {
|
||||
};
|
||||
|
||||
const onEditTable = async (data: any) => {
|
||||
let { db, id, tableName, tableComment, type, parentKey, key, flowProcdef } = data.params;
|
||||
let { db, id, tableName, tableComment, type, parentKey, key, version } = data.params;
|
||||
// data.label就是表名
|
||||
if (tableName) {
|
||||
state.tableCreateDialog.title = '修改表';
|
||||
@@ -719,14 +851,14 @@ const onEditTable = async (data: any) => {
|
||||
|
||||
state.tableCreateDialog.activeName = '1';
|
||||
state.tableCreateDialog.dbId = id;
|
||||
state.tableCreateDialog.version = version;
|
||||
state.tableCreateDialog.db = db;
|
||||
state.tableCreateDialog.dbType = type;
|
||||
state.tableCreateDialog.flowProcdef = flowProcdef;
|
||||
state.tableCreateDialog.visible = true;
|
||||
};
|
||||
|
||||
const onDeleteTable = async (data: any) => {
|
||||
let { db, id, tableName, parentKey, flowProcdef, schema } = data.params;
|
||||
let { db, id, tableName, parentKey, schema } = data.params;
|
||||
await ElMessageBox.confirm(`此操作是永久性且无法撤销,确定删除【${tableName}】? `, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
@@ -737,20 +869,33 @@ const onDeleteTable = async (data: any) => {
|
||||
let dialect = getDbDialect(state.nowDbInst.type);
|
||||
let schemaStr = schema ? `${dialect.quoteIdentifier(schema)}.` : '';
|
||||
|
||||
dbApi.sqlExec.request({ id, db, sql: `drop table ${schemaStr + dialect.quoteIdentifier(tableName)}` }).then(() => {
|
||||
if (flowProcdef) {
|
||||
ElMessage.success('工单提交成功');
|
||||
return;
|
||||
dbApi.sqlExec.request({ id, db, sql: `drop table ${schemaStr + dialect.quoteIdentifier(tableName)}` }).then((res) => {
|
||||
let success = true;
|
||||
for (let re of res) {
|
||||
if (re.errorMsg) {
|
||||
success = false;
|
||||
ElMessage.error(`${re.sql} -> 执行失败: ${re.errorMsg}`);
|
||||
}
|
||||
}
|
||||
if (success) {
|
||||
ElMessage.success('删除成功');
|
||||
setTimeout(() => {
|
||||
parentKey && reloadNode(parentKey);
|
||||
}, 1000);
|
||||
}
|
||||
ElMessage.success('删除成功');
|
||||
setTimeout(() => {
|
||||
parentKey && reloadNode(parentKey);
|
||||
}, 1000);
|
||||
});
|
||||
};
|
||||
|
||||
const onGenDdl = async (data: any) => {
|
||||
let { db, id, tableName, type } = data.params;
|
||||
state.chooseTableName = tableName;
|
||||
let res = await dbApi.tableDdl.request({ id, db, tableName });
|
||||
state.ddlDialog.ddl = sqlFormatter(res, { language: getDbDialect(type).getInfo().formatSqlDialect as any });
|
||||
state.ddlDialog.visible = true;
|
||||
};
|
||||
|
||||
const onRenameTable = async (data: any) => {
|
||||
let { db, id, tableName, parentKey, flowProcdef } = data.params;
|
||||
let { db, id, tableName, parentKey } = data.params;
|
||||
let tableData = { db, oldTableName: tableName, tableName };
|
||||
|
||||
let value = ref(tableName);
|
||||
@@ -773,7 +918,6 @@ const onRenameTable = async (data: any) => {
|
||||
dbId: id as any,
|
||||
db: db as any,
|
||||
dbType: nowDbInst.value.getDialect().getInfo().formatSqlDialect,
|
||||
flowProcdef: flowProcdef,
|
||||
runSuccessCallback: () => {
|
||||
setTimeout(() => {
|
||||
parentKey && reloadNode(parentKey);
|
||||
@@ -833,7 +977,6 @@ const getNowDbInfo = () => {
|
||||
name: di.name,
|
||||
type: di.type,
|
||||
host: di.host,
|
||||
flowProcdef: di.flowProcdef,
|
||||
dbName: state.db,
|
||||
};
|
||||
};
|
||||
@@ -854,7 +997,7 @@ const getNowDbInfo = () => {
|
||||
margin: 0 0 5px;
|
||||
|
||||
.el-tabs__item {
|
||||
padding: 0 10px;
|
||||
padding: 0 5px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,46 +1,33 @@
|
||||
<template>
|
||||
<div class="sync-task-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-drawer :title="title" v-model="dialogVisible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="45%">
|
||||
<template #header>
|
||||
<DrawerHeader :header="title" :back="cancel" />
|
||||
</template>
|
||||
|
||||
<el-form :model="form" ref="dbForm" :rules="rules" label-width="auto">
|
||||
<el-tabs v-model="tabActiveName" style="height: 450px">
|
||||
<el-tabs v-model="tabActiveName">
|
||||
<el-tab-pane label="基本信息" :name="basicTab">
|
||||
<el-form-item>
|
||||
<el-row>
|
||||
<el-col :span="11">
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="taskName" label="任务名" required>
|
||||
<el-input v-model.trim="form.taskName" placeholder="请输入同步任务名" auto-complete="off" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="11">
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="taskCron" label="cron" required>
|
||||
<CrontabInput v-model="form.taskCron" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="2">
|
||||
<el-form-item prop="status" label="状态" label-width="60" required>
|
||||
<el-switch
|
||||
v-model="form.status"
|
||||
inline-prompt
|
||||
active-text="启用"
|
||||
inactive-text="禁用"
|
||||
:active-value="1"
|
||||
:inactive-value="-1"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="status" label="状态" label-width="60" required>
|
||||
<el-switch v-model="form.status" inline-prompt active-text="启用" inactive-text="禁用" :active-value="1" :inactive-value="-1" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="srcDbId" label="源数据库" required>
|
||||
<db-select-tree
|
||||
placeholder="请选择源数据库"
|
||||
@@ -69,36 +56,73 @@
|
||||
<monaco-editor height="150px" class="task-sql" language="sql" v-model="form.dataSql" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="targetTableName" label="目标库表" required>
|
||||
<el-select v-model="form.targetTableName" filterable placeholder="请选择目标数据库表">
|
||||
<el-option
|
||||
v-for="item in state.targetTableList"
|
||||
:key="item.tableName"
|
||||
:label="item.tableName + (item.tableComment && '-' + item.tableComment)"
|
||||
:value="item.tableName"
|
||||
/>
|
||||
</el-select>
|
||||
<el-form-item>
|
||||
<el-row class="w100">
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="targetTableName" label="目标库表" required>
|
||||
<el-select v-model="form.targetTableName" filterable placeholder="请选择目标数据库表">
|
||||
<el-option
|
||||
v-for="item in state.targetTableList"
|
||||
:key="item.tableName"
|
||||
:label="item.tableName + (item.tableComment && '-' + item.tableComment)"
|
||||
:value="item.tableName"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="pageSize" label="分页大小" required>
|
||||
<el-input type="number" v-model.number="form.pageSize" placeholder="同步数据时查询的每页数据大小" auto-complete="off" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-row>
|
||||
<el-col :span="8">
|
||||
<el-form-item prop="pageSize" label="分页大小" required>
|
||||
<el-input type="number" v-model.number="form.pageSize" placeholder="同步数据时查询的每页数据大小" auto-complete="off" />
|
||||
<el-form-item class="w100" prop="updField">
|
||||
<template #label>
|
||||
更新字段
|
||||
<el-tooltip content="查询数据源的时候会带上这个字段当前最大值,支持带别名,如:t.create_time" placement="top">
|
||||
<el-icon>
|
||||
<question-filled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-input v-model.trim="form.updField" placeholder="查询数据源的时候会带上这个字段当前最大值" auto-complete="off" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="8">
|
||||
<el-tooltip content="查询数据源的时候会带上这个字段当前最大值,支持带别名,如:t.create_time" placement="top">
|
||||
<el-form-item prop="updField" label="更新字段" required>
|
||||
<el-input v-model.trim="form.updField" placeholder="查询数据源的时候会带上这个字段当前最大值" auto-complete="off" />
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
<el-form-item class="w100" prop="updFieldVal">
|
||||
<template #label>
|
||||
更新值
|
||||
<el-tooltip content="记录更新字段当前值,如:当前时间,当前日期等,下次查询数据时会带上该值条件" placement="top">
|
||||
<el-icon>
|
||||
<question-filled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-input v-model.trim="form.updFieldVal" placeholder="更新字段当前最大值" auto-complete="off" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="8">
|
||||
<el-form-item prop="updFieldVal" label="更新值">
|
||||
<el-input v-model.trim="form.updFieldVal" placeholder="更新字段当前最大值" auto-complete="off" />
|
||||
<el-form-item class="w100" prop="updFieldSrc">
|
||||
<template #label>
|
||||
值来源
|
||||
<el-tooltip
|
||||
content="从查询结果中取更新值的字段名,默认同更新字段,如果查询结果指定了字段别名且与原更新字段不一致,则取这个字段值为当前更新值"
|
||||
placement="top"
|
||||
>
|
||||
<el-icon>
|
||||
<question-filled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-input v-model.trim="form.updFieldSrc" placeholder="更新值来源" auto-complete="off" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@@ -183,7 +207,18 @@
|
||||
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk">确 定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-drawer>
|
||||
|
||||
<!-- <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-dialog> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -196,6 +231,7 @@ import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||
import { DbInst, registerDbCompletionItemProvider } from '@/views/ops/db/db';
|
||||
import { compatibleDuplicateStrategy, DbType, DuplicateStrategy, getDbDialect } from '@/views/ops/db/dialect';
|
||||
import CrontabInput from '@/components/crontab/CrontabInput.vue';
|
||||
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
@@ -253,6 +289,7 @@ type FormData = {
|
||||
pageSize?: number;
|
||||
updField?: string;
|
||||
updFieldVal?: string;
|
||||
updFieldSrc?: string;
|
||||
fieldMap?: { src: string; target: string }[];
|
||||
status?: 1 | 2;
|
||||
duplicateStrategy?: -1 | 1 | 2;
|
||||
@@ -326,9 +363,9 @@ watch(dialogVisible, async (newValue: boolean) => {
|
||||
const db = dbInfoRes.list[0];
|
||||
// 初始化实例
|
||||
db.databases = db.database?.split(' ').sort() || [];
|
||||
state.srcDbInst = DbInst.getOrNewInst(db);
|
||||
state.srcDbInst = await DbInst.getOrNewInst(db);
|
||||
state.form.srcDbType = state.srcDbInst.type;
|
||||
state.form.srcInstName = db.instanceName;
|
||||
state.form.srcInstName = db.name;
|
||||
}
|
||||
|
||||
// 初始化target数据源
|
||||
@@ -338,9 +375,9 @@ watch(dialogVisible, async (newValue: boolean) => {
|
||||
const db = dbInfoRes.list[0];
|
||||
// 初始化实例
|
||||
db.databases = db.database?.split(' ').sort() || [];
|
||||
state.targetDbInst = DbInst.getOrNewInst(db);
|
||||
state.targetDbInst = await DbInst.getOrNewInst(db);
|
||||
state.form.targetDbType = state.targetDbInst.type;
|
||||
state.form.targetInstName = db.instanceName;
|
||||
state.form.targetInstName = db.name;
|
||||
}
|
||||
|
||||
if (targetDbId && state.form.targetDbName) {
|
||||
@@ -397,12 +434,12 @@ const refreshPreviewInsertSql = () => {
|
||||
const onSelectSrcDb = async (params: any) => {
|
||||
// 初始化数据源
|
||||
params.databases = params.dbs; // 数据源里需要这个值
|
||||
state.srcDbInst = DbInst.getOrNewInst(params);
|
||||
state.srcDbInst = await DbInst.getOrNewInst(params);
|
||||
registerDbCompletionItemProvider(params.id, params.db, params.dbs, params.type);
|
||||
};
|
||||
|
||||
const onSelectTargetDb = async (params: any) => {
|
||||
state.targetDbInst = DbInst.getOrNewInst(params);
|
||||
state.targetDbInst = await DbInst.getOrNewInst(params);
|
||||
await loadDbTables(params.id, params.db);
|
||||
};
|
||||
|
||||
@@ -465,7 +502,7 @@ const handleGetSrcFields = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
let filedMap = {};
|
||||
let filedMap: any = {};
|
||||
if (state.form.fieldMap && state.form.fieldMap.length > 0) {
|
||||
state.form.fieldMap.forEach((a: any) => {
|
||||
filedMap[a.src] = a.target;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Api from '@/common/Api';
|
||||
import { Base64 } from 'js-base64';
|
||||
import { AesEncrypt } from '@/common/crypto';
|
||||
|
||||
export const dbApi = {
|
||||
// 获取权限列表
|
||||
@@ -16,17 +16,7 @@ export const dbApi = {
|
||||
pgSchemas: Api.newGet('/dbs/{id}/pg/schemas'),
|
||||
// 获取表即列提示
|
||||
hintTables: Api.newGet('/dbs/{id}/hint-tables'),
|
||||
sqlExec: Api.newPost('/dbs/{id}/exec-sql').withBeforeHandler((param: any) => {
|
||||
// sql编码处理
|
||||
if (param.sql) {
|
||||
// 判断是开发环境就打印sql
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log(param.sql);
|
||||
}
|
||||
param.sql = Base64.encode(param.sql);
|
||||
}
|
||||
return param;
|
||||
}),
|
||||
sqlExec: Api.newPost('/dbs/{id}/exec-sql').withBeforeHandler(async (param: any) => await encryptField(param, 'sql')),
|
||||
// 保存sql
|
||||
saveSql: Api.newPost('/dbs/{id}/sql'),
|
||||
// 获取保存的sql
|
||||
@@ -36,6 +26,8 @@ export const dbApi = {
|
||||
deleteDbSql: Api.newDelete('/dbs/{id}/sql'),
|
||||
// 获取数据库sql执行记录
|
||||
getSqlExecs: Api.newGet('/dbs/sql-execs'),
|
||||
// 获取数据库兼容版本
|
||||
getCompatibleDbVersion: Api.newGet('/dbs/{id}/version'),
|
||||
|
||||
instances: Api.newGet('/instances'),
|
||||
getInstance: Api.newGet('/instances/{instanceId}'),
|
||||
@@ -70,13 +62,7 @@ export const dbApi = {
|
||||
|
||||
// 数据同步相关
|
||||
datasyncTasks: Api.newGet('/datasync/tasks'),
|
||||
saveDatasyncTask: Api.newPost('/datasync/tasks/save').withBeforeHandler((param: any) => {
|
||||
// sql编码处理
|
||||
if (param.dataSql) {
|
||||
param.dataSql = Base64.encode(param.dataSql);
|
||||
}
|
||||
return param;
|
||||
}),
|
||||
saveDatasyncTask: Api.newPost('/datasync/tasks/save').withBeforeHandler(async (param: any) => await encryptField(param, 'dataSql')),
|
||||
getDatasyncTask: Api.newGet('/datasync/tasks/{taskId}'),
|
||||
deleteDatasyncTask: Api.newDelete('/datasync/tasks/{taskId}/del'),
|
||||
updateDatasyncTaskStatus: Api.newPost('/datasync/tasks/{taskId}/status'),
|
||||
@@ -88,12 +74,31 @@ export const dbApi = {
|
||||
dbTransferTasks: Api.newGet('/dbTransfer'),
|
||||
saveDbTransferTask: Api.newPost('/dbTransfer/save'),
|
||||
deleteDbTransferTask: Api.newDelete('/dbTransfer/{taskId}/del'),
|
||||
updateDbTransferTaskStatus: Api.newPost('/dbTransfer/{taskId}/status'),
|
||||
runDbTransferTask: Api.newPost('/dbTransfer/{taskId}/run'),
|
||||
stopDbTransferTask: Api.newPost('/dbTransfer/{taskId}/stop'),
|
||||
dbTransferTaskLogs: Api.newGet('/dbTransfer/{taskId}/logs'),
|
||||
dbTransferFileList: Api.newGet('/dbTransfer/files/{taskId}'),
|
||||
dbTransferFileDel: Api.newPost('/dbTransfer/files/del/{fileId}'),
|
||||
dbTransferFileRun: Api.newPost('/dbTransfer/files/run'),
|
||||
dbTransferFileDown: Api.newGet('/dbTransfer/files/down/{fileUuid}'),
|
||||
};
|
||||
|
||||
export const dbSqlExecApi = {
|
||||
// 根据业务key获取sql执行信息
|
||||
getSqlExecByBizKey: Api.newGet('/dbs/sql-execs'),
|
||||
};
|
||||
const encryptField = async (param: any, field: string) => {
|
||||
// sql编码处理
|
||||
if (!param['_encrypted'] && param[field]) {
|
||||
// 判断是开发环境就打印sql
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log(param[field]);
|
||||
}
|
||||
// 使用rsa公钥加密sql
|
||||
param['_encrypted'] = 1;
|
||||
param[field] = AesEncrypt(param[field]);
|
||||
// console.log('解密结果', DesDecrypt(param[field]));
|
||||
}
|
||||
return param;
|
||||
};
|
||||
|
||||
@@ -27,25 +27,13 @@ import TagTreeResourceSelect from '../../component/TagTreeResourceSelect.vue';
|
||||
import { computed } from 'vue';
|
||||
import { DbInst } from '../db';
|
||||
|
||||
const props = defineProps({
|
||||
dbId: {
|
||||
type: Number,
|
||||
},
|
||||
instName: {
|
||||
type: String,
|
||||
},
|
||||
dbName: {
|
||||
type: String,
|
||||
},
|
||||
tagPath: {
|
||||
type: String,
|
||||
},
|
||||
dbType: {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
const dbId = defineModel<number>('dbId');
|
||||
const instName = defineModel<string>('instName');
|
||||
const dbName = defineModel<string>('dbName');
|
||||
const tagPath = defineModel<string>('tagPath');
|
||||
const dbType = defineModel<string>('dbType');
|
||||
|
||||
const emits = defineEmits(['update:dbName', 'update:tagPath', 'update:instName', 'update:dbId', 'update:dbType', 'selectDb']);
|
||||
const emits = defineEmits(['selectDb']);
|
||||
|
||||
/**
|
||||
* 树节点类型
|
||||
@@ -63,7 +51,7 @@ class SqlExecNodeType {
|
||||
|
||||
const selectNode = computed({
|
||||
get: () => {
|
||||
return props.dbName ? `${props.tagPath} > ${props.instName} > ${props.dbName}` : '';
|
||||
return dbName.value ? `${tagPath.value} > ${instName.value} > ${dbName.value}` : '';
|
||||
},
|
||||
set: () => {
|
||||
//
|
||||
@@ -116,6 +104,7 @@ const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc(as
|
||||
.withParams({
|
||||
tagPath: params.tagPath,
|
||||
id: params.id,
|
||||
code: params.code,
|
||||
instanceId: params.instanceId,
|
||||
name: params.name,
|
||||
type: params.type,
|
||||
@@ -156,12 +145,12 @@ const NodeTypePostgresSchema = new NodeType(SqlExecNodeType.PgSchema);
|
||||
|
||||
const changeNode = (nodeData: TagTreeNode) => {
|
||||
const params = nodeData.params;
|
||||
// postgres
|
||||
emits('update:dbName', params.db);
|
||||
emits('update:instName', params.name);
|
||||
emits('update:dbId', params.id);
|
||||
emits('update:tagPath', params.tagPath);
|
||||
emits('update:dbType', params.type);
|
||||
dbName.value = params.db;
|
||||
instName.value = params.name;
|
||||
dbId.value = params.id;
|
||||
tagPath.value = params.tagPath;
|
||||
dbType.value = params.type;
|
||||
|
||||
emits('selectDb', params);
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
:limit="100"
|
||||
>
|
||||
<el-tooltip :show-after="1000" class="box-item" effect="dark" content="SQL脚本执行" placement="top">
|
||||
<el-link type="success" :underline="false" icon="Document"></el-link>
|
||||
<el-link v-auth="'db:sqlscript:run'" type="success" :underline="false" icon="Document"></el-link>
|
||||
</el-tooltip>
|
||||
</el-upload>
|
||||
</div>
|
||||
@@ -52,7 +52,7 @@
|
||||
|
||||
<Pane :size="100 - state.editorSize">
|
||||
<div class="mt5 sql-exec-res h100">
|
||||
<el-tabs class="h100 w100" v-if="state.execResTabs.length > 0" @tab-remove="onRemoveTab" v-model="state.activeTab">
|
||||
<el-tabs class="h100 w100" v-if="state.execResTabs.length > 0" @tab-remove="onRemoveTab" @tab-change="active" v-model="state.activeTab">
|
||||
<el-tab-pane class="h100" closable v-for="dt in state.execResTabs" :label="dt.id" :name="dt.id" :key="dt.id">
|
||||
<template #label>
|
||||
<el-popover :show-after="1000" placement="top-start" title="执行信息" trigger="hover" :width="300">
|
||||
@@ -297,6 +297,8 @@ const onRunSql = async (newTab = false) => {
|
||||
// 去除字符串前的空格、换行等
|
||||
sql = sql.replace(/(^\s*)/g, '');
|
||||
|
||||
const sqls = splitSql(sql);
|
||||
|
||||
// 简单截取前十个字符
|
||||
const sqlPrefix = sql.slice(0, 10).toLowerCase();
|
||||
const nonQuery =
|
||||
@@ -307,28 +309,37 @@ const onRunSql = async (newTab = false) => {
|
||||
sqlPrefix.startsWith('drop') ||
|
||||
sqlPrefix.startsWith('create');
|
||||
|
||||
// 启用工单审批
|
||||
if (nonQuery && getNowDbInst().flowProcdef) {
|
||||
try {
|
||||
getNowDbInst().promptExeSql(props.dbName, sql, null, () => {
|
||||
ElMessage.success('工单提交成功');
|
||||
if (sqls.length == 1) {
|
||||
let execRemark;
|
||||
if (nonQuery) {
|
||||
const res: any = await ElMessageBox.prompt('请输入备注', 'Tip', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputErrorMessage: '输入执行该sql的备注信息',
|
||||
});
|
||||
} catch (e) {
|
||||
ElMessage.success('工单提交失败');
|
||||
execRemark = res.value;
|
||||
}
|
||||
runSql(sql, execRemark, newTab);
|
||||
} else {
|
||||
let isFirst = true;
|
||||
for (let s of sqls) {
|
||||
if (isFirst) {
|
||||
isFirst = false;
|
||||
runSql(s, '', newTab);
|
||||
} else {
|
||||
runSql(s, '', true);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let execRemark;
|
||||
if (nonQuery) {
|
||||
const res: any = await ElMessageBox.prompt('请输入备注', 'Tip', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputErrorMessage: '输入执行该sql的备注信息',
|
||||
});
|
||||
execRemark = res.value;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 执行单条sql
|
||||
*
|
||||
* @param sql 单条sql
|
||||
* @param newTab 是否新建tab
|
||||
*/
|
||||
const runSql = async (sql: string, remark = '', newTab = false) => {
|
||||
let execRes: ExecResTab;
|
||||
let i = 0;
|
||||
let id;
|
||||
@@ -356,12 +367,16 @@ const onRunSql = async (newTab = false) => {
|
||||
execRes.errorMsg = '';
|
||||
execRes.sql = '';
|
||||
|
||||
const { data, execute, isFetching, abort } = getNowDbInst().execSql(props.dbName, sql, execRemark);
|
||||
const { data, execute, isFetching, abort } = getNowDbInst().execSql(props.dbName, sql, remark);
|
||||
execRes.loading = isFetching;
|
||||
execRes.abortFn = abort;
|
||||
|
||||
await execute();
|
||||
const colAndData: any = data.value;
|
||||
const colAndData: any = (data.value as any)[0];
|
||||
if (colAndData.errorMsg) {
|
||||
throw { msg: colAndData.errorMsg };
|
||||
}
|
||||
|
||||
if (colAndData.res.length == 0) {
|
||||
state.tableDataEmptyText = '查无数据';
|
||||
}
|
||||
@@ -381,7 +396,8 @@ const onRunSql = async (newTab = false) => {
|
||||
execRes.data = [];
|
||||
execRes.tableColumn = [];
|
||||
execRes.table = '';
|
||||
execRes.errorMsg = e.msg;
|
||||
// 要实时响应,故需要用索引改变数据才生效
|
||||
state.execResTabs[i].errorMsg = e.msg;
|
||||
return;
|
||||
} finally {
|
||||
execRes.sql = sql;
|
||||
@@ -403,6 +419,64 @@ const onRunSql = async (newTab = false) => {
|
||||
}
|
||||
};
|
||||
|
||||
function splitSql(sql: string) {
|
||||
let state = 'normal';
|
||||
let buffer = '';
|
||||
let result = [];
|
||||
let inString = null; // 用于记录当前字符串的引号类型(' 或 ")
|
||||
|
||||
for (let i = 0; i < sql.length; i++) {
|
||||
const char = sql[i];
|
||||
const nextChar = sql[i + 1];
|
||||
|
||||
if (state === 'normal') {
|
||||
if (char === '-' && nextChar === '-') {
|
||||
state = 'singleLineComment';
|
||||
i++; // 跳过下一个字符
|
||||
} else if (char === '/' && nextChar === '*') {
|
||||
state = 'multiLineComment';
|
||||
i++; // 跳过下一个字符
|
||||
} else if (char === "'" || char === '"') {
|
||||
state = 'string';
|
||||
inString = char;
|
||||
buffer += char;
|
||||
} else if (char === ';') {
|
||||
if (buffer.trim()) {
|
||||
result.push(buffer.trim());
|
||||
}
|
||||
buffer = '';
|
||||
} else {
|
||||
buffer += char;
|
||||
}
|
||||
} else if (state === 'string') {
|
||||
buffer += char;
|
||||
if (char === '\\') {
|
||||
// 处理转义字符
|
||||
buffer += nextChar;
|
||||
i++;
|
||||
} else if (char === inString) {
|
||||
state = 'normal';
|
||||
inString = null;
|
||||
}
|
||||
} else if (state === 'singleLineComment') {
|
||||
if (char === '\n') {
|
||||
state = 'normal';
|
||||
}
|
||||
} else if (state === 'multiLineComment') {
|
||||
if (char === '*' && nextChar === '/') {
|
||||
state = 'normal';
|
||||
i++; // 跳过下一个字符
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (buffer.trim()) {
|
||||
result.push(buffer.trim());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取sql,如果有鼠标选中,则返回选中内容,否则返回输入框内所有内容
|
||||
*/
|
||||
@@ -700,6 +774,19 @@ const initMonacoEditor = () => {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const active = () => {
|
||||
const resTab = state.execResTabs[state.activeTab - 1];
|
||||
if (!resTab || !resTab.dbTableRef) {
|
||||
return;
|
||||
}
|
||||
|
||||
resTab.dbTableRef?.active();
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
active,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -2,18 +2,7 @@
|
||||
<div>
|
||||
<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" />
|
||||
<el-input
|
||||
@keyup.enter="runSql"
|
||||
ref="remarkInputRef"
|
||||
v-model="remark"
|
||||
:placeholder="props.flowProcdef ? '执行备注(必填)' : '执行备注(选填)'"
|
||||
class="mt5"
|
||||
/>
|
||||
|
||||
<div v-if="props.flowProcdef">
|
||||
<el-divider content-position="left">审批节点</el-divider>
|
||||
<procdef-tasks :procdef="props.flowProcdef" />
|
||||
</div>
|
||||
<el-input @keyup.enter="runSql" ref="remarkInputRef" v-model="remark" placeholder="执行备注" class="mt5" />
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
@@ -28,13 +17,13 @@
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, ref, reactive, onMounted } from 'vue';
|
||||
import { dbApi } from '@/views/ops/db/api';
|
||||
import { ElDialog, ElButton, ElInput, ElMessage, InputInstance, ElDivider } from 'element-plus';
|
||||
import { ElDialog, ElButton, ElInput, ElMessage, InputInstance } from 'element-plus';
|
||||
// import base style
|
||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||
import { format as sqlFormatter } from 'sql-formatter';
|
||||
|
||||
import { SqlExecProps } from './SqlExecBox';
|
||||
import ProcdefTasks from '@/views/flow/components/ProcdefTasks.vue';
|
||||
import { isTrue } from '@/common/assert';
|
||||
|
||||
const props = withDefaults(defineProps<SqlExecProps>(), {});
|
||||
|
||||
@@ -58,12 +47,6 @@ onMounted(() => {
|
||||
* 执行sql
|
||||
*/
|
||||
const runSql = async () => {
|
||||
// 存在流程审批,则备注为必填
|
||||
if (!state.remark && props.flowProcdef) {
|
||||
ElMessage.error('请输入执行的备注信息');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
state.btnLoading = true;
|
||||
runSuccess = true;
|
||||
@@ -75,19 +58,15 @@ const runSql = async () => {
|
||||
sql: state.sqlValue.trim(),
|
||||
});
|
||||
|
||||
// 存在流程审批
|
||||
if (props.flowProcdef) {
|
||||
ElMessage.success('工单提交成功');
|
||||
return;
|
||||
}
|
||||
|
||||
for (let re of res.res) {
|
||||
if (re.result !== 'success') {
|
||||
ElMessage.error(`${re.sql} \n执行失败: ${re.result}`);
|
||||
throw new Error(re.result);
|
||||
let isSuccess = true;
|
||||
for (let re of res) {
|
||||
if (re.errorMsg) {
|
||||
isSuccess = false;
|
||||
ElMessage.error(`${re.sql} \n执行失败: ${re.errorMsg}`);
|
||||
}
|
||||
}
|
||||
|
||||
isTrue(isSuccess, '存在执行失败sql');
|
||||
ElMessage.success('执行成功');
|
||||
} catch (e) {
|
||||
runSuccess = false;
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
:clearable="false"
|
||||
type="Date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="选择日期"
|
||||
:placeholder="`选择日期-${placeholder}`"
|
||||
/>
|
||||
|
||||
<el-date-picker
|
||||
@@ -41,7 +41,7 @@
|
||||
:clearable="false"
|
||||
type="datetime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="选择日期时间"
|
||||
:placeholder="`选择日期时间-${placeholder}`"
|
||||
/>
|
||||
|
||||
<el-time-picker
|
||||
@@ -56,7 +56,7 @@
|
||||
v-model="itemValue"
|
||||
:clearable="false"
|
||||
value-format="HH:mm:ss"
|
||||
placeholder="选择时间"
|
||||
:placeholder="`选择时间-${placeholder}`"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -152,6 +152,10 @@ const getEditorLangByValue = (value: any) => {
|
||||
<style lang="scss">
|
||||
.string-input-container {
|
||||
position: relative;
|
||||
|
||||
.el-input__wrapper {
|
||||
padding: 1px 3px;
|
||||
}
|
||||
}
|
||||
.string-input-container-show-icon {
|
||||
.el-input__inner {
|
||||
@@ -174,6 +178,10 @@ const getEditorLangByValue = (value: any) => {
|
||||
.el-input__prefix {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-input__wrapper {
|
||||
padding: 1px 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-time-picker-popper {
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
fixed
|
||||
class="table"
|
||||
:row-event-handlers="rowEventHandlers"
|
||||
@scroll="onTableScroll"
|
||||
>
|
||||
<template #header="{ columns }">
|
||||
<div v-for="(column, i) in columns" :key="i">
|
||||
@@ -59,9 +60,7 @@
|
||||
</div>
|
||||
|
||||
<div v-else class="header-column-title">
|
||||
<b class="el-text">
|
||||
{{ column.title }}
|
||||
</b>
|
||||
<b class="el-text"> {{ column.title }} </b>
|
||||
</div>
|
||||
|
||||
<!-- 字段列右部分内容 -->
|
||||
@@ -96,7 +95,7 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else :class="isUpdated(rowIndex, column.dataKey) ? 'update_field_active' : ''">
|
||||
<div v-else :class="isUpdated(rowIndex, column.dataKey) ? 'update_field_active ml2 mr2' : 'ml2 mr2'">
|
||||
<span v-if="rowData[column.dataKey!] === null" style="color: var(--el-color-info-light-5)"> NULL </span>
|
||||
|
||||
<span v-else :title="rowData[column.dataKey!]" class="el-text el-text--small is-truncated">
|
||||
@@ -121,7 +120,7 @@
|
||||
|
||||
<template #empty>
|
||||
<div style="text-align: center">
|
||||
<el-empty class="h100" :description="props.emptyText" :image-size="100" />
|
||||
<el-empty :description="props.emptyText" :image-size="100" />
|
||||
</div>
|
||||
</template>
|
||||
</el-table-v2>
|
||||
@@ -134,7 +133,7 @@
|
||||
<el-button id="copyValue" @click="copyGenTxt(state.genTxtDialog.txt)" icon="CopyDocument" type="success" size="small">一键复制</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-input v-model="state.genTxtDialog.txt" type="textarea" rows="20" />
|
||||
<el-input v-model="state.genTxtDialog.txt" type="textarea" :rows="20" />
|
||||
</el-dialog>
|
||||
|
||||
<DbTableDataForm
|
||||
@@ -157,7 +156,7 @@
|
||||
import { onBeforeUnmount, onMounted, reactive, ref, toRefs, watch } from 'vue';
|
||||
import { ElInput, ElMessage } from 'element-plus';
|
||||
import { copyToClipboard } from '@/common/utils/string';
|
||||
import { DbInst } from '@/views/ops/db/db';
|
||||
import { DbInst, DbThemeConfig } from '@/views/ops/db/db';
|
||||
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
import { exportCsv, exportFile } from '@/common/utils/export';
|
||||
@@ -259,12 +258,10 @@ const cmDataDel = new ContextmenuItem('deleteData', '删除')
|
||||
return state.table == '';
|
||||
});
|
||||
|
||||
const cmDataEdit = new ContextmenuItem('editData', '编辑行')
|
||||
.withIcon('edit')
|
||||
.withOnClick(() => onEditRowData())
|
||||
.withHideFunc(() => {
|
||||
return state.table == '';
|
||||
});
|
||||
const cmFormView = new ContextmenuItem('formView', '表单视图').withIcon('Document').withOnClick(() => onEditRowData());
|
||||
// .withHideFunc(() => {
|
||||
// return state.table == '';
|
||||
// });
|
||||
|
||||
const cmDataGenInsertSql = new ContextmenuItem('genInsertSql', 'Insert SQL')
|
||||
.withIcon('tickets')
|
||||
@@ -364,7 +361,7 @@ const state = reactive({
|
||||
|
||||
const { tableHeight, datas } = toRefs(state);
|
||||
|
||||
const dbConfig = useStorage('dbConfig', { showColumnComment: false });
|
||||
const dbConfig = useStorage('dbConfig', DbThemeConfig);
|
||||
|
||||
/**
|
||||
* 行号字段列
|
||||
@@ -486,7 +483,7 @@ const setTableColumns = (columns: any) => {
|
||||
dataKey: columnName,
|
||||
width: DbInst.flexColumnWidth(columnName, state.datas),
|
||||
title: columnName,
|
||||
align: 'center',
|
||||
align: x.dataType == DataType.Number ? 'right' : 'left',
|
||||
headerClass: 'table-column',
|
||||
class: 'table-column',
|
||||
sortable: true,
|
||||
@@ -596,7 +593,7 @@ const dataContextmenuClick = (event: any, rowIndex: number, column: any, data: a
|
||||
const { clientX, clientY } = event;
|
||||
state.contextmenu.dropdown.x = clientX;
|
||||
state.contextmenu.dropdown.y = clientY;
|
||||
state.contextmenu.items = [cmDataCopyCell, cmDataDel, cmDataEdit, cmDataGenInsertSql, cmDataGenJson, cmDataExportCsv, cmDataExportSql];
|
||||
state.contextmenu.items = [cmDataCopyCell, cmDataDel, cmFormView, cmDataGenInsertSql, cmDataGenJson, cmDataExportCsv, cmDataExportSql];
|
||||
contextmenuRef.value.openContextmenu({ column, rowData: data });
|
||||
};
|
||||
|
||||
@@ -617,10 +614,6 @@ const onDeleteData = async () => {
|
||||
const db = state.db;
|
||||
const dbInst = getNowDbInst();
|
||||
dbInst.promptExeSql(db, await dbInst.genDeleteByPrimaryKeysSql(db, state.table, deleteDatas as any), null, () => {
|
||||
// 存在流程则恢复原值,需工单流程审批完后自动执行
|
||||
if (dbInst.flowProcdef) {
|
||||
return;
|
||||
}
|
||||
emits('dataDelete', deleteDatas);
|
||||
});
|
||||
};
|
||||
@@ -628,12 +621,12 @@ const onDeleteData = async () => {
|
||||
const onEditRowData = () => {
|
||||
const selectionDatas = Array.from(selectionRowsMap.values());
|
||||
if (selectionDatas.length > 1) {
|
||||
ElMessage.warning('只能编辑一行数据');
|
||||
ElMessage.warning('只能选择一行数据');
|
||||
return;
|
||||
}
|
||||
const data = selectionDatas[0];
|
||||
state.tableDataFormDialog.data = { ...data };
|
||||
state.tableDataFormDialog.title = `编辑表'${props.table}'数据`;
|
||||
state.tableDataFormDialog.title = state.table ? `'${props.table}'表单数据` : '表单视图';
|
||||
state.tableDataFormDialog.visible = true;
|
||||
};
|
||||
|
||||
@@ -649,7 +642,7 @@ const onGenerateJson = async () => {
|
||||
// 按列字段重新排序对象key
|
||||
const jsonObj = [];
|
||||
for (let selectionData of selectionDatas) {
|
||||
let obj = {};
|
||||
let obj: any = {};
|
||||
for (let column of state.columns) {
|
||||
if (column.show) {
|
||||
obj[column.title] = selectionData[column.dataKey];
|
||||
@@ -753,7 +746,7 @@ const submitUpdateFields = async () => {
|
||||
|
||||
for (let updateRow of cellUpdateMap.values()) {
|
||||
const rowData = { ...updateRow.rowData };
|
||||
let updateColumnValue = {};
|
||||
let updateColumnValue: any = {};
|
||||
|
||||
for (let k of updateRow.columnsMap.keys()) {
|
||||
const v = updateRow.columnsMap.get(k);
|
||||
@@ -768,11 +761,6 @@ const submitUpdateFields = async () => {
|
||||
}
|
||||
|
||||
dbInst.promptExeSql(db, res, null, () => {
|
||||
// 存在流程则恢复原值,需工单流程审批完后自动执行
|
||||
if (dbInst.flowProcdef) {
|
||||
cancelUpdateFields();
|
||||
return;
|
||||
}
|
||||
triggerRefresh();
|
||||
cellUpdateMap.clear();
|
||||
changeUpdatedField();
|
||||
@@ -841,11 +829,23 @@ const triggerRefresh = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const scrollLeftValue = ref(0);
|
||||
const onTableScroll = (param: any) => {
|
||||
scrollLeftValue.value = param.scrollLeft;
|
||||
};
|
||||
/**
|
||||
* 激活表格,恢复滚动位置,否则会造成表头与数据单元格错位(暂不知为啥,先这样解决)
|
||||
*/
|
||||
const active = () => {
|
||||
setTimeout(() => tableRef.value.scrollToLeft(scrollLeftValue.value));
|
||||
};
|
||||
|
||||
const getNowDbInst = () => {
|
||||
return DbInst.getInst(state.dbId);
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
active,
|
||||
submitUpdateFields,
|
||||
cancelUpdateFields,
|
||||
});
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
:key="column.columnName"
|
||||
class="w100 mb5"
|
||||
:prop="column.columnName"
|
||||
:required="!column.nullable && !column.isPrimaryKey && !column.isIdentity"
|
||||
:required="props.tableName != '' && !column.nullable && !column.isPrimaryKey && !column.isIdentity"
|
||||
>
|
||||
<template #label>
|
||||
<span class="pointer" :title="`${column.columnType} | ${column.columnComment}`">
|
||||
<span class="pointer" :title="column?.columnComment ? `${column.columnType} | ${column.columnComment}` : column.columnType">
|
||||
{{ column.columnName }}
|
||||
</span>
|
||||
</template>
|
||||
@@ -17,13 +17,13 @@
|
||||
<ColumnFormItem
|
||||
v-model="modelValue[`${column.columnName}`]"
|
||||
:data-type="dbInst.getDialect().getDataType(column.dataType)"
|
||||
:placeholder="`${column.columnType} ${column.columnComment}`"
|
||||
:placeholder="column?.columnComment ? `${column.columnType} | ${column.columnComment}` : column.columnType"
|
||||
:column-name="column.columnName"
|
||||
:disabled="column.isIdentity"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<template #footer v-if="props.tableName">
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="closeDialog">取消</el-button>
|
||||
<el-button type="primary" @click="confirm">确定</el-button>
|
||||
@@ -99,7 +99,7 @@ const confirm = async () => {
|
||||
|
||||
let sql = '';
|
||||
if (oldValue) {
|
||||
const updateColumnValue = {};
|
||||
const updateColumnValue: any = {};
|
||||
Object.keys(oldValue).forEach((key) => {
|
||||
// 如果新旧值不相等,则为需要更新的字段
|
||||
if (oldValue[key] !== modelValue.value[key]) {
|
||||
|
||||
@@ -50,22 +50,6 @@
|
||||
</el-tooltip>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<!-- 表数据展示配置 -->
|
||||
<el-popover
|
||||
popper-style="max-height: 550px; overflow: auto; max-width: 450px"
|
||||
placement="bottom"
|
||||
width="auto"
|
||||
title="展示配置"
|
||||
trigger="click"
|
||||
>
|
||||
<el-checkbox v-model="dbConfig.showColumnComment" label="显示字段备注" :true-value="true" :false-value="false" size="small" />
|
||||
<template #reference>
|
||||
<el-link type="primary" icon="setting" :underline="false"></el-link>
|
||||
</template>
|
||||
</el-popover>
|
||||
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-tooltip :show-after="500" v-if="hasUpdatedFileds" class="box-item" effect="dark" content="提交修改" placement="top">
|
||||
<el-link @click="submitUpdateFields()" type="success" :underline="false" class="font12">提交</el-link>
|
||||
</el-tooltip>
|
||||
@@ -258,8 +242,8 @@ import { DbInst } from '@/views/ops/db/db';
|
||||
import DbTableData from './DbTableData.vue';
|
||||
import { DbDialect } from '@/views/ops/db/dialect';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
import { useEventListener, useStorage } from '@vueuse/core';
|
||||
import { copyToClipboard } from '@/common/utils/string';
|
||||
import { useEventListener } from '@vueuse/core';
|
||||
import { copyToClipboard, fuzzyMatchField } from '@/common/utils/string';
|
||||
import DbTableDataForm from './DbTableDataForm.vue';
|
||||
|
||||
const props = defineProps({
|
||||
@@ -288,8 +272,6 @@ const condDialogInputRef: Ref = ref(null);
|
||||
|
||||
const defaultPageSize = DbInst.DefaultLimit;
|
||||
|
||||
const dbConfig = useStorage('dbConfig', { showColumnComment: false });
|
||||
|
||||
const state = reactive({
|
||||
datas: [],
|
||||
sql: '', // 当前数据tab执行的sql
|
||||
@@ -404,7 +386,8 @@ const selectData = async () => {
|
||||
|
||||
let sql = dbInst.getDefaultSelectSql(db, table, state.condition, state.orderBy, state.pageNum, state.pageSize);
|
||||
state.sql = sql;
|
||||
const colAndData: any = await dbInst.runSql(db, sql);
|
||||
const res: any = await dbInst.runSql(db, sql);
|
||||
const colAndData: any = res[0];
|
||||
state.datas = colAndData.res;
|
||||
} finally {
|
||||
state.loading = false;
|
||||
@@ -435,7 +418,8 @@ const handleCount = async () => {
|
||||
const db = props.dbName;
|
||||
const table = props.tableName;
|
||||
const dbInst = getNowDbInst();
|
||||
const countRes = await dbInst.runSql(db, dbInst.getDefaultCountSql(table, state.condition));
|
||||
let countRes = await dbInst.runSql(db, dbInst.getDefaultCountSql(table, state.condition));
|
||||
countRes = countRes[0];
|
||||
state.total = parseInt(countRes.res[0].count || countRes.res[0].COUNT || 0);
|
||||
state.showTotal = true;
|
||||
} catch (e) {
|
||||
@@ -476,10 +460,7 @@ const getColumnTips = (queryString: string, callback: any) => {
|
||||
|
||||
let res = [];
|
||||
if (columnNameSearch) {
|
||||
columnNameSearch = columnNameSearch.toLowerCase();
|
||||
res = columns.filter((data: any) => {
|
||||
return data.columnName.toLowerCase().includes(columnNameSearch);
|
||||
});
|
||||
res = fuzzyMatchField(columnNameSearch, columns, (x: any) => x.columnName);
|
||||
}
|
||||
|
||||
completeCond = condition.value;
|
||||
@@ -534,10 +515,12 @@ const filterColumns = (searchKey: string) => {
|
||||
if (!searchKey) {
|
||||
return columns;
|
||||
}
|
||||
searchKey = searchKey.toLowerCase();
|
||||
return columns.filter((data: any) => {
|
||||
return data.columnName.toLowerCase().includes(searchKey) || data.columnComment.toLowerCase().includes(searchKey);
|
||||
});
|
||||
return fuzzyMatchField(
|
||||
searchKey,
|
||||
columns,
|
||||
(x: any) => x.columnName,
|
||||
(x: any) => x.columnComment
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -622,6 +605,10 @@ const onShowAddDataDialog = async () => {
|
||||
state.addDataDialog.title = `添加'${props.tableName}'表数据`;
|
||||
state.addDataDialog.visible = true;
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
active: () => dbTableRef.value.active(),
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
<el-select v-else-if="item.prop === 'type'" filterable size="small" v-model="scope.row.type">
|
||||
<el-option
|
||||
v-for="pgsqlType in getDbDialect(dbType).getInfo().columnTypes"
|
||||
v-for="pgsqlType in getDbDialect(dbType!).getInfo().columnTypes"
|
||||
:key="pgsqlType.dataType"
|
||||
:value="pgsqlType.udtName"
|
||||
:label="pgsqlType.dataType"
|
||||
@@ -127,7 +127,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, toRefs, watch } from 'vue';
|
||||
import { computed, reactive, ref, toRefs, watch } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import SqlExecBox from '../sqleditor/SqlExecBox';
|
||||
import { DbType, getDbDialect, IndexDefinition, RowDefinition } from '../../dialect/index';
|
||||
@@ -152,15 +152,15 @@ const props = defineProps({
|
||||
dbType: {
|
||||
type: String,
|
||||
},
|
||||
flowProcdef: {
|
||||
type: Object,
|
||||
version: {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
|
||||
//定义事件
|
||||
const emit = defineEmits(['update:visible', 'cancel', 'val-change', 'submit-sql']);
|
||||
|
||||
let dbDialect = getDbDialect(props.dbType);
|
||||
let dbDialect: any = computed(() => getDbDialect(props.dbType!, props.version));
|
||||
|
||||
type ColName = {
|
||||
prop: string;
|
||||
@@ -274,7 +274,7 @@ const { dialogVisible, btnloading, activeName, tableData } = toRefs(state);
|
||||
|
||||
watch(props, async (newValue) => {
|
||||
state.dialogVisible = newValue.visible;
|
||||
dbDialect = getDbDialect(newValue.dbType);
|
||||
dbDialect.value = getDbDialect(newValue.dbType!);
|
||||
});
|
||||
|
||||
// 切换到索引tab时,刷新索引字段下拉选项
|
||||
@@ -309,11 +309,11 @@ const addRow = () => {
|
||||
};
|
||||
|
||||
const addIndex = () => {
|
||||
state.tableData.indexs.res.push(dbDialect.getDefaultIndex());
|
||||
state.tableData.indexs.res.push(dbDialect.value.getDefaultIndex());
|
||||
};
|
||||
|
||||
const addDefaultRows = () => {
|
||||
state.tableData.fields.res.push(...dbDialect.getDefaultRows());
|
||||
state.tableData.fields.res.push(...dbDialect.value.getDefaultRows());
|
||||
};
|
||||
|
||||
const deleteRow = (index: any) => {
|
||||
@@ -334,8 +334,7 @@ const submit = async () => {
|
||||
sql: sql,
|
||||
dbId: props.dbId as any,
|
||||
db: props.db as any,
|
||||
dbType: dbDialect.getInfo().formatSqlDialect,
|
||||
flowProcdef: props.flowProcdef,
|
||||
dbType: dbDialect.value.getInfo().formatSqlDialect,
|
||||
runSuccessCallback: () => {
|
||||
emit('submit-sql', { tableName: state.tableData.tableName });
|
||||
// cancel();
|
||||
@@ -371,11 +370,11 @@ const filterChangedData = (oldArr: object[], nowArr: object[], key: string): { d
|
||||
return data;
|
||||
}
|
||||
|
||||
let oldMap = {},
|
||||
newMap = {};
|
||||
oldArr.forEach((a) => (oldMap[a[key]] = a));
|
||||
let oldMap: any = {},
|
||||
newMap: any = {};
|
||||
oldArr.forEach((a: any) => (oldMap[a[key]] = a));
|
||||
|
||||
nowArr.forEach((a) => {
|
||||
nowArr.forEach((a: any) => {
|
||||
let k = a[key];
|
||||
newMap[k] = a;
|
||||
// 取oldName,因为修改了name,但是oldName不会变
|
||||
@@ -388,7 +387,7 @@ const filterChangedData = (oldArr: object[], nowArr: object[], key: string): { d
|
||||
}
|
||||
});
|
||||
|
||||
oldArr.forEach((a) => {
|
||||
oldArr.forEach((a: any) => {
|
||||
let k = a[key];
|
||||
let newData = newMap[k];
|
||||
if (!newData) {
|
||||
@@ -415,21 +414,22 @@ const genSql = () => {
|
||||
let data = state.tableData;
|
||||
// 创建表
|
||||
if (!props.data?.edit) {
|
||||
let createTable = dbDialect.getCreateTableSql(data);
|
||||
let createTable = dbDialect.value.getCreateTableSql(data);
|
||||
let createIndex = '';
|
||||
if (data.indexs.res.length > 0) {
|
||||
createIndex = dbDialect.getCreateIndexSql(data);
|
||||
createIndex = dbDialect.value.getCreateIndexSql(data);
|
||||
}
|
||||
return createTable + ';' + createIndex;
|
||||
} else {
|
||||
// 修改列
|
||||
let changeColData = filterChangedData(state.tableData.fields.oldFields, state.tableData.fields.res, 'name');
|
||||
let colSql = changeColData.changed ? dbDialect.getModifyColumnSql(data, data.tableName, changeColData) : '';
|
||||
let colSql = changeColData.changed ? dbDialect.value.getModifyColumnSql(data, data.tableName, changeColData) : '';
|
||||
// 修改索引
|
||||
let changeIdxData = filterChangedData(state.tableData.indexs.oldIndexs, state.tableData.indexs.res, 'indexName');
|
||||
let idxSql = changeIdxData.changed ? dbDialect.getModifyIndexSql(data, data.tableName, changeIdxData) : '';
|
||||
let idxSql = changeIdxData.changed ? dbDialect.value.getModifyIndexSql(data, data.tableName, changeIdxData) : '';
|
||||
// 修改表名,表注释
|
||||
let tableInfoSql = data.tableName !== data.oldTableName || data.tableComment !== data.oldTableComment ? dbDialect.getModifyTableInfoSql(data) : '';
|
||||
let tableInfoSql =
|
||||
data.tableName !== data.oldTableName || data.tableComment !== data.oldTableComment ? dbDialect.value.getModifyTableInfoSql(data) : '';
|
||||
|
||||
let sqlArr = [];
|
||||
colSql && sqlArr.push(colSql);
|
||||
|
||||
@@ -109,7 +109,6 @@
|
||||
:dbId="dbId"
|
||||
:db="db"
|
||||
:dbType="dbType"
|
||||
:flow-procdef="props.flowProcdef"
|
||||
:data="tableCreateDialog.data"
|
||||
v-model:visible="tableCreateDialog.visible"
|
||||
@submit-sql="onSubmitSql"
|
||||
@@ -131,6 +130,7 @@ 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';
|
||||
import { fuzzyMatchField } from '@/common/utils/string';
|
||||
|
||||
const DbTableOp = defineAsyncComponent(() => import('./DbTableOp.vue'));
|
||||
|
||||
@@ -151,9 +151,6 @@ const props = defineProps({
|
||||
type: [String],
|
||||
required: true,
|
||||
},
|
||||
flowProcdef: {
|
||||
type: [Object],
|
||||
},
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
@@ -219,17 +216,11 @@ const filterTableInfos = computed(() => {
|
||||
if (!tableNameSearch && !tableCommentSearch) {
|
||||
return tables;
|
||||
}
|
||||
return tables.filter((data: any) => {
|
||||
let tnMatch = true;
|
||||
let tcMatch = true;
|
||||
if (tableNameSearch) {
|
||||
tnMatch = data.tableName.toLowerCase().includes(tableNameSearch.toLowerCase());
|
||||
}
|
||||
if (tableCommentSearch) {
|
||||
tcMatch = data.tableComment.includes(tableCommentSearch);
|
||||
}
|
||||
return tnMatch && tcMatch;
|
||||
});
|
||||
|
||||
if (tableNameSearch) {
|
||||
return fuzzyMatchField(tableNameSearch, tables, (table: any) => table.tableName);
|
||||
}
|
||||
return fuzzyMatchField(tableCommentSearch, tables, (table: any) => table.tableComment);
|
||||
});
|
||||
|
||||
const getTables = async () => {
|
||||
@@ -317,7 +308,6 @@ const dropTable = async (row: any) => {
|
||||
sql: `DROP TABLE ${tableName}`,
|
||||
dbId: props.dbId as any,
|
||||
db: props.db as any,
|
||||
flowProcdef: props.flowProcdef,
|
||||
runSuccessCallback: async () => {
|
||||
await getTables();
|
||||
},
|
||||
|
||||
@@ -9,6 +9,7 @@ import { registerCompletionItemProvider } from '@/components/monaco/completionIt
|
||||
import { DbDialect, EditorCompletionItem, getDbDialect } from './dialect';
|
||||
import { type RemovableRef, useLocalStorage } from '@vueuse/core';
|
||||
import { DbGetDbNamesMode } from './enums';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
const hintsStorage: RemovableRef<Map<string, any>> = useLocalStorage('db-table-hints', new Map());
|
||||
const tableStorage: RemovableRef<Map<string, any>> = useLocalStorage('db-tables', new Map());
|
||||
@@ -41,11 +42,8 @@ export class DbInst {
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* 流程定义,若存在则需要审批执行
|
||||
*/
|
||||
flowProcdef: any;
|
||||
|
||||
/** 兼容版本 */
|
||||
version: string;
|
||||
/**
|
||||
* dbName -> db
|
||||
*/
|
||||
@@ -226,12 +224,18 @@ export class DbInst {
|
||||
* @param remark 执行备注
|
||||
*/
|
||||
async runSql(dbName: string, sql: string, remark: string = '') {
|
||||
return await dbApi.sqlExec.request({
|
||||
const res = await dbApi.sqlExec.request({
|
||||
id: this.id,
|
||||
db: dbName,
|
||||
sql: sql.trim(),
|
||||
remark,
|
||||
});
|
||||
for (let re of res) {
|
||||
if (re.errorMsg) {
|
||||
ElMessage.error(`${re.sql} -> 执行失败: ${re.errorMsg}`);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -311,7 +315,7 @@ export class DbInst {
|
||||
* @param columnValue 要更新的列以及对应的值 field->columnName; value->columnValue
|
||||
* @param rowData 表的一行完整数据(需要获取主键信息)
|
||||
*/
|
||||
async genUpdateSql(dbName: string, table: string, columnValue: {}, rowData: {}) {
|
||||
async genUpdateSql(dbName: string, table: string, columnValue: any, rowData: any) {
|
||||
let schema = '';
|
||||
let dbArr = dbName.split('/');
|
||||
if (dbArr.length == 2) {
|
||||
@@ -360,7 +364,6 @@ export class DbInst {
|
||||
dbType: this.getDialect().getInfo().formatSqlDialect,
|
||||
runSuccessCallback: successFunc,
|
||||
cancelCallback: cancelFunc,
|
||||
flowProcdef: this.flowProcdef,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -378,17 +381,17 @@ export class DbInst {
|
||||
* @param inst 数据库实例,后端返回的列表接口中的信息
|
||||
* @returns DbInst
|
||||
*/
|
||||
static getOrNewInst(inst: any) {
|
||||
static async getOrNewInst(inst: any) {
|
||||
if (!inst) {
|
||||
throw new Error('inst不能为空');
|
||||
}
|
||||
let dbInst = dbInstCache.get(inst.id);
|
||||
if (dbInst) {
|
||||
// 更新可能更改的流程定义
|
||||
if (inst.flowProcdef !== undefined) {
|
||||
dbInst.flowProcdef = inst.flowProcdef;
|
||||
dbInstCache.set(dbInst.id, dbInst);
|
||||
// 可能同一个库关联多个标签,展示需要
|
||||
if (inst.tagPath) {
|
||||
dbInst.tagPath = inst.tagPath;
|
||||
}
|
||||
|
||||
return dbInst;
|
||||
}
|
||||
console.info(`new dbInst: ${inst.id}, tagPath: ${inst.tagPath}`);
|
||||
@@ -399,7 +402,10 @@ export class DbInst {
|
||||
dbInst.name = inst.name;
|
||||
dbInst.type = inst.type;
|
||||
dbInst.databases = inst.databases;
|
||||
dbInst.flowProcdef = inst.flowProcdef;
|
||||
|
||||
if (dbInst.databases?.[0]) {
|
||||
dbInst.version = await dbApi.getCompatibleDbVersion.request({ id: inst.id, db: dbInst.databases?.[0] });
|
||||
}
|
||||
|
||||
dbInstCache.set(dbInst.id, dbInst);
|
||||
return dbInst;
|
||||
@@ -408,7 +414,6 @@ export class DbInst {
|
||||
/**
|
||||
* 获取数据库实例id,若不存在,则新建一个并缓存
|
||||
* @param dbId 数据库实例id
|
||||
* @param dbType 第一次获取时为必传项,即第一次创建时
|
||||
* @returns 数据库实例
|
||||
*/
|
||||
static getInst(dbId?: number): DbInst {
|
||||
@@ -419,7 +424,26 @@ export class DbInst {
|
||||
if (dbInst) {
|
||||
return dbInst;
|
||||
}
|
||||
throw new Error('dbInst不存在! 请在合适调用点使用DbInst.newInst()新建该实例');
|
||||
throw new Error('dbInst不存在! 请在合适调用点使用DbInst.getInstA()新建该实例');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据库实例信息,若不存在,调接口获取数据库信息
|
||||
* @param dbId 数据库id
|
||||
* @returns
|
||||
*/
|
||||
static async getInstA(dbId?: number): Promise<DbInst> {
|
||||
if (!dbId) {
|
||||
throw new Error('dbId不能为空');
|
||||
}
|
||||
let dbInst = dbInstCache.get(dbId);
|
||||
if (dbInst) {
|
||||
return Promise.resolve(dbInst);
|
||||
}
|
||||
|
||||
const dbInfoRes = await dbApi.dbs.request({ id: dbId });
|
||||
const db = dbInfoRes.list[0];
|
||||
return Promise.resolve(DbInst.getOrNewInst(db));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -450,8 +474,8 @@ export class DbInst {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取列名称的长度 加上排序图标长度、abc为字段类型简称占位符
|
||||
const columnWidth: number = getTextWidth(prop + 'abc') + 23;
|
||||
// 获取列名称的长度 加上排序图标长度、abc为字段类型简称占位符、排序图标等
|
||||
const columnWidth: number = getTextWidth(prop + 'abc') + 10;
|
||||
// prop为该列的字段名(传字符串);tableData为该表格的数据源(传变量);
|
||||
if (!tableData || !tableData.length || tableData.length === 0 || tableData === undefined) {
|
||||
return columnWidth;
|
||||
@@ -471,7 +495,7 @@ export class DbInst {
|
||||
maxWidthText = nowText;
|
||||
}
|
||||
}
|
||||
const contentWidth: number = getTextWidth(maxWidthText) + 15;
|
||||
const contentWidth: number = getTextWidth(maxWidthText) + 3;
|
||||
const flexWidth: number = contentWidth > columnWidth ? contentWidth : columnWidth;
|
||||
return flexWidth > 500 ? 500 : flexWidth;
|
||||
};
|
||||
@@ -601,6 +625,11 @@ export class TabInfo {
|
||||
*/
|
||||
params: any;
|
||||
|
||||
/**
|
||||
* 组件ref
|
||||
*/
|
||||
componentRef: any;
|
||||
|
||||
getNowDbInst() {
|
||||
return DbInst.getInst(this.dbId);
|
||||
}
|
||||
@@ -644,7 +673,7 @@ export function registerDbCompletionItemProvider(dbId: number, db: string, dbs:
|
||||
triggerCharacters: ['.', ' '],
|
||||
provideCompletionItems: async (model: editor.ITextModel, position: Position): Promise<languages.CompletionList | null | undefined> => {
|
||||
let word = model.getWordUntilPosition(position);
|
||||
const dbInst = DbInst.getInst(dbId);
|
||||
const dbInst = await DbInst.getInstA(dbId);
|
||||
const { lineNumber, column } = position;
|
||||
const { startColumn, endColumn } = word;
|
||||
|
||||
@@ -837,3 +866,23 @@ function getTableName4SqlCtx(sql: string, alias: string = '', defaultDb: string)
|
||||
return tables.length > 0 ? tables[0] : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据库主题配置
|
||||
*/
|
||||
export const DbThemeConfig = {
|
||||
/**
|
||||
* 表数据表头是否显示备注
|
||||
*/
|
||||
showColumnComment: true,
|
||||
|
||||
/**
|
||||
* 是否自动定位至树节点
|
||||
*/
|
||||
locationTreeNode: true,
|
||||
|
||||
/**
|
||||
* 是否缓存表信息
|
||||
*/
|
||||
cacheTable: true,
|
||||
};
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { MysqlDialect } from './mysql_dialect';
|
||||
import { PostgresqlDialect } from './postgres_dialect';
|
||||
import { DMDialect } from '@/views/ops/db/dialect/dm_dialect';
|
||||
import { OracleDialect } from '@/views/ops/db/dialect/oracle_dialect';
|
||||
import { MariadbDialect } from '@/views/ops/db/dialect/mariadb_dialect';
|
||||
import { SqliteDialect } from '@/views/ops/db/dialect/sqlite_dialect';
|
||||
import { MssqlDialect } from '@/views/ops/db/dialect/mssql_dialect';
|
||||
import { GaussDialect } from '@/views/ops/db/dialect/gauss_dialect';
|
||||
import { KingbaseEsDialect } from '@/views/ops/db/dialect/kingbaseES_dialect';
|
||||
import { VastbaseDialect } from '@/views/ops/db/dialect/vastbase_dialect';
|
||||
import {MysqlDialect} from './mysql_dialect';
|
||||
import {PostgresqlDialect} from './postgres_dialect';
|
||||
import {DMDialect} from '@/views/ops/db/dialect/dm_dialect';
|
||||
import {OracleDialect} from '@/views/ops/db/dialect/oracle_dialect';
|
||||
import {MariadbDialect} from '@/views/ops/db/dialect/mariadb_dialect';
|
||||
import {SqliteDialect} from '@/views/ops/db/dialect/sqlite_dialect';
|
||||
import {MssqlDialect} from '@/views/ops/db/dialect/mssql_dialect';
|
||||
import {GaussDialect} from '@/views/ops/db/dialect/gauss_dialect';
|
||||
import {KingbaseEsDialect} from '@/views/ops/db/dialect/kingbaseES_dialect';
|
||||
import {VastbaseDialect} from '@/views/ops/db/dialect/vastbase_dialect';
|
||||
import {Oracle11Dialect} from "@/views/ops/db/dialect/oracle11_dialect";
|
||||
|
||||
export interface sqlColumnType {
|
||||
udtName: string;
|
||||
@@ -37,6 +38,7 @@ export interface IndexDefinition {
|
||||
indexType: string;
|
||||
indexComment?: string;
|
||||
}
|
||||
|
||||
export const commonCustomKeywords = ['GROUP BY', 'ORDER BY', 'LEFT JOIN', 'RIGHT JOIN', 'INNER JOIN', 'SELECT * FROM'];
|
||||
|
||||
export interface EditorCompletionItem {
|
||||
@@ -69,11 +71,11 @@ export enum DataType {
|
||||
}
|
||||
|
||||
/** 列数据类型角标 */
|
||||
export const ColumnTypeSubscript = {
|
||||
export const ColumnTypeSubscript: any = {
|
||||
/** 字符串 */
|
||||
string: 'abc',
|
||||
string: 'ab',
|
||||
/** 数字 */
|
||||
number: '123',
|
||||
number: '12',
|
||||
/** 日期 */
|
||||
date: 'icon-clock',
|
||||
/** 时间 */
|
||||
@@ -212,7 +214,11 @@ export interface DbDialect {
|
||||
* @param tableName 表名
|
||||
* @param changeData 改变信息
|
||||
*/
|
||||
getModifyColumnSql(tableData: any, tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string;
|
||||
getModifyColumnSql(tableData: any, tableName: string, changeData: {
|
||||
del: RowDefinition[];
|
||||
add: RowDefinition[];
|
||||
upd: RowDefinition[]
|
||||
}): string;
|
||||
|
||||
/**
|
||||
* 生成编辑索引sql
|
||||
@@ -249,17 +255,21 @@ export enum DuplicateStrategy {
|
||||
let mysqlDialect = new MysqlDialect();
|
||||
|
||||
let dbType2DialectMap: Map<string, DbDialect> = new Map();
|
||||
let dbType2DialectVersionMap: Map<string, DbDialect> = new Map();
|
||||
|
||||
export const registerDbDialect = (dbType: string, dd: DbDialect) => {
|
||||
dbType2DialectMap.set(dbType, dd);
|
||||
};
|
||||
export const registerDbDialectVersion = (dbType: string, dd: DbDialect) => {
|
||||
dbType2DialectVersionMap.set(dbType, dd);
|
||||
};
|
||||
|
||||
export const getDbDialectMap = () => {
|
||||
return dbType2DialectMap;
|
||||
};
|
||||
|
||||
export const getDbDialect = (dbType?: string): DbDialect => {
|
||||
return dbType2DialectMap.get(dbType!) || mysqlDialect;
|
||||
export const getDbDialect = (dbType: string, version = ''): DbDialect => {
|
||||
return dbType2DialectVersionMap.get(dbType + version) || dbType2DialectMap.get(dbType) || mysqlDialect;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -282,6 +292,7 @@ export const QuoteEscape = (str: string): string => {
|
||||
registerDbDialect(DbType.gauss, new GaussDialect());
|
||||
registerDbDialect(DbType.dm, new DMDialect());
|
||||
registerDbDialect(DbType.oracle, new OracleDialect());
|
||||
registerDbDialectVersion(DbType.oracle + '11', new Oracle11Dialect()); // oracle 11g及以前版本的一些语法兼容
|
||||
registerDbDialect(DbType.sqlite, new SqliteDialect());
|
||||
registerDbDialect(DbType.mssql, new MssqlDialect());
|
||||
registerDbDialect(DbType.kingbaseEs, new KingbaseEsDialect());
|
||||
|
||||
55
mayfly_go_web/src/views/ops/db/dialect/oracle11_dialect.ts
Normal file
55
mayfly_go_web/src/views/ops/db/dialect/oracle11_dialect.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/** oracle 11g 及以前的版本的一些语法兼容 */
|
||||
import {OracleDialect} from '@/views/ops/db/dialect/oracle_dialect';
|
||||
import {DialectInfo, RowDefinition} from '@/views/ops/db/dialect/index';
|
||||
|
||||
let oracle11DialectInfo: DialectInfo;
|
||||
|
||||
export class Oracle11Dialect extends OracleDialect {
|
||||
|
||||
getInfo(): DialectInfo {
|
||||
if (oracle11DialectInfo) {
|
||||
return oracle11DialectInfo;
|
||||
}
|
||||
|
||||
oracle11DialectInfo = {} as DialectInfo;
|
||||
Object.assign(oracle11DialectInfo, super.getInfo());
|
||||
oracle11DialectInfo.name = 'Oracle11x';
|
||||
return oracle11DialectInfo;
|
||||
}
|
||||
|
||||
// 重写创建自增列sql
|
||||
|
||||
genColumnBasicSql(cl: RowDefinition, create: boolean, data = {}): string {
|
||||
let length = this.getTypeLengthSql(cl);
|
||||
// 默认值
|
||||
let defVal = this.getDefaultValueSql(cl, false, data);
|
||||
// 忽略自增配置,11g不支持直接设置自增列,需要单独设置自增序列
|
||||
// 如果有原名以原名为准
|
||||
let name = cl.oldName && cl.name !== cl.oldName ? cl.oldName : cl.name;
|
||||
let baseSql = ` ${this.quoteIdentifier(name)} ${cl.type}${length}`;
|
||||
return ` ${baseSql} ${defVal} ${cl.notNull ? 'NOT NULL' : ''} `;
|
||||
}
|
||||
|
||||
getDefaultValueSql(cl: RowDefinition, create?: boolean, data?: any): string {
|
||||
if (cl.value) {
|
||||
return ` DEFAULT ${cl.value}`;
|
||||
} else if (cl.auto_increment) {
|
||||
return ` DEFAULT ${data.tableName}_${cl.name}_SEQ.NEXTVAL`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
getOtherCreateTableSql(data: any): string {
|
||||
// 通过字段自增信息创建自增序列
|
||||
|
||||
let result = '';
|
||||
data.fields.res.forEach((field: RowDefinition) => {
|
||||
let seqName = `${data.tableName}_${field.name}_SEQ`;
|
||||
if (field.auto_increment) {
|
||||
result += `CREATE SEQUENCE ${seqName} START WITH 1 INCREMENT BY 1 CACHE 20`;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,8 @@ import {
|
||||
DuplicateStrategy,
|
||||
EditorCompletion,
|
||||
EditorCompletionItem,
|
||||
QuoteEscape,
|
||||
IndexDefinition,
|
||||
QuoteEscape,
|
||||
RowDefinition,
|
||||
sqlColumnType,
|
||||
} from './index';
|
||||
@@ -85,10 +85,10 @@ const replaceFunctions: EditorCompletionItem[] = [
|
||||
{ label: 'CURRENT_DATE', insertText: 'CURRENT_DATE', description: '获取当前日期' },
|
||||
{ label: 'CURRENT_TIMESTAMP', insertText: 'TIMESTAMP', description: '获取当前时间' },
|
||||
// 转换函数
|
||||
{ label: 'TO_CHAR', insertText: 'TO_CHAR(d|n[,fmt])', description: '把日期和数字转换为制定格式的字符串' },
|
||||
{ label: 'TO_CHAR', insertText: `TO_CHAR(d|n, 'yyyy-MM-dd HH24:mi:ss')`, description: '把日期和数字转换为制定格式的字符串' },
|
||||
{ label: 'TO_DATE', insertText: `TO_DATE(X, 'yyyy-MM-dd HH24:mi:ss')`, description: '把一个字符串以fmt格式转换成一个日期类型' },
|
||||
{ label: 'TO_NUMBER', insertText: 'TO_NUMBER(X,[,fmt])', description: '把一个字符串以fmt格式转换为一个数字' },
|
||||
{ label: 'TO_TIMESTAMP', insertText: 'TO_TIMESTAMP(X,[,fmt])', description: '把一个字符串以fmt格式转换为日期类型' },
|
||||
{ label: 'TO_NUMBER', insertText: `TO_NUMBER(X, 'yyyy-MM-dd HH24:mi:ss')`, description: '把一个字符串以fmt格式转换为一个数字' },
|
||||
{ label: 'TO_TIMESTAMP', insertText: `TO_TIMESTAMP(X, 'yyyy-MM-dd HH24:mi:ss.ff')`, description: '把一个字符串以fmt格式转换为日期类型' },
|
||||
// 其他
|
||||
{ label: 'NVL', insertText: 'NVL(X,VALUE)', description: '如果X为空,返回value,否则返回X' },
|
||||
{ label: 'NVL2', insertText: 'NVL2(x,value1,value2)', description: '如果x非空,返回value1,否则返回value2' },
|
||||
@@ -293,7 +293,7 @@ class OracleDialect implements DbDialect {
|
||||
return '';
|
||||
}
|
||||
|
||||
genColumnBasicSql(cl: RowDefinition, create: boolean): string {
|
||||
genColumnBasicSql(cl: RowDefinition, create: boolean, data = {}): string {
|
||||
let length = this.getTypeLengthSql(cl);
|
||||
// 默认值
|
||||
let defVal = this.getDefaultValueSql(cl);
|
||||
@@ -309,6 +309,11 @@ class OracleDialect implements DbDialect {
|
||||
return incr ? baseSql : ` ${baseSql} ${defVal} ${cl.notNull ? 'NOT NULL' : ''} `;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars
|
||||
getOtherCreateTableSql(data: any) {
|
||||
return '';
|
||||
}
|
||||
|
||||
getCreateTableSql(data: any): string {
|
||||
let schemaArr = data.db.split('/');
|
||||
let schema = schemaArr.length > 1 ? schemaArr[schemaArr.length - 1] : schemaArr[0];
|
||||
@@ -322,7 +327,7 @@ class OracleDialect implements DbDialect {
|
||||
// 创建表结构
|
||||
let fields: string[] = [];
|
||||
data.fields.res.forEach((item: any) => {
|
||||
item.name && fields.push(this.genColumnBasicSql(item, true));
|
||||
item.name && fields.push(this.genColumnBasicSql(item, true, data));
|
||||
// 列注释
|
||||
if (item.remark) {
|
||||
columCommentSql += ` COMMENT ON COLUMN ${dbTable}.${this.quoteIdentifier(item.name)} is '${QuoteEscape(item.remark)}'; `;
|
||||
@@ -344,7 +349,9 @@ class OracleDialect implements DbDialect {
|
||||
tableCommentSql = ` COMMENT ON TABLE ${dbTable} is '${QuoteEscape(data.tableComment)}'; `;
|
||||
}
|
||||
|
||||
return createSql + tableCommentSql + columCommentSql;
|
||||
// 其余建表信息,如:自增字段在老版本的使用方式是创建自增序列
|
||||
let other = this.getOtherCreateTableSql(data);
|
||||
return createSql + tableCommentSql + columCommentSql + other;
|
||||
}
|
||||
|
||||
getCreateIndexSql(tableData: any): string {
|
||||
@@ -391,7 +398,7 @@ class OracleDialect implements DbDialect {
|
||||
commentArr.push(commentSql);
|
||||
}
|
||||
}
|
||||
modifyArr.push(` MODIFY (${this.genColumnBasicSql(a, false)})`);
|
||||
modifyArr.push(` MODIFY (${this.genColumnBasicSql(a, false, tableData)})`);
|
||||
if (a.pri) {
|
||||
priArr.add(`${this.quoteIdentifier(a.name)}`);
|
||||
}
|
||||
@@ -400,7 +407,7 @@ class OracleDialect implements DbDialect {
|
||||
|
||||
if (changeData.add.length > 0) {
|
||||
changeData.add.forEach((a) => {
|
||||
modifyArr.push(` ADD (${this.genColumnBasicSql(a, false)})`);
|
||||
modifyArr.push(` ADD (${this.genColumnBasicSql(a, false, tableData)})`);
|
||||
if (a.remark) {
|
||||
commentArr.push(`COMMENT ON COLUMN ${dbTable}.${this.quoteIdentifier(a.name)} is '${QuoteEscape(a.remark)}'`);
|
||||
}
|
||||
|
||||
@@ -79,7 +79,11 @@ const functions: EditorCompletionItem[] = [
|
||||
{ label: 'sign', insertText: 'sign(X)', description: '返回数字符号 1正 -1负 0零 null' },
|
||||
{ label: 'soundex', insertText: 'soundex(X)', description: '返回字符串X的soundex编码字符串' },
|
||||
{ label: 'sqlite_compileoption_get', insertText: 'sqlite_compileoption_get(N)', description: '获取指定编译选项的值' },
|
||||
{ label: 'sqlite_compileoption_used', insertText: 'sqlite_compileoption_used(X)', description: '检查SQLite编译时是否使用了指定的编译选项' },
|
||||
{
|
||||
label: 'sqlite_compileoption_used',
|
||||
insertText: 'sqlite_compileoption_used(X)',
|
||||
description: '检查SQLite编译时是否使用了指定的编译选项',
|
||||
},
|
||||
{ label: 'sqlite_source_id', insertText: 'sqlite_source_id()', description: '获取sqlite源代码标识符' },
|
||||
{ label: 'sqlite_version', insertText: 'sqlite_version()', description: '获取sqlite版本' },
|
||||
{ label: 'substr', insertText: 'substr(X,Y[,Z])', description: '截取字符串' },
|
||||
@@ -98,12 +102,21 @@ const functions: EditorCompletionItem[] = [
|
||||
{ label: 'sum', insertText: 'sum(X)', description: '返回分组中非空值的总和。' },
|
||||
{ label: 'total', insertText: 'total(X)', description: '返回YYYY-MM-DD格式的字符串' },
|
||||
{ label: 'date', insertText: 'date(time-value[, modifier, ...])', description: '返回HH:MM:SS格式的字符串' },
|
||||
{ label: 'time', insertText: 'time(time-value[, modifier, ...])', description: '将日期和时间字符串转换为特定的日期和时间格式' },
|
||||
{
|
||||
label: 'time',
|
||||
insertText: 'time(time-value[, modifier, ...])',
|
||||
description: '将日期和时间字符串转换为特定的日期和时间格式',
|
||||
},
|
||||
{ label: 'datetime', insertText: 'datetime(time-value[, modifier, ...])', description: '计算日期和时间的儒略日数' },
|
||||
{ label: 'julianday', insertText: 'julianday(time-value[, modifier, ...])', description: '将日期和时间格式化为指定的字符串' },
|
||||
{
|
||||
label: 'julianday',
|
||||
insertText: 'julianday(time-value[, modifier, ...])',
|
||||
description: '将日期和时间格式化为指定的字符串',
|
||||
},
|
||||
];
|
||||
|
||||
let sqliteDialectInfo: DialectInfo;
|
||||
|
||||
class SqliteDialect implements DbDialect {
|
||||
getInfo(): DialectInfo {
|
||||
if (sqliteDialectInfo) {
|
||||
@@ -124,7 +137,7 @@ class SqliteDialect implements DbDialect {
|
||||
};
|
||||
|
||||
sqliteDialectInfo = {
|
||||
name: 'Sqlite',
|
||||
name: 'Sqlite3',
|
||||
icon: 'iconfont icon-sqlite',
|
||||
defaultPort: 0,
|
||||
formatSqlDialect: 'sql',
|
||||
@@ -135,10 +148,8 @@ class SqliteDialect implements DbDialect {
|
||||
}
|
||||
|
||||
getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
|
||||
return `SELECT * FROM ${this.quoteIdentifier(table)} ${condition ? 'WHERE ' + condition : ''} ${orderBy ? orderBy : ''} ${this.getPageSql(
|
||||
pageNum,
|
||||
limit
|
||||
)};`;
|
||||
return `SELECT *
|
||||
FROM ${this.quoteIdentifier(table)} ${condition ? 'WHERE ' + condition : ''} ${orderBy ? orderBy : ''} ${this.getPageSql(pageNum, limit)};`;
|
||||
}
|
||||
|
||||
getPageSql(pageNum: number, limit: number) {
|
||||
@@ -147,8 +158,28 @@ class SqliteDialect implements DbDialect {
|
||||
|
||||
getDefaultRows(): RowDefinition[] {
|
||||
return [
|
||||
{ name: 'id', type: 'integer', length: '', numScale: '', value: '', notNull: true, pri: true, auto_increment: true, remark: '主键ID' },
|
||||
{ name: 'creator_id', type: 'bigint', length: '20', numScale: '', value: '', notNull: true, pri: false, auto_increment: false, remark: '创建人id' },
|
||||
{
|
||||
name: 'id',
|
||||
type: 'integer',
|
||||
length: '',
|
||||
numScale: '',
|
||||
value: '',
|
||||
notNull: true,
|
||||
pri: true,
|
||||
auto_increment: true,
|
||||
remark: '主键ID',
|
||||
},
|
||||
{
|
||||
name: 'creator_id',
|
||||
type: 'bigint',
|
||||
length: '20',
|
||||
numScale: '',
|
||||
value: '',
|
||||
notNull: true,
|
||||
pri: false,
|
||||
auto_increment: false,
|
||||
remark: '创建人id',
|
||||
},
|
||||
{
|
||||
name: 'creator',
|
||||
type: 'varchar',
|
||||
@@ -171,8 +202,28 @@ class SqliteDialect implements DbDialect {
|
||||
auto_increment: false,
|
||||
remark: '创建时间',
|
||||
},
|
||||
{ name: 'updator_id', type: 'bigint', length: '20', numScale: '', value: '', notNull: true, pri: false, auto_increment: false, remark: '修改人id' },
|
||||
{ name: 'updator', type: 'varchar', length: '100', numScale: '', value: '', notNull: true, pri: false, auto_increment: false, remark: '修改姓名' },
|
||||
{
|
||||
name: 'updator_id',
|
||||
type: 'bigint',
|
||||
length: '20',
|
||||
numScale: '',
|
||||
value: '',
|
||||
notNull: true,
|
||||
pri: false,
|
||||
auto_increment: false,
|
||||
remark: '修改人id',
|
||||
},
|
||||
{
|
||||
name: 'updator',
|
||||
type: 'varchar',
|
||||
length: '100',
|
||||
numScale: '',
|
||||
value: '',
|
||||
notNull: true,
|
||||
pri: false,
|
||||
auto_increment: false,
|
||||
remark: '修改姓名',
|
||||
},
|
||||
{
|
||||
name: 'update_time',
|
||||
type: 'datetime',
|
||||
@@ -211,6 +262,7 @@ class SqliteDialect implements DbDialect {
|
||||
}
|
||||
return ` ${this.quoteIdentifier(cl.name)} ${cl.type}${length} ${nullAble} ${defVal} `;
|
||||
}
|
||||
|
||||
getCreateTableSql(data: any): string {
|
||||
// 创建表结构
|
||||
let fields: string[] = [];
|
||||
@@ -219,7 +271,9 @@ class SqliteDialect implements DbDialect {
|
||||
});
|
||||
|
||||
return `CREATE TABLE ${this.quoteIdentifier(data.db)}.${this.quoteIdentifier(data.tableName)}
|
||||
( ${fields.join(',')} )`;
|
||||
(
|
||||
${fields.join(',')}
|
||||
)`;
|
||||
}
|
||||
|
||||
getCreateIndexSql(data: any): string {
|
||||
@@ -227,13 +281,30 @@ class SqliteDialect implements DbDialect {
|
||||
let sql = [] as string[];
|
||||
data.indexs.res.forEach((a: any) => {
|
||||
sql.push(
|
||||
`CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${this.quoteIdentifier(data.db)}.${this.quoteIdentifier(a.indexName)} ON "${data.tableName}" (${a.columnNames.join(',')})`
|
||||
`CREATE
|
||||
${a.unique ? 'UNIQUE' : ''} INDEX
|
||||
${this.quoteIdentifier(data.db)}
|
||||
.
|
||||
${this.quoteIdentifier(a.indexName)}
|
||||
ON
|
||||
"${data.tableName}"
|
||||
(
|
||||
${a.columnNames.join(',')}
|
||||
)`
|
||||
);
|
||||
});
|
||||
return sql.join(';');
|
||||
}
|
||||
|
||||
getModifyColumnSql(tableData: any, tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
|
||||
getModifyColumnSql(
|
||||
tableData: any,
|
||||
tableName: string,
|
||||
changeData: {
|
||||
del: RowDefinition[];
|
||||
add: RowDefinition[];
|
||||
upd: RowDefinition[];
|
||||
}
|
||||
): string {
|
||||
// sqlite修改表结构需要先删除再创建
|
||||
|
||||
// 1.删除旧表索引 DROP INDEX "main"."aa";
|
||||
@@ -270,16 +341,25 @@ class SqliteDialect implements DbDialect {
|
||||
});
|
||||
// 生成sql
|
||||
sql.push(
|
||||
`INSERT INTO ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(tableName)} (${insertFields.join(',')}) SELECT ${queryFields.join(
|
||||
','
|
||||
)} FROM ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(oldTableName)}`
|
||||
`INSERT INTO ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(tableName)} (${insertFields.join(',')})
|
||||
SELECT ${queryFields.join(',')}
|
||||
FROM ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(oldTableName)}`
|
||||
);
|
||||
|
||||
// 5.创建索引
|
||||
tableData.indexs.res.forEach((a: any) => {
|
||||
a.indexName &&
|
||||
sql.push(
|
||||
`CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(a.indexName)} ON "${tableName}" (${a.columnNames.join(',')})`
|
||||
`CREATE
|
||||
${a.unique ? 'UNIQUE' : ''} INDEX
|
||||
${this.quoteIdentifier(tableData.db)}
|
||||
.
|
||||
${this.quoteIdentifier(a.indexName)}
|
||||
ON
|
||||
"${tableName}"
|
||||
(
|
||||
${a.columnNames.join(',')}
|
||||
)`
|
||||
);
|
||||
});
|
||||
|
||||
@@ -308,7 +388,14 @@ class SqliteDialect implements DbDialect {
|
||||
|
||||
if (indexData.length > 0) {
|
||||
indexData.forEach((a) => {
|
||||
sql.push(`CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${this.quoteIdentifier(a.indexName)} ON ${tableName} (${a.columnNames.join(',')})`);
|
||||
sql.push(`CREATE
|
||||
${a.unique ? 'UNIQUE' : ''} INDEX
|
||||
${this.quoteIdentifier(a.indexName)}
|
||||
ON
|
||||
${tableName}
|
||||
(
|
||||
${a.columnNames.join(',')}
|
||||
)`);
|
||||
});
|
||||
}
|
||||
return sql.join(';');
|
||||
|
||||
@@ -11,6 +11,7 @@ export const DbSqlExecTypeEnum = {
|
||||
Delete: EnumValue.of(2, 'DELETE').setTagColor('#F9E2AE'),
|
||||
Insert: EnumValue.of(3, 'INSERT').setTagColor('#A8DEE0'),
|
||||
Query: EnumValue.of(4, 'QUERY').setTagColor('#A8DEE0'),
|
||||
Ddl: EnumValue.of(5, 'DDL').setTagColor('#F9E2AE'),
|
||||
Other: EnumValue.of(-1, 'OTHER').setTagColor('#F9E2AE'),
|
||||
};
|
||||
|
||||
@@ -42,3 +43,9 @@ export const DbTransferRunningStateEnum = {
|
||||
Fail: EnumValue.of(-1, '失败').setTagType('danger'),
|
||||
Stop: EnumValue.of(-2, '手动终止').setTagType('warning'),
|
||||
};
|
||||
|
||||
export const DbTransferFileStatusEnum = {
|
||||
Running: EnumValue.of(1, '执行中').setTagType('primary'),
|
||||
Success: EnumValue.of(2, '成功').setTagType('success'),
|
||||
Fail: EnumValue.of(-1, '失败').setTagType('danger'),
|
||||
};
|
||||
|
||||
@@ -20,16 +20,8 @@
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item prop="code" label="编号" required>
|
||||
<el-input
|
||||
:disabled="form.id"
|
||||
v-model.trim="form.code"
|
||||
placeholder="请输入编号 (大小写字母、数字、_-.:), 不可修改"
|
||||
auto-complete="off"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="name" label="名称" required>
|
||||
<el-input v-model.trim="form.name" placeholder="请输入机器别名" auto-complete="off"></el-input>
|
||||
<el-input v-model.trim="form.name" placeholder="请输入机器名称(不可重复)" auto-complete="off"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="protocol" label="协议" required>
|
||||
<el-radio-group v-model="form.protocol" @change="handleChangeProtocol">
|
||||
@@ -90,7 +82,6 @@ import ResourceAuthCertTableEdit from '../component/ResourceAuthCertTableEdit.vu
|
||||
import SshTunnelSelect from '../component/SshTunnelSelect.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({
|
||||
@@ -118,18 +109,18 @@ const rules = {
|
||||
trigger: ['change'],
|
||||
},
|
||||
],
|
||||
code: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入编码',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
{
|
||||
pattern: ResourceCodePattern.pattern,
|
||||
message: ResourceCodePattern.message,
|
||||
trigger: ['blur'],
|
||||
},
|
||||
],
|
||||
// code: [
|
||||
// {
|
||||
// required: true,
|
||||
// message: '请输入编码',
|
||||
// trigger: ['change', 'blur'],
|
||||
// },
|
||||
// {
|
||||
// pattern: ResourceCodePattern.pattern,
|
||||
// message: ResourceCodePattern.message,
|
||||
// trigger: ['blur'],
|
||||
// },
|
||||
// ],
|
||||
name: [
|
||||
{
|
||||
required: true,
|
||||
|
||||
@@ -278,10 +278,8 @@ const perms = {
|
||||
};
|
||||
|
||||
const searchItems = [
|
||||
SearchItem.input('keyword', '关键字').withPlaceholder('ip / 名称 / 编号'),
|
||||
getTagPathSearchItem(TagResourceTypeEnum.MachineAuthCert.value),
|
||||
SearchItem.input('code', '编号'),
|
||||
SearchItem.input('ip', 'IP'),
|
||||
SearchItem.input('name', '名称'),
|
||||
];
|
||||
|
||||
const columns = [
|
||||
@@ -298,7 +296,7 @@ const columns = [
|
||||
];
|
||||
|
||||
// 该用户拥有的的操作列按钮权限,使用v-if进行判断,v-auth对el-dropdown-item无效
|
||||
const actionBtns = hasPerms([perms.updateMachine]);
|
||||
const actionBtns: any = hasPerms([perms.updateMachine]);
|
||||
|
||||
const state = reactive({
|
||||
params: {
|
||||
|
||||
@@ -34,20 +34,15 @@
|
||||
|
||||
<Pane>
|
||||
<div class="machine-terminal-tabs card pd5">
|
||||
<el-tabs
|
||||
v-if="state.tabs.size > 0"
|
||||
type="card"
|
||||
@tab-remove="onRemoveTab"
|
||||
@tab-change="onTabChange"
|
||||
style="width: 100%"
|
||||
v-model="state.activeTermName"
|
||||
class="h100"
|
||||
>
|
||||
<el-tabs v-if="state.tabs.size > 0" type="card" @tab-remove="onRemoveTab" style="width: 100%" v-model="state.activeTermName" class="h100">
|
||||
<el-tab-pane class="h100" closable v-for="dt in state.tabs.values()" :label="dt.label" :name="dt.key" :key="dt.key">
|
||||
<template #label>
|
||||
<el-popconfirm @confirm="handleReconnect(dt, true)" title="确认重新连接?">
|
||||
<template #reference>
|
||||
<el-icon class="mr5" :color="dt.status == 1 ? '#67c23a' : '#f56c6c'" :title="dt.status == 1 ? '' : '点击重连'"
|
||||
<el-icon
|
||||
class="mr5"
|
||||
:color="EnumValue.getEnumByValue(TerminalStatusEnum, dt.status)?.extra?.iconColor"
|
||||
:title="dt.status == TerminalStatusEnum.Connected.value ? '' : '点击重连'"
|
||||
><Connection />
|
||||
</el-icon>
|
||||
</template>
|
||||
@@ -62,7 +57,7 @@
|
||||
<el-descriptions :column="1" size="small">
|
||||
<el-descriptions-item label="机器名"> {{ dt.params?.name }} </el-descriptions-item>
|
||||
<el-descriptions-item label="host"> {{ dt.params?.ip }} : {{ dt.params?.port }} </el-descriptions-item>
|
||||
<el-descriptions-item label="username"> {{ dt.params?.username }} </el-descriptions-item>
|
||||
<el-descriptions-item label="username"> {{ dt.params?.selectAuthCert.username }} </el-descriptions-item>
|
||||
<el-descriptions-item label="remark"> {{ dt.params?.remark }} </el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
@@ -165,13 +160,14 @@ import TagTree from '../component/TagTree.vue';
|
||||
import { Pane, Splitpanes } from 'splitpanes';
|
||||
import { ContextmenuItem } from '@/components/contextmenu/index';
|
||||
import TerminalBody from '@/components/terminal/TerminalBody.vue';
|
||||
import { TerminalStatus } from '@/components/terminal/common';
|
||||
import { TerminalStatus, TerminalStatusEnum } 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';
|
||||
import EnumValue from '@/common/Enum';
|
||||
|
||||
// 组件
|
||||
const ScriptManage = defineAsyncComponent(() => import('./ScriptManage.vue'));
|
||||
@@ -340,8 +336,13 @@ watch(
|
||||
watch(
|
||||
() => state.activeTermName,
|
||||
(newValue, oldValue) => {
|
||||
fitTerminal();
|
||||
|
||||
oldValue && terminalRefs[oldValue]?.blur && terminalRefs[oldValue]?.blur();
|
||||
terminalRefs[newValue]?.focus && terminalRefs[newValue]?.focus();
|
||||
|
||||
const nowTab = state.tabs.get(state.activeTermName);
|
||||
tagTreeRef.value.setCurrentKey(nowTab?.authCert);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -496,20 +497,27 @@ const onRemoveTab = (targetName: string) => {
|
||||
if (tabName !== targetName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
state.tabs.delete(targetName);
|
||||
let info = state.tabs.get(targetName);
|
||||
if (info) {
|
||||
terminalRefs[info.key]?.close();
|
||||
}
|
||||
|
||||
if (activeTermName != targetName) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 如果删除的tab是当前激活的tab,则切换到前一个或后一个tab
|
||||
const nextTab = tabNames[i + 1] || tabNames[i - 1];
|
||||
if (nextTab) {
|
||||
activeTermName = nextTab;
|
||||
} else {
|
||||
activeTermName = '';
|
||||
}
|
||||
let info = state.tabs.get(targetName);
|
||||
if (info) {
|
||||
terminalRefs[info.key]?.close();
|
||||
}
|
||||
|
||||
state.tabs.delete(targetName);
|
||||
state.activeTermName = activeTermName;
|
||||
onTabChange();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -535,21 +543,13 @@ const onResizeTagTree = () => {
|
||||
fitTerminal();
|
||||
};
|
||||
|
||||
const onTabChange = () => {
|
||||
fitTerminal();
|
||||
|
||||
const nowTab = state.tabs.get(state.activeTermName);
|
||||
tagTreeRef.value.setCurrentKey(nowTab?.authCert);
|
||||
};
|
||||
|
||||
const fitTerminal = () => {
|
||||
setTimeout(() => {
|
||||
let info = state.tabs.get(state.activeTermName);
|
||||
if (info) {
|
||||
terminalRefs[info.key]?.fitTerminal && terminalRefs[info.key]?.fitTerminal();
|
||||
terminalRefs[info.key]?.focus && terminalRefs[info.key]?.focus();
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
};
|
||||
|
||||
const handleReconnect = (tab: any, force = false) => {
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
@open="getTermOps()"
|
||||
>
|
||||
<page-table ref="pageTableRef" :page-api="machineApi.termOpRecs" :lazy="true" height="100%" v-model:query-form="query" :columns="columns">
|
||||
<template #fileKey="{ data }">
|
||||
<FileInfo :fileKey="data.fileKey" />
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
<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>
|
||||
@@ -49,6 +53,8 @@ import 'asciinema-player/dist/bundle/asciinema-player.css';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { formatDate } from '@/common/utils/format';
|
||||
import { getFileUrl } from '@/common/request';
|
||||
import FileInfo from '@/components/file/FileInfo.vue';
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean },
|
||||
@@ -62,7 +68,7 @@ const columns = [
|
||||
TableColumn.new('creator', '操作者').setMinWidth(120),
|
||||
TableColumn.new('createTime', '开始时间').isTime().setMinWidth(150),
|
||||
TableColumn.new('endTime', '结束时间').isTime().setMinWidth(150),
|
||||
TableColumn.new('recordFilePath', '文件路径').setMinWidth(200),
|
||||
TableColumn.new('fileKey', '文件').isSlot(),
|
||||
TableColumn.new('action', '操作').isSlot().setMinWidth(120).fixedRight().alignCenter(),
|
||||
];
|
||||
|
||||
@@ -109,14 +115,9 @@ const playRec = async (rec: any) => {
|
||||
player.dispose();
|
||||
}
|
||||
rec.playRecLoding = true;
|
||||
const content = await machineApi.termOpRec.request({
|
||||
recId: rec.id,
|
||||
id: rec.machineId,
|
||||
});
|
||||
|
||||
state.playerDialogVisible = true;
|
||||
nextTick(() => {
|
||||
player = AsciinemaPlayer.create(`data:text/plain;base64,${content}`, playerRef.value, {
|
||||
player = AsciinemaPlayer.create(getFileUrl(rec.fileKey), playerRef.value, {
|
||||
autoPlay: true,
|
||||
speed: 1.0,
|
||||
idleTimeLimit: 2,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { joinClientParams } from '@/common/request';
|
||||
export const machineApi = {
|
||||
// 获取权限列表
|
||||
list: Api.newGet('/machines'),
|
||||
getByCodes: Api.newGet('/machines/simple'),
|
||||
tagList: Api.newGet('/machines/tags'),
|
||||
getMachinePwd: Api.newGet('/machines/{id}/pwd'),
|
||||
info: Api.newGet('/machines/{id}/sysinfo'),
|
||||
@@ -12,6 +13,8 @@ export const machineApi = {
|
||||
process: Api.newGet('/machines/{id}/process'),
|
||||
// 终止进程
|
||||
killProcess: Api.newDelete('/machines/{id}/process'),
|
||||
users: Api.newGet('/machines/{id}/users'),
|
||||
groups: Api.newGet('/machines/{id}/groups'),
|
||||
testConn: Api.newPost('/machines/test-conn'),
|
||||
// 保存按钮
|
||||
saveMachine: Api.newPost('/machines'),
|
||||
@@ -44,8 +47,6 @@ export const machineApi = {
|
||||
delConf: Api.newDelete('/machines/{machineId}/files/{id}'),
|
||||
// 机器终端操作记录列表
|
||||
termOpRecs: Api.newGet('/machines/{machineId}/term-recs'),
|
||||
// 机器终端操作记录详情
|
||||
termOpRec: Api.newGet('/machines/{id}/term-recs/{recId}'),
|
||||
};
|
||||
|
||||
export const cronJobApi = {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<el-dialog
|
||||
:title="title"
|
||||
v-model="dialogVisible"
|
||||
@open="search()"
|
||||
:close-on-click-modal="false"
|
||||
:before-close="cancel"
|
||||
:show-close="true"
|
||||
@@ -27,7 +28,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch, ref, toRefs, reactive, Ref } from 'vue';
|
||||
import { ref, toRefs, reactive, Ref } from 'vue';
|
||||
import { cronJobApi } from '../api';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
@@ -47,8 +48,6 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:visible', 'update:data', 'cancel']);
|
||||
|
||||
const searchItems = [SearchItem.input('machineCode', '机器编号'), SearchItem.select('status', '状态').withEnum(CronJobExecStatusEnum)];
|
||||
|
||||
const columns = ref([
|
||||
@@ -65,7 +64,7 @@ const state = reactive({
|
||||
tags: [] as any,
|
||||
params: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 8,
|
||||
cronJobId: 0,
|
||||
status: null,
|
||||
machineCode: '',
|
||||
@@ -78,24 +77,17 @@ const state = reactive({
|
||||
machines: [],
|
||||
});
|
||||
|
||||
const { dialogVisible, params } = toRefs(state);
|
||||
const { params } = toRefs(state);
|
||||
|
||||
watch(props, async (newValue: any) => {
|
||||
state.dialogVisible = newValue.visible;
|
||||
if (!newValue.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.params.cronJobId = props.data?.id;
|
||||
setTimeout(() => search(), 300);
|
||||
});
|
||||
const dialogVisible = defineModel<boolean>('visible');
|
||||
|
||||
const search = async () => {
|
||||
state.params.cronJobId = props.data?.id;
|
||||
pageTableRef.value.search();
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
emit('update:visible', false);
|
||||
dialogVisible.value = false;
|
||||
setTimeout(() => {
|
||||
initData();
|
||||
}, 500);
|
||||
|
||||
@@ -18,12 +18,12 @@
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="path" label="路径" min-width="150px" show-overflow-tooltip>
|
||||
<el-table-column prop="path" label="路径" min-width="180" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<el-input v-model="scope.row.path" :disabled="scope.row.id != null" clearable> </el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-wdith="120px">
|
||||
<el-table-column label="操作" min-width="130">
|
||||
<template #default="scope">
|
||||
<el-button v-if="scope.row.id == null" @click="addFiles(scope.row)" type="success" icon="success-filled" plain></el-button>
|
||||
<el-button v-if="scope.row.id != null" @click="getConf(scope.row)" type="primary" icon="tickets" plain></el-button>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
>
|
||||
<el-table-column type="selection" width="30" />
|
||||
|
||||
<el-table-column prop="name" label="名称">
|
||||
<el-table-column prop="name" label="名称" min-width="380">
|
||||
<template #header>
|
||||
<div class="machine-file-table-header">
|
||||
<div>
|
||||
@@ -171,7 +171,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="size" label="大小" width="100" sortable>
|
||||
<el-table-column prop="size" label="大小" min-width="90" sortable>
|
||||
<template #default="scope">
|
||||
<span style="color: #67c23a; font-weight: bold" v-if="scope.row.type == '-'"> {{ formatByteSize(scope.row.size) }} </span>
|
||||
<span style="color: #67c23a; font-weight: bold" v-if="scope.row.type == 'd' && scope.row.dirSize"> {{ scope.row.dirSize }} </span>
|
||||
@@ -182,7 +182,11 @@
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="mode" label="属性" width="110"> </el-table-column>
|
||||
<el-table-column prop="modTime" label="修改时间" width="165" sortable> </el-table-column>
|
||||
<el-table-column v-if="$props.protocol == MachineProtocolEnum.Ssh.value" prop="username" label="用户" min-width="55" show-overflow-tooltip>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="$props.protocol == MachineProtocolEnum.Ssh.value" prop="groupname" label="组" min-width="55" show-overflow-tooltip>
|
||||
</el-table-column>
|
||||
<el-table-column prop="modTime" label="修改时间" width="160" sortable> </el-table-column>
|
||||
|
||||
<el-table-column width="100">
|
||||
<template #header>
|
||||
@@ -288,6 +292,8 @@ import MachineFileContent from './MachineFileContent.vue';
|
||||
import { getToken } from '@/common/utils/storage';
|
||||
import { convertToBytes, formatByteSize } from '@/common/utils/format';
|
||||
import { getMachineConfig } from '@/common/sysconfig';
|
||||
import { MachineProtocolEnum } from '../enums';
|
||||
import { fuzzyMatchField } from '@/common/utils/string';
|
||||
|
||||
const props = defineProps({
|
||||
machineId: { type: Number },
|
||||
@@ -303,6 +309,9 @@ const folderUploadRef: any = ref();
|
||||
|
||||
const folderType = 'd';
|
||||
|
||||
const userMap = new Map<number, any>();
|
||||
const groupMap = new Map<number, any>();
|
||||
|
||||
// 路径分隔符
|
||||
const pathSep = '/';
|
||||
|
||||
@@ -343,13 +352,27 @@ const { basePath, nowPath, loading, fileNameFilter, progressNum, uploadProgressS
|
||||
|
||||
onMounted(async () => {
|
||||
state.basePath = props.path;
|
||||
const machineId = props.machineId;
|
||||
|
||||
if (props.protocol == MachineProtocolEnum.Ssh.value) {
|
||||
machineApi.users.request({ id: machineId }).then((res: any) => {
|
||||
for (let user of res) {
|
||||
userMap.set(user.uid, user);
|
||||
}
|
||||
});
|
||||
|
||||
machineApi.groups.request({ id: machineId }).then((res: any) => {
|
||||
for (let group of res) {
|
||||
groupMap.set(group.gid, group);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setFiles(props.path);
|
||||
state.machineConfig = await getMachineConfig();
|
||||
});
|
||||
|
||||
const filterFiles = computed(() =>
|
||||
state.files.filter((data: any) => !state.fileNameFilter || data.name.toLowerCase().includes(state.fileNameFilter.toLowerCase()))
|
||||
);
|
||||
const filterFiles = computed(() => fuzzyMatchField(state.fileNameFilter, state.files, (file: any) => file.name));
|
||||
|
||||
const filePathNav = computed(() => {
|
||||
let basePath = state.basePath;
|
||||
@@ -517,6 +540,11 @@ const lsFile = async (path: string) => {
|
||||
path,
|
||||
});
|
||||
for (const file of res) {
|
||||
if (props.protocol == MachineProtocolEnum.Ssh.value) {
|
||||
file.username = userMap.get(file.uid)?.uname || file.uid;
|
||||
file.groupname = groupMap.get(file.gid)?.gname || file.gid;
|
||||
}
|
||||
|
||||
const type = file.type;
|
||||
if (type == folderType) {
|
||||
file.isFolder = true;
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column prop="codePaths" label="关联机器" min-width="250px" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<TagCodePath :path="scope.row.tags.map((tag: any) => tag.codePath)" />
|
||||
<TagCodePath :path="scope.row.tags?.map((tag: any) => tag.codePath)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" show-overflow-tooltip width="120px"> </el-table-column>
|
||||
@@ -173,7 +173,7 @@ const openFormDialog = (data: any) => {
|
||||
state.form = { ...DefaultForm };
|
||||
} else {
|
||||
state.form = _.cloneDeep(data);
|
||||
state.form.codePaths = data.tags.map((tag: any) => tag.codePath);
|
||||
state.form.codePaths = data.tags?.map((tag: any) => tag.codePath);
|
||||
}
|
||||
state.dialogVisible = true;
|
||||
};
|
||||
|
||||
@@ -17,14 +17,7 @@
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item prop="code" label="编号" required>
|
||||
<el-input
|
||||
:disabled="form.id"
|
||||
v-model.trim="form.code"
|
||||
placeholder="请输入编号 (大小写字母、数字、_-.:), 不可修改"
|
||||
auto-complete="off"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="name" label="名称" required>
|
||||
<el-input v-model.trim="form.name" placeholder="请输入名称" auto-complete="off"></el-input>
|
||||
</el-form-item>
|
||||
@@ -64,7 +57,6 @@ import { mongoApi } from './api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import TagTreeSelect from '../component/TagTreeSelect.vue';
|
||||
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
|
||||
import { ResourceCodePattern } from '@/common/pattern';
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
@@ -89,18 +81,6 @@ const rules = {
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
code: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入编码',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
{
|
||||
pattern: ResourceCodePattern.pattern,
|
||||
message: ResourceCodePattern.message,
|
||||
trigger: ['blur'],
|
||||
},
|
||||
],
|
||||
name: [
|
||||
{
|
||||
required: true,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user