27 Commits

Author SHA1 Message Date
meilin.huang
2118acf244 release: v1.9.0 2024-10-23 17:30:05 +08:00
meilin.huang
44a1bd626e feat: 数据库迁移至文件支持文件保存天数等 2024-10-22 20:39:44 +08:00
meilin.huang
ea3c70a8a8 feat: 新增统一文件模块,统一文件操作 2024-10-21 22:27:42 +08:00
zongyangleo
6343173cf8 !124 一些更新和bug
* fix: 代码合并
* feat:支持数据库版本兼容,目前兼容了oracle11g部分特性
* fix: 修改数据同步bug,数据sql里指定修改字段别,导致未正确记录修改字段值
* feat: 数据库迁移支持定时迁移和迁移到sql文件
2024-10-20 03:52:23 +00:00
meilin.huang
6837a9c867 fix: sql切割转义等问题处理 2024-10-18 17:15:58 +08:00
meilin.huang
a726927a28 feat: sql脚本执行调整 2024-10-18 12:32:53 +08:00
meilin.huang
e135e4ce64 feat: sql解析器替换、工单统一由‘我的流程’发起、流程定义支持自定义条件触发审批、资源隐藏编号、model支持物理删除等 2024-10-16 17:24:50 +08:00
zongyangleo
43edef412c !123 一些bug修复
* fix: 数据同步、数据迁移体验优化
* fix: des加密传输sql
* fix: 修复达梦字段注释显示问题
* fix: mysql timestamp 字段类型导出ddl错误修复
2024-08-22 00:43:39 +00:00
meilin.huang
2deb3109c2 feat: dbms表数据新增表单视图 2024-07-19 17:06:11 +08:00
meilin.huang
a80221a950 fix: 数据库实例删除等问题修复 2024-07-05 13:14:31 +08:00
meilin.huang
10630847df fix: 工单流程信息展示问题修复 2024-06-24 17:17:57 +08:00
meilin.huang
f43851698e fix: 资源关联多标签删除、数据库实例删除等问题修复与数据库等名称过滤优化 2024-06-07 12:31:40 +08:00
Coder慌
73884bb693 !122 fix: mysql导出修复
Merge pull request !122 from zongyangleo/dev_1.8.6_fix
2024-06-05 04:17:03 +00:00
zongyangleo
1b5bb1de8b fix: mysql导出修复 2024-06-01 13:35:31 +08:00
meilin.huang
4814793546 fix: 修复数据库表数据横向滚动后切换tab导致表头错位&数据取消居中显示 2024-05-31 12:12:40 +08:00
meilin.huang
d85bbff270 release v1.8.6 2024-05-23 17:18:22 +08:00
meilin.huang
bb1522f4dc refactor: 数据库管理迁移至数据库实例-库管理、机器管理-文件支持用户和组信息查看 2024-05-21 12:34:26 +08:00
zongyangleo
a7632fbf58 !121 fix: rdp ssh
* fix: rdp ssh
2024-05-21 04:06:13 +00:00
meilin.huang
c4cb4234fd fix: 问题修复 2024-05-16 17:26:32 +08:00
meilin.huang
89e12678eb refactor: 引入dayjs、新增refreshToken无感刷新、团队新增有效期、数据库等问题修复 2024-05-13 19:55:43 +08:00
meilin.huang
137ebb8e9e fix: 数据库表新增数据表单全必填问题修复等 2024-05-11 12:09:55 +08:00
meilin.huang
05625bd8c1 feat: 1.8.3 2024-05-10 19:59:49 +08:00
meilin.huang
4afeac5fdd refactor: 代码优化与数据库表列显示配置优化 2024-05-09 21:29:34 +08:00
meilin.huang
1d0e91f1af refactor: 机器计划任务与流程定义关联至标签 2024-05-08 21:04:25 +08:00
Coder慌
cf5111a325 !118 修复空数组分隔异常
Merge pull request !118 from 蒋小小/N/A
2024-05-07 11:59:12 +00:00
meilin.huang
78957a8ebd refactor: base.repo与app重构优化 2024-05-05 14:53:30 +08:00
蒋小小
29fd5a25d2 修复空数组分隔异常
Signed-off-by: 蒋小小 <bwcx_jzy@163.com>
2024-04-20 17:33:19 +00:00
431 changed files with 403778 additions and 4840 deletions

View File

@@ -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

View File

@@ -22,7 +22,7 @@
### 介绍
web 版 **linux(终端[终端回放、命令过滤] 文件 脚本 进程 计划任务)、数据库mysql postgres oracle sqlserver 达梦 高斯 sqlite数据同步 数据迁移、redis(单机 哨兵 集群)、mongo 等集工单流程审批于一体的统一管理操作平台**
web 版 **linux(终端[终端回放、命令过滤] 文件 脚本 进程 计划任务)、数据库mysql postgres oracle sqlserver 达梦 高斯 sqlite数据操作 数据同步 数据迁移、redis(单机 哨兵 集群)、mongo 等集工单流程审批于一体的统一管理操作平台**
### 开发语言与主要框架

View File

@@ -14,7 +14,7 @@ services:
restart: always
server:
image: mayfly-go:v1.3.1
image: ccr.ccs.tencentyun.com/mayfly/mayfly-go:v1.8.5
build:
context: .
dockerfile: Dockerfile

View File

@@ -5,4 +5,6 @@ VITE_PORT = 8889
VITE_OPEN = false
# public path 配置线上环境路径(打包)
VITE_PUBLIC_PATH = ''
VITE_PUBLIC_PATH = ''
VITE_EDITOR=idea

View File

@@ -1,67 +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",
"echarts": "^5.5.0",
"element-plus": "^2.7.2",
"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.25",
"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.25",
"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.75.0",
"typescript": "^5.4.5",
"vite": "^5.2.10",
"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"
]
}

View File

@@ -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);
}

View File

@@ -15,7 +15,7 @@ const config = {
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
// 系统版本
version: 'v1.8.2',
version: 'v1.9.0',
};
export default config;

View 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);
}

View File

@@ -2,6 +2,7 @@ import request from './request';
export default {
login: (param: any) => request.post('/auth/accounts/login', param),
refreshToken: (param: any) => request.get('/auth/accounts/refreshToken', param),
otpVerify: (param: any) => request.post('/auth/accounts/otp-verify', param),
getPublicKey: () => request.get('/common/public-key'),
getConfigValue: (params: any) => request.get('/sys/configs/value', params),
@@ -13,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(',')}`),
};

View File

@@ -38,6 +38,7 @@ export enum ResultEnum {
PARAM_ERROR = 405,
SERVER_ERROR = 500,
NO_PERMISSION = 501,
ACCESS_TOKEN_INVALID = 502, // accessToken失效
}
export const baseUrl: string = config.baseApiUrl;
@@ -208,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;

View File

@@ -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(() => {

View File

@@ -1,31 +0,0 @@
export function dateFormat2(fmt: string, date: Date) {
let ret;
const opt = {
'y+': date.getFullYear().toString(), // 年
'M+': (date.getMonth() + 1).toString(), // 月
'd+': date.getDate().toString(), // 日
'H+': date.getHours().toString(), // 时
'm+': date.getMinutes().toString(), // 分
's+': date.getSeconds().toString(), // 秒
'S+': date.getMilliseconds() ? date.getMilliseconds().toString() : '', // 毫秒
// 有其他格式化字符需求可以继续添加,必须转化成字符串
};
for (const k in opt) {
ret = new RegExp('(' + k + ')').exec(fmt);
if (ret) {
fmt = fmt.replace(ret[1], ret[1].length == 1 ? opt[k] : opt[k].padStart(ret[1].length, '0'));
}
}
return fmt;
}
export function dateStrFormat(fmt: string, dateStr: string) {
return dateFormat2(fmt, new Date(dateStr));
}
export function dateFormat(dateStr: string) {
if (!dateStr) {
return '';
}
return dateFormat2('yyyy-MM-dd HH:mm:ss', new Date(dateStr));
}

View File

@@ -1,3 +1,18 @@
import dayjs from 'dayjs';
/**
* 格式化日期
* @param date 日期 字符串 Date 时间戳等
* @param format 格式化格式 默认 YYYY-MM-DD HH:mm:ss
* @returns 格式化后内容
*/
export function formatDate(date: any, format: string = 'YYYY-MM-DD HH:mm:ss') {
if (!date) {
return '';
}
return dayjs(date).format(format);
}
/**
* 格式化字节单位
* @param size byte size
@@ -46,110 +61,6 @@ export function convertToBytes(sizeStr: string) {
return bytes;
}
/*
* 年(Y) 可用1-4个占位符
* 月(m)、日(d)、小时(H)、分(M)、秒(S) 可用1-2个占位符
* 星期(W) 可用1-3个占位符
* 季度(q为阿拉伯数字Q为中文数字)可用1或4个占位符
*
* let date = new Date()
* formatDate(date, "YYYY-mm-dd HH:MM:SS") // 2020-02-09 14:04:23
* formatDate(date, "YYYY-mm-dd HH:MM:SS Q") // 2020-02-09 14:09:03 一
* formatDate(date, "YYYY-mm-dd HH:MM:SS WWW") // 2020-02-09 14:45:12 星期日
* formatDate(date, "YYYY-mm-dd HH:MM:SS QQQQ") // 2020-02-09 14:09:36 第一季度
* formatDate(date, "YYYY-mm-dd HH:MM:SS WWW QQQQ") // 2020-02-09 14:46:12 星期日 第一季度
*/
export function formatDate(date: Date, format: string) {
let we = date.getDay(); // 星期
let qut = Math.floor((date.getMonth() + 3) / 3).toString(); // 季度
const opt: any = {
'Y+': date.getFullYear().toString(), // 年
'm+': (date.getMonth() + 1).toString(), // 月(月份从0开始要+1)
'd+': date.getDate().toString(), // 日
'H+': date.getHours().toString(), // 时
'M+': date.getMinutes().toString(), // 分
'S+': date.getSeconds().toString(), // 秒
'q+': qut, // 季度
};
// 中文数字 (星期)
const week: any = {
'0': '日',
'1': '一',
'2': '二',
'3': '三',
'4': '四',
'5': '五',
'6': '六',
};
// 中文数字(季度)
const quarter: any = {
'1': '一',
'2': '二',
'3': '三',
'4': '四',
};
if (/(W+)/.test(format)) format = format.replace(RegExp.$1, RegExp.$1.length > 1 ? (RegExp.$1.length > 2 ? '星期' + week[we] : '周' + week[we]) : week[we]);
if (/(Q+)/.test(format)) format = format.replace(RegExp.$1, RegExp.$1.length == 4 ? '第' + quarter[qut] + '季度' : quarter[qut]);
for (let k in opt) {
let r = new RegExp('(' + k + ')').exec(format);
// 若输入的长度不为1则前面补零
if (r) format = format.replace(r[1], RegExp.$1.length == 1 ? opt[k] : opt[k].padStart(RegExp.$1.length, '0'));
}
return format;
}
/**
* 10秒 10 * 1000
* 1分 60 * 1000
* 1小时 60 * 60 * 1000
* 24小时60 * 60 * 24 * 1000
* 3天 60 * 60* 24 * 1000 * 3
*
* let data = new Date()
* formatPast(data) // 刚刚
* formatPast(data - 11 * 1000) // 11秒前
* formatPast(data - 2 * 60 * 1000) // 2分钟前
* formatPast(data - 60 * 60 * 2 * 1000) // 2小时前
* formatPast(data - 60 * 60 * 2 * 1000) // 2小时前
* formatPast(data - 60 * 60 * 71 * 1000) // 2天前
* formatPast("2020-06-01") // 2020-06-01
* formatPast("2020-06-01", "YYYY-mm-dd HH:MM:SS WWW QQQQ") // 2020-06-01 08:00:00 星期一 第二季度
*/
export function formatPast(param: any, format: string = 'YYYY-mm-dd') {
// 传入格式处理、存储转换值
let t: any, s: any;
// 获取js 时间戳
let time: any = new Date().getTime();
// 是否是对象
typeof param === 'string' || 'object' ? (t = new Date(param).getTime()) : (t = param);
// 当前时间戳 - 传入时间戳
time = Number.parseInt(`${time - t}`);
if (time < 10000) {
// 10秒内
return '刚刚';
} else if (time < 60000 && time >= 10000) {
// 超过10秒少于1分钟内
s = Math.floor(time / 1000);
return `${s}秒前`;
} else if (time < 3600000 && time >= 60000) {
// 超过1分钟少于1小时
s = Math.floor(time / 60000);
return `${s}分钟前`;
} else if (time < 86400000 && time >= 3600000) {
// 超过1小时少于24小时
s = Math.floor(time / 3600000);
return `${s}小时前`;
} else if (time < 259200000 && time >= 86400000) {
// 超过1天少于3天内
s = Math.floor(time / 86400000);
return `${s}天前`;
} else {
// 超过3天
let date = typeof param === 'string' || 'object' ? new Date(param) : param;
return formatDate(date, format);
}
}
/**
* 格式化指定时间数为人性化可阅读的内容(默认time为秒单位)
*

View File

@@ -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];
}

View File

@@ -1,6 +1,7 @@
import { randomUuid } from './string';
const TokenKey = 'm-token';
const RefreshTokenKey = 'm-refresh-token';
const UserKey = 'm-user';
const TagViewsKey = 'm-tagViews';
const ClientIdKey = 'm-clientId';
@@ -15,6 +16,14 @@ export function saveToken(token: string) {
setLocal(TokenKey, token);
}
export function getRefreshToken(): string {
return getLocal(RefreshTokenKey);
}
export function saveRefreshToken(refreshToken: string) {
return setLocal(RefreshTokenKey, refreshToken);
}
// 获取登录用户基础信息
export function getUser() {
return getLocal(UserKey);
@@ -39,6 +48,7 @@ export function getThemeConfig() {
export function clearUser() {
removeLocal(TokenKey);
removeLocal(UserKey);
removeLocal(RefreshTokenKey);
}
export function getTagViews() {

View File

@@ -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 -> trueprefix=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;
}

View File

@@ -4,6 +4,7 @@ export interface ViteEnv {
VITE_PORT: number;
VITE_OPEN: boolean;
VITE_PUBLIC_PATH: string;
VITE_EDITOR: string;
}
export function loadEnv(): ViteEnv {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View 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>

View File

@@ -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>

View 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>

View File

@@ -252,7 +252,9 @@ const changeLanguage = (value: any) => {
};
const setEditorValue = (value: any) => {
monacoEditorIns.getModel()?.setValue(value);
if (value) {
monacoEditorIns.getModel()?.setValue(value);
}
};
/**

View File

@@ -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">

View File

@@ -1,5 +1,5 @@
import EnumValue from '@/common/Enum';
import { dateFormat } from '@/common/utils/date';
import { formatDate } from '@/common/utils/format';
import { getValueByPath } from '@/common/utils/object';
import { getTextWidth } from '@/common/utils/string';
@@ -172,7 +172,7 @@ export class TableColumn {
*/
isTime(): TableColumn {
this.setFormatFunc((data: any, prop: string) => {
return dateFormat(getValueByPath(data, prop));
return formatDate(getValueByPath(data, prop));
});
return this;
}

View File

@@ -24,8 +24,33 @@
<SvgIcon name="Refresh" @click="connect(0, 0)" :size="20" class="pointer-icon mr10" title="重新连接" />
</div>
<clipboard-dialog ref="clipboardRef" v-model:visible="state.clipboardDialog.visible" @close="closePaste" @submit="onsubmitClipboard" />
<el-dialog
v-if="!state.fullscreen"
destroy-on-close
:title="state.filesystemDialog.title"
v-model="state.filesystemDialog.visible"
:close-on-click-modal="false"
width="70%"
>
<machine-file
:machine-id="state.filesystemDialog.machineId"
:auth-cert-name="state.filesystemDialog.authCertName"
:protocol="state.filesystemDialog.protocol"
:file-id="state.filesystemDialog.fileId"
:path="state.filesystemDialog.path"
/>
</el-dialog>
</div>
<el-dialog destroy-on-close :title="state.filesystemDialog.title" v-model="state.filesystemDialog.visible" :close-on-click-modal="false" width="70%">
<el-dialog
v-if="!state.fullscreen"
destroy-on-close
:title="state.filesystemDialog.title"
v-model="state.filesystemDialog.visible"
:close-on-click-modal="false"
width="70%"
>
<machine-file
:machine-id="state.filesystemDialog.machineId"
:auth-cert-name="state.filesystemDialog.authCertName"
@@ -84,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,

View File

@@ -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) => {

View File

@@ -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)' }),
};

View File

@@ -1,5 +1,5 @@
import router from '@/router';
import { getClientId, getToken } from '@/common/utils/storage';
import { clearUser, getClientId, getRefreshToken, getToken, saveRefreshToken, saveToken } from '@/common/utils/storage';
import { templateResolve } from '@/common/utils/string';
import { ElMessage } from 'element-plus';
import { createFetch } from '@vueuse/core';
@@ -8,6 +8,7 @@ import { Result, ResultEnum } from '@/common/request';
import config from '@/common/config';
import { unref } from 'vue';
import { URL_401 } from '@/router/staticRouter';
import openApi from '@/common/openApi';
const baseUrl: string = config.baseApiUrl;
@@ -41,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;
@@ -56,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) {
@@ -88,61 +89,104 @@ export function useApiFetch<T>(api: Api, params: any = null, reqOptions: Request
return {
execute: async function () {
try {
await uaf.execute(true);
} catch (e: any) {
const rejectPromise = Promise.reject(e);
if (e?.name == 'AbortError') {
console.log('请求已取消');
return rejectPromise;
}
const respStatus = uaf.response.value?.status;
if (respStatus == 404) {
ElMessage.error('请求接口不存在');
return rejectPromise;
}
if (respStatus == 500) {
ElMessage.error('服务器响应异常');
return rejectPromise;
}
console.error(e);
ElMessage.error('网络请求错误');
return rejectPromise;
}
const result: Result = uaf.data.value as any;
if (!result) {
ElMessage.error('网络请求失败');
return Promise.reject(result);
}
// 如果返回为成功结果则将结果的data赋值给响应式data
if (result.code === ResultEnum.SUCCESS) {
uaf.data.value = result.data;
return;
}
// 如果提示没有权限,则跳转至无权限页面
if (result.code === ResultEnum.NO_PERMISSION) {
router.push({
path: URL_401,
});
return Promise.reject(result);
}
// 如果返回的code不为成功则会返回对应的错误msg则直接统一通知即可。忽略登录超时或没有权限的提示直接跳转至401页面
if (result.msg && result?.code != ResultEnum.NO_PERMISSION) {
ElMessage.error(result.msg);
uaf.error.value = new Error(result.msg);
}
return Promise.reject(result);
return execUaf(uaf);
},
isFetching: uaf.isFetching,
data: uaf.data,
abort: uaf.abort,
};
}
let refreshingToken = false;
let queue: any[] = [];
async function execUaf(uaf: any) {
try {
await uaf.execute(true);
} catch (e: any) {
const rejectPromise = Promise.reject(e);
if (e?.name == 'AbortError') {
console.log('请求已取消');
return rejectPromise;
}
const respStatus = uaf.response.value?.status;
if (respStatus == 404) {
ElMessage.error('请求接口不存在');
return rejectPromise;
}
if (respStatus == 500) {
ElMessage.error('服务器响应异常');
return rejectPromise;
}
console.error(e);
ElMessage.error('网络请求错误');
return rejectPromise;
}
const result: Result = uaf.data.value as any;
if (!result) {
ElMessage.error('网络请求失败');
return Promise.reject(result);
}
const resultCode = result.code;
// 如果返回为成功结果则将结果的data赋值给响应式data
if (resultCode === ResultEnum.SUCCESS) {
uaf.data.value = result.data;
return;
}
// 如果是accessToken失效则使用refreshToken刷新token
if (resultCode == ResultEnum.ACCESS_TOKEN_INVALID) {
if (refreshingToken) {
// 请求加入队列等待, 防止并发多次请求refreshToken
return new Promise((resolve) => {
queue.push(() => {
resolve(execUaf(uaf));
});
});
}
try {
refreshingToken = true;
const res = await openApi.refreshToken({ refresh_token: getRefreshToken() });
saveToken(res.token);
saveRefreshToken(res.refresh_token);
// 重新缓存后端用户权限code
await openApi.getPermissions();
// 执行accessToken失效的请求
queue.forEach((resolve: any) => {
resolve();
});
} catch (e: any) {
clearUser();
} finally {
refreshingToken = false;
queue = [];
}
await execUaf(uaf);
return;
}
// 如果提示没有权限,则跳转至无权限页面
if (resultCode === ResultEnum.NO_PERMISSION) {
router.push({
path: URL_401,
});
return Promise.reject(result);
}
// 如果返回的code不为成功则会返回对应的错误msg则直接统一通知即可。忽略登录超时或没有权限的提示直接跳转至401页面
if (result.msg && resultCode != ResultEnum.NO_PERMISSION) {
ElMessage.error(result.msg);
uaf.error.value = new Error(result.msg);
}
return Promise.reject(result);
}

View File

@@ -1,5 +1,5 @@
import { defineStore } from 'pinia';
import { dateFormat2 } from '@/common/utils/date';
import { formatDate } from '@/common/utils/format';
import { useUserInfo } from '@/store/userInfo';
import { getSysStyleConfig } from '@/common/sysconfig';
import { getLocal, getThemeConfig } from '@/common/utils/storage';
@@ -191,7 +191,7 @@ export const useThemeConfig = defineStore('themeConfig', {
},
// 设置水印时间为当前时间
setWatermarkNowTime() {
this.themeConfig.watermarkText[1] = dateFormat2('yyyy-MM-dd HH:mm:ss', new Date());
this.themeConfig.watermarkText[1] = formatDate(new Date());
},
// 切换暗黑模式
switchDark(isDark: boolean) {

View File

@@ -1 +1 @@
@import 'common/transition.scss';
@use 'common/transition.scss';

View File

@@ -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 时,有子级菜单的背景色

View File

@@ -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';

View File

@@ -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;
}
}
}
}

View File

@@ -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%;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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';

View File

@@ -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;
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View 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>

View File

@@ -1,6 +1,6 @@
<template>
<div>
<el-drawer @open="initSort" :title="title" v-model="visible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false">
<el-drawer @open="initSort" :title="title" v-model="visible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
<template #header>
<DrawerHeader :header="title" :back="cancel" />
</template>
@@ -17,10 +17,33 @@
<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>
<el-form-item ref="tagSelectRef" prop="codePaths" label="关联资源">
<tag-tree-check height="300px" v-model="form.codePaths" :tag-type="[TagResourceTypeEnum.DbName.value, TagResourceTypeEnum.Redis.value]" />
</el-form-item>
<el-divider content-position="left">审批节点</el-divider>
<el-table ref="taskTableRef" :data="tasks" row-key="taskKey" stripe style="width: 100%">
@@ -70,6 +93,8 @@ import AccountSelectFormItem from '@/views/system/account/components/AccountSele
import Sortable from 'sortablejs';
import { randomUuid } from '../../common/utils/string';
import { ProcdefStatus } from './enums';
import TagTreeCheck from '../ops/component/TagTreeCheck.vue';
import { TagResourceTypeEnum } from '@/common/commonEnum';
const props = defineProps({
data: {
@@ -112,9 +137,11 @@ const state = reactive({
name: null,
defKey: null,
status: null,
condition: '',
remark: null,
// 流程的审批节点任务
tasks: '',
codePaths: [],
},
sortable: '' as any,
});
@@ -126,6 +153,7 @@ const { isFetching: saveBtnLoading, execute: saveFlowDefExec } = procdefApi.save
watch(props, (newValue: any) => {
if (newValue.data) {
state.form = { ...newValue.data };
state.form.codePaths = newValue.data.tags?.map((tag: any) => tag.codePath);
const tasks = JSON.parse(state.form.tasks);
tasks.forEach((t: any) => {
t.userId = Number.parseInt(t.userId);
@@ -133,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 = [];
}
});
@@ -160,25 +205,26 @@ const deleteTask = (idx: any) => {
};
const btnOk = async () => {
formRef.value.validate(async (valid: boolean) => {
if (!valid) {
ElMessage.error('表单填写有误');
return false;
}
const checkRes = checkTasks();
if (checkRes.err) {
ElMessage.error(checkRes.err);
return false;
}
try {
await formRef.value.validate();
} catch (e: any) {
ElMessage.error('请正确填写信息');
return false;
}
state.form.tasks = JSON.stringify(checkRes.tasks);
await saveFlowDefExec();
ElMessage.success('操作成功');
emit('val-change', state.form);
//重置表单域
formRef.value.resetFields();
state.form = {} as any;
});
const checkRes = checkTasks();
if (checkRes.err) {
ElMessage.error(checkRes.err);
return false;
}
state.form.tasks = JSON.stringify(checkRes.tasks);
await saveFlowDefExec();
ElMessage.success('操作成功');
emit('val-change', state.form);
//重置表单域
formRef.value.resetFields();
state.form = {} as any;
};
const checkTasks = () => {

View File

@@ -18,6 +18,10 @@
<el-link @click="showProcdefTasks(data)" icon="view" type="primary" :underline="false"> </el-link>
</template>
<template #codePaths="{ data }">
<TagCodePath :path="data.tags?.map((tag: any) => tag.codePath)" />
</template>
<template #action="{ data }">
<el-button link v-if="actionBtns[perms.save]" @click="editFlowDef(data)" type="primary">编辑</el-button>
</template>
@@ -42,6 +46,7 @@ import { SearchItem } from '@/components/SearchForm';
import ProcdefEdit from './ProcdefEdit.vue';
import ProcdefTasks from './components/ProcdefTasks.vue';
import { ProcdefStatus } from './enums';
import TagCodePath from '../ops/component/TagCodePath.vue';
const perms = {
save: 'flow:procdef:save',
@@ -55,12 +60,13 @@ const columns = [
TableColumn.new('status', '状态').typeTag(ProcdefStatus),
TableColumn.new('remark', '备注'),
TableColumn.new('tasks', '审批节点').isSlot().alignCenter().setMinWidth(60),
TableColumn.new('codePaths', '关联资源').isSlot().setMinWidth('250px'),
TableColumn.new('creator', '创建账号'),
TableColumn.new('createTime', '创建时间').isTime(),
];
// 该用户拥有的的操作列按钮权限
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);

View File

@@ -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="发起时间">{{ dateFormat(procinst.createTime) }}</el-descriptions-item>
<div v-if="procinst.duration">
<el-descriptions-item label="持续时间">{{ formatTime(procinst.duration) }}</el-descriptions-item>
<el-descriptions-item label="结束时间">{{ dateFormat(procinst.endTime) }}</el-descriptions-item>
</div>
<el-descriptions-item label="流程状态">
<enum-tag :enums="ProcinstStatus" :value="procinst.status"></enum-tag>
@@ -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">
@@ -86,14 +78,14 @@ import { procinstApi } from './api';
import { ElMessage } from 'element-plus';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import { FlowBizType, ProcinstBizStatus, ProcinstTaskStatus, ProcinstStatus } from './enums';
import { dateFormat } from '@/common/utils/date';
import ProcdefTasks from './components/ProcdefTasks.vue';
import { formatTime } from '@/common/utils/format';
import EnumTag from '@/components/enumtag/EnumTag.vue';
import AccountInfo from '@/views/system/account/components/AccountInfo.vue';
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({

View File

@@ -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();

View File

@@ -2,13 +2,14 @@ import Api from '@/common/Api';
export const procdefApi = {
list: Api.newGet('/flow/procdefs'),
getByKey: Api.newGet('/flow/procdefs/{key}'),
getByResource: Api.newGet('/flow/procdefs/{resourceType}/{resourceCode}'),
save: Api.newPost('/flow/procdefs'),
del: Api.newDelete('/flow/procdefs/{id}'),
};
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'),

View File

@@ -3,7 +3,7 @@
<el-step v-for="task in tasksArr" :status="getStepStatus(task)" :title="task.name" :key="task.taskKey">
<template #description>
<div>{{ `${task.accountUsername}(${task.accountName})` }}</div>
<div v-if="task.completeTime">{{ `${dateFormat(task.completeTime)}` }}</div>
<div v-if="task.completeTime">{{ `${formatDate(task.completeTime)}` }}</div>
<div v-if="task.remark">{{ task.remark }}</div>
</template>
</el-step>
@@ -14,8 +14,7 @@
import { toRefs, reactive, watch, onMounted } from 'vue';
import { accountApi } from '../../system/api';
import { ProcinstTaskStatus } from '../enums';
import { dateFormat } from '@/common/utils/date';
import { procdefApi } from '../api';
import { formatDate } from '@/common/utils/format';
import { ElSteps, ElStep } from 'element-plus';
const props = defineProps({
@@ -23,8 +22,8 @@ const props = defineProps({
tasks: {
type: [String, Object],
},
procdefKey: {
type: String,
procdef: {
type: [Object],
},
// 流程实例任务列表
procinstTasks: {
@@ -54,7 +53,7 @@ watch(
);
watch(
() => props.procdefKey,
() => props.procdef,
async (newValue: any) => {
if (newValue) {
parseTasksByKey(newValue);
@@ -63,15 +62,14 @@ watch(
);
onMounted(() => {
if (props.procdefKey) {
parseTasksByKey(props.procdefKey);
if (props.procdef) {
parseTasksByKey(props.procdef);
return;
}
parseTasks(props.tasks);
});
const parseTasksByKey = async (key: string) => {
const procdef = await procdefApi.getByKey.request({ key });
const parseTasksByKey = async (procdef: any) => {
parseTasks(procdef.tasks);
};

View File

@@ -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'),
};

View File

@@ -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>

View File

@@ -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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -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>
@@ -35,7 +43,7 @@
</el-col>
<el-col :xs="24" :sm="12" class="personal-item mb6">
<div class="personal-item-label">上次登录时间</div>
<div class="personal-item-value">{{ dateFormat(userInfo.lastLoginTime) }}</div>
<div class="personal-item-value">{{ formatDate(userInfo.lastLoginTime) }}</div>
</el-col>
</el-row>
</el-col>
@@ -84,12 +92,12 @@
<el-table :data="state.machine.opLogs" :height="state.resourceOpTableHeight" stripe size="small" empty-text="暂无操作记录">
<el-table-column prop="createTime" show-overflow-tooltip width="135">
<template #default="scope">
{{ dateFormat(scope.row.createTime) }}
{{ formatDate(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column prop="codePath" min-width="400" show-overflow-tooltip>
<template #default="scope">
<TagCodePath :path="scope.row.codePath" />
<TagCodePath :path="scope.row.codePath" :tagInfos="state.machine.tagInfos" />
</template>
</el-table-column>
<el-table-column width="30">
@@ -118,12 +126,12 @@
<el-table :data="state.db.opLogs" :height="state.resourceOpTableHeight" stripe size="small" empty-text="暂无操作记录">
<el-table-column prop="createTime" show-overflow-tooltip min-width="135">
<template #default="scope">
{{ dateFormat(scope.row.createTime) }}
{{ formatDate(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column prop="codePath" min-width="380" show-overflow-tooltip>
<template #default="scope">
<TagCodePath :path="scope.row.codePath" />
<TagCodePath :path="scope.row.codePath" :tagInfos="state.db.tagInfos" />
</template>
</el-table-column>
<el-table-column width="30">
@@ -159,12 +167,12 @@
<el-table :data="state.redis.opLogs" :height="state.resourceOpTableHeight" stripe size="small" empty-text="暂无操作记录">
<el-table-column prop="createTime" show-overflow-tooltip min-width="135">
<template #default="scope">
{{ dateFormat(scope.row.createTime) }}
{{ formatDate(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column prop="codePath" min-width="380" show-overflow-tooltip>
<template #default="scope">
<TagCodePath :path="scope.row.codePath" />
<TagCodePath :path="scope.row.codePath" :tagInfos="state.redis.tagInfos" />
</template>
</el-table-column>
<el-table-column width="30">
@@ -198,12 +206,12 @@
<el-table :data="state.mongo.opLogs" :height="state.resourceOpTableHeight" stripe size="small" empty-text="暂无操作记录">
<el-table-column prop="createTime" show-overflow-tooltip min-width="135">
<template #default="scope">
{{ dateFormat(scope.row.createTime) }}
{{ formatDate(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column prop="codePath" min-width="380" show-overflow-tooltip>
<template #default="scope">
<TagCodePath :path="scope.row.codePath" />
<TagCodePath :path="scope.row.codePath" :tagInfos="state.mongo.tagInfos" />
</template>
</el-table-column>
<el-table-column width="30">
@@ -228,7 +236,7 @@
<el-table-column property="msg" label="消息"></el-table-column>
<el-table-column property="createTime" label="时间" width="150">
<template #default="scope">
{{ dateFormat(scope.row.createTime) }}
{{ formatDate(scope.row.createTime) }}
</template>
</el-table-column>
</el-table>
@@ -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 { dateFormat } from '@/common/utils/date';
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;
}

View File

@@ -132,7 +132,7 @@ import { nextTick, onMounted, ref, toRefs, reactive, computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
import { initRouter } from '@/router/index';
import { saveToken, saveUser } from '@/common/utils/storage';
import { getRefreshToken, saveRefreshToken, saveToken, saveUser } from '@/common/utils/storage';
import { formatAxis } from '@/common/utils/format';
import openApi from '@/common/openApi';
import { RsaEncrypt } from '@/common/rsa';
@@ -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' }],
@@ -279,19 +280,20 @@ const login = () => {
};
const otpVerify = async () => {
otpFormRef.value.validate(async (valid: boolean) => {
if (!valid) {
return false;
}
try {
state.loading.otpConfirm = true;
const accessToken = await openApi.otpVerify(state.otpDialog.form);
await signInSuccess(accessToken);
state.otpDialog.visible = false;
} finally {
state.loading.otpConfirm = false;
}
});
try {
await otpFormRef.value.validate();
} catch (e: any) {
return false;
}
try {
state.loading.otpConfirm = true;
const res = await openApi.otpVerify(state.otpDialog.form);
await signInSuccess(res.token, res.refresh_token);
state.otpDialog.visible = false;
} finally {
state.loading.otpConfirm = false;
}
};
// 登录
@@ -327,46 +329,55 @@ const onSignIn = async () => {
};
const updateUserInfo = async () => {
baseInfoFormRef.value.validate(async (valid: boolean) => {
if (!valid) {
return false;
}
try {
state.loading.updateUserConfirm = true;
const form = state.baseInfoDialog.form;
await personApi.updateAccount.request(state.baseInfoDialog.form);
state.baseInfoDialog.visible = false;
useUserInfo().userInfo.username = form.username;
useUserInfo().userInfo.name = form.name;
await toIndex();
} finally {
state.loading.updateUserConfirm = false;
}
});
try {
await baseInfoFormRef.value.validate();
} catch (e: any) {
return false;
}
try {
state.loading.updateUserConfirm = true;
const form = state.baseInfoDialog.form;
await personApi.updateAccount.request(state.baseInfoDialog.form);
state.baseInfoDialog.visible = false;
useUserInfo().userInfo.username = form.username;
useUserInfo().userInfo.name = form.name;
await toIndex();
} finally {
state.loading.updateUserConfirm = false;
}
};
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);
signInSuccess(token, loginRes.refresh_token);
return;
}
@@ -379,12 +390,16 @@ const loginResDeal = (loginRes: any) => {
};
// 登录成功后的跳转
const signInSuccess = async (accessToken: string = '') => {
const signInSuccess = async (accessToken: string = '', refreshToken = '') => {
if (!accessToken) {
accessToken = getToken();
}
if (!refreshToken) {
refreshToken = getRefreshToken();
}
// 存储 token 到浏览器缓存
saveToken(accessToken);
saveRefreshToken(refreshToken);
// 初始化路由
await initRouter();
@@ -415,26 +430,27 @@ const toIndex = async () => {
}, 300);
};
const changePwd = () => {
changePwdFormRef.value.validate(async (valid: boolean) => {
if (!valid) {
return false;
}
try {
state.loading.changePwd = true;
const form = state.changePwdDialog.form;
const changePwdReq: any = { ...form };
changePwdReq.oldPassword = await RsaEncrypt(form.oldPassword);
changePwdReq.newPassword = await RsaEncrypt(form.newPassword);
await openApi.changePwd(changePwdReq);
ElMessage.success('密码修改成功, 新密码已填充至登录密码框');
state.loginForm.password = state.changePwdDialog.form.newPassword;
state.changePwdDialog.visible = false;
getCaptcha();
} finally {
state.loading.changePwd = false;
}
});
const changePwd = async () => {
try {
await changePwdFormRef.value.validate();
} catch (e: any) {
return false;
}
try {
state.loading.changePwd = true;
const form = state.changePwdDialog.form;
const changePwdReq: any = { ...form };
changePwdReq.oldPassword = await RsaEncrypt(form.oldPassword);
changePwdReq.newPassword = await RsaEncrypt(form.newPassword);
await openApi.changePwd(changePwdReq);
ElMessage.success('密码修改成功, 新密码已填充至登录密码框');
state.loginForm.password = state.changePwdDialog.form.newPassword;
state.changePwdDialog.visible = false;
getCaptcha();
} finally {
state.loading.changePwd = false;
}
};
const cancelChangePwd = () => {

View File

@@ -39,6 +39,11 @@
/>
<el-option :key="TagResourceTypeEnum.Db.value" :label="TagResourceTypeEnum.Db.label" :value="TagResourceTypeEnum.Db.value" />
<el-option
:key="TagResourceTypeEnum.Redis.value"
:label="TagResourceTypeEnum.Redis.label"
:value="TagResourceTypeEnum.Redis.value"
/>
</el-select>
</el-form-item>
<el-form-item prop="resourceCode" label="资源编号" required>
@@ -46,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>

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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>
@@ -35,7 +35,9 @@
</slot>
</span>
<slot :node="node" :data="data" name="suffix"></slot>
<span class="label-suffix">
<slot :node="node" :data="data" name="suffix"></slot>
</span>
</span>
</template>
</el-tree>
@@ -46,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: {
@@ -103,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);
};
/**
@@ -124,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;
}
@@ -139,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();
@@ -205,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({
@@ -222,5 +237,13 @@ defineExpose({
display: inline-block;
min-width: 100%;
}
.label-suffix {
position: absolute;
right: 10px;
color: #c4c9c4;
font-size: 10px;
margin-top: 2px;
}
}
</style>

View File

@@ -1,54 +1,56 @@
<template>
<div class="w100" style="border: 1px solid var(--el-border-color)">
<el-input v-model="filterTag" clearable placeholder="输入关键字过滤" size="small" />
<el-scrollbar :style="{ height: props.height }">
<el-tree
v-bind="$attrs"
ref="tagTreeRef"
style="width: 100%"
:data="state.tags"
:default-expanded-keys="checkedTags"
:default-checked-keys="checkedTags"
multiple
:render-after-expand="true"
show-checkbox
check-strictly
:node-key="$props.nodeKey"
:props="{
value: $props.nodeKey,
label: 'codePath',
children: 'children',
disabled: 'disabled',
}"
@check="tagTreeNodeCheck"
:filter-node-method="filterNode"
>
<template #default="{ data }">
<span class="custom-tree-node">
<SvgIcon
:name="EnumValue.getEnumByValue(TagResourceTypeEnum, data.type)?.extra.icon"
:color="EnumValue.getEnumByValue(TagResourceTypeEnum, data.type)?.extra.iconColor"
/>
<div class="w100 tag-tree-check">
<el-input v-model="filterTag" @input="onFilterValChanged" clearable placeholder="输入关键字过滤" size="small" />
<div class="mt3" style="border: 1px solid var(--el-border-color)">
<el-scrollbar :style="{ height: props.height }">
<el-tree
v-bind="$attrs"
ref="tagTreeRef"
:data="state.tags"
:default-expanded-keys="state.defaultExpandedKeys"
:default-checked-keys="checkedTags"
multiple
:render-after-expand="true"
show-checkbox
check-strictly
:node-key="$props.nodeKey"
:props="{
value: $props.nodeKey,
label: 'codePath',
children: 'children',
disabled: 'disabled',
}"
@check="tagTreeNodeCheck"
:filter-node-method="filterNode"
>
<template #default="{ data }">
<span>
<SvgIcon
:name="EnumValue.getEnumByValue(TagResourceTypeEnum, data.type)?.extra.icon"
:color="EnumValue.getEnumByValue(TagResourceTypeEnum, data.type)?.extra.iconColor"
/>
<span class="font13 ml5">
{{ data.code }}
<span style="color: #3c8dbc"></span>
{{ data.name }}
<span style="color: #3c8dbc"></span>
<el-tag v-if="data.children !== null" size="small">{{ data.children.length }} </el-tag>
<span class="font13 ml5">
{{ 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>
</span>
</span>
</template>
</el-tree>
</el-scrollbar>
</template>
</el-tree>
</el-scrollbar>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, onMounted, watch } from 'vue';
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: {
@@ -56,7 +58,7 @@ const props = defineProps({
default: 'calc(100vh - 330px)',
},
tagType: {
type: Number,
type: [Number, Array<Number>],
default: TagResourceTypeEnum.Tag.value,
},
nodeKey: {
@@ -73,15 +75,22 @@ const tagTreeRef: any = ref(null);
const filterTag = ref('');
const state = reactive({
defaultExpandedKeys: [] as any,
tags: [],
});
onMounted(() => {
state.defaultExpandedKeys = checkedTags.value;
search();
});
const search = async () => {
state.tags = await tagApi.getTagTrees.request({ type: props.tagType });
let tagType: any = props.tagType;
if (Array.isArray(props.tagType)) {
tagType = props.tagType.join(',');
}
state.tags = await tagApi.getTagTrees.request({ type: tagType });
setTimeout(() => {
const checkedNodes = tagTreeRef.value.getCheckedNodes();
@@ -93,15 +102,12 @@ const search = async () => {
}, 200);
};
watch(filterTag, (val) => {
tagTreeRef.value!.filter(val);
});
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) => {
tagTreeRef.value!.filter(val);
};
const tagTreeNodeCheck = (data: any) => {
@@ -150,4 +156,12 @@ const disableParentNodes = (node: any, disable = true) => {
disableParentNodes(node.parent, disable);
};
</script>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
.tag-tree-check {
.el-tree {
min-width: 100%;
// 横向滚动生效
display: inline-block;
}
}
</style>

View File

@@ -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) {
@@ -199,3 +201,50 @@ 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 = [];
let currentPath = '';
for (let i = 0; i < parts.length - 1; i++) {
currentPath += parts[i] + '/';
result.push(currentPath);
}
return result;
}

View File

@@ -10,20 +10,12 @@
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>
<el-form-item prop="authCertName" label="授权凭证" required>
<el-select @change="changeAuthCert" v-model="form.authCertName" placeholder="请选择授权凭证" filterable>
<el-select v-model="form.authCertName" placeholder="请选择授权凭证" filterable>
<el-option v-for="item in state.authCerts" :key="item.id" :label="`${item.name}`" :value="item.name">
{{ item.name }}
@@ -39,8 +31,15 @@
</el-select>
</el-form-item>
<el-form-item prop="getDatabaseMode" label="获库方式" required>
<el-select v-model="form.getDatabaseMode" @change="onChangeGetDatabaseMode" placeholder="请选择库名获取方式">
<el-option v-for="item in DbGetDbNamesMode" :key="item.value" :label="item.label" :value="item.value"> </el-option>
</el-select>
</el-form-item>
<el-form-item prop="database" label="数据库名">
<el-select
:disabled="form.getDatabaseMode == DbGetDbNamesMode.Auto.value || !form.authCertName"
v-model="dbNamesSelected"
multiple
clearable
@@ -49,8 +48,9 @@
filterable
:filter-method="filterDbNames"
allow-create
placeholder="请确保数据库实例信息填写完整后获取库名"
style="width: 100%"
placeholder="获库方式为‘指定库名’时,可选择"
@focus="getAllDatabase(form.authCertName)"
:loading="state.loadingDbNames"
>
<template #header>
<el-checkbox v-model="checkAllDbNames" :indeterminate="indeterminateDbNames" @change="handleCheckAll"> 全选 </el-checkbox>
@@ -62,8 +62,6 @@
<el-form-item prop="remark" label="备注">
<el-input v-model.trim="form.remark" auto-complete="off" type="textarea"></el-input>
</el-form-item>
<procdef-select-form-item v-model="form.flowProcdefKey" />
</el-form>
<template #footer>
@@ -77,19 +75,17 @@
</template>
<script lang="ts" setup>
import { toRefs, reactive, watch, ref, watchEffect } from 'vue';
import { toRefs, reactive, watch, ref } from 'vue';
import { dbApi } from './api';
import { ElMessage } from 'element-plus';
// import TagTreeSelect from '../component/TagTreeSelect.vue';
import type { CheckboxValueType } from 'element-plus';
import ProcdefSelectFormItem from '@/views/flow/components/ProcdefSelectFormItem.vue';
import { DbType } from '@/views/ops/db/dialect';
import { ResourceCodePattern } from '@/common/pattern';
import EnumTag from '@/components/enumtag/EnumTag.vue';
import { AuthCertCiphertextTypeEnum } from '../tag/enums';
import { resourceAuthCertApi } from '../tag/api';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import { DbGetDbNamesMode } from './enums';
const props = defineProps({
visible: {
@@ -125,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,
@@ -151,10 +135,10 @@ const rules = {
trigger: ['change', 'blur'],
},
],
database: [
getDatabaseMode: [
{
required: true,
message: '请添加数据库',
message: '请选择库名获取方式',
trigger: ['change', 'blur'],
},
],
@@ -176,39 +160,45 @@ const state = reactive({
authCerts: [] as any,
form: {
id: null,
// tagId: [],
name: null,
code: '',
getDatabaseMode: DbGetDbNamesMode.Auto.value,
database: '',
remark: '',
instanceId: null as any,
authCertName: '',
flowProcdefKey: '',
},
instances: [] as any,
loadingDbNames: false,
});
const { dialogVisible, allDatabases, form, dbNamesSelected } = toRefs(state);
watchEffect(() => {
state.dialogVisible = props.visible;
if (!state.dialogVisible) {
return;
watch(
() => props.visible,
() => {
state.dialogVisible = props.visible;
if (!state.dialogVisible) {
return;
}
const db: any = props.db;
if (db.code) {
state.form = { ...db };
if (db.getDatabaseMode == DbGetDbNamesMode.Assign.value) {
// 将数据库名使用空格切割,获取所有数据库列表
state.dbNamesSelected = db.database.split(' ');
}
} else {
state.form = { getDatabaseMode: DbGetDbNamesMode.Auto.value } as any;
state.dbNamesSelected = [];
}
}
const db: any = props.db;
if (db.code) {
state.form = { ...db };
// state.form.tagId = newValue.db.tags.map((t: any) => t.tagId);
// 将数据库名使用空格切割,获取所有数据库列表
state.dbNamesSelected = db.database.split(' ');
} else {
state.form = {} as any;
);
const onChangeGetDatabaseMode = (val: any) => {
if (val == DbGetDbNamesMode.Auto.value) {
state.dbNamesSelected = [];
}
});
const changeAuthCert = (val: string) => {
getAllDatabase(val);
};
const getAuthCerts = async () => {
@@ -222,15 +212,20 @@ const getAuthCerts = async () => {
};
const getAllDatabase = async (authCertName: string) => {
const req = { ...(props.instance as any) };
req.authCert = state.authCerts?.find((x: any) => x.name == authCertName);
let dbs = await dbApi.getAllDatabase.request(req);
state.allDatabases = dbs;
try {
state.loadingDbNames = true;
const req = { ...(props.instance as any) };
req.authCert = state.authCerts?.find((x: any) => x.name == authCertName);
let dbs = await dbApi.getAllDatabase.request(req);
state.allDatabases = dbs;
// 如果是oracle且没查出数据库列表则取实例sid
let instance = state.instances.find((item: any) => item.id === state.form.instanceId);
if (instance && instance.type === DbType.oracle && dbs.length === 0) {
state.allDatabases = [instance.sid];
// 如果是oracle且没查出数据库列表则取实例sid
let instance = state.instances.find((item: any) => item.id === state.form.instanceId);
if (instance && instance.type === DbType.oracle && dbs.length === 0) {
state.allDatabases = [instance.sid];
}
} finally {
state.loadingDbNames = false;
}
};

View File

@@ -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="state.currentDbs = data.database" 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" 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,128 +187,98 @@
<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="3" label="工单流程key">{{ infoDialog.data?.flowProcdefKey }}</el-descriptions-item>
<el-descriptions-item :span="2" label="创建时间">{{ dateFormat(infoDialog.data?.createTime) }} </el-descriptions-item>
<el-descriptions-item :span="1" label="创建者">{{ infoDialog.data?.creator }}</el-descriptions-item>
<el-descriptions-item :span="2" label="更新时间">{{ dateFormat(infoDialog.data?.updateTime) }} </el-descriptions-item>
<el-descriptions-item :span="1" label="修改者">{{ infoDialog.data?.modifier }}</el-descriptions-item>
</el-descriptions>
</el-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 { dateFormat } from '@/common/utils/date';
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';
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('flowProcdefKey', '关联流程'),
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: '',
currentDbs: '',
loadingDbNames: false,
currentDbNames: [],
dbNameSearch: '',
instances: [] as any,
/**
* 选中的数据
*/
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,
},
// 数据库备份弹框
@@ -320,6 +309,7 @@ const state = reactive({
exportDialog: {
visible: false,
dbId: 0,
db: '',
type: 3,
data: [] as any,
value: [],
@@ -338,64 +328,77 @@ 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);
const search = async () => {
state.query.instanceId = props.instance?.id;
pageTableRef.value.search();
};
const getDbNames = async (db: any) => {
try {
state.loadingDbNames = true;
state.currentDbNames = await DbInst.getDbNames(db);
} finally {
state.loadingDbNames = false;
}
search();
});
};
const filterDbs = computed(() => {
const dbsStr = state.currentDbs;
if (!dbsStr) {
const dbNames = state.currentDbNames;
if (!dbNames) {
return [];
}
const dbs = dbsStr.split(' ').map((db: any) => {
return { dbName: db };
const dbNameObjs = dbNames.map((x) => {
return {
dbName: x,
};
});
return dbs.filter((db: any) => {
return dbNameObjs.filter((db: any) => {
return db.dbName.includes(state.dbNameSearch);
});
});
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();
}
};
@@ -403,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;
@@ -429,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;
};
@@ -442,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({
@@ -469,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;
@@ -512,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 {

View File

@@ -45,14 +45,14 @@
<el-descriptions :column="1" border>
<el-descriptions-item :span="1" label="数据库名称">{{ infoDialog.data.dbName }}</el-descriptions-item>
<el-descriptions-item v-if="infoDialog.data.pointInTime" :span="1" label="恢复时间点">{{
dateFormat(infoDialog.data.pointInTime)
formatDate(infoDialog.data.pointInTime)
}}</el-descriptions-item>
<el-descriptions-item v-if="!infoDialog.data.pointInTime" :span="1" label="数据库备份">{{
infoDialog.data.dbBackupHistoryName
}}</el-descriptions-item>
<el-descriptions-item :span="1" label="开始时间">{{ dateFormat(infoDialog.data.startTime) }}</el-descriptions-item>
<el-descriptions-item :span="1" label="开始时间">{{ formatDate(infoDialog.data.startTime) }}</el-descriptions-item>
<el-descriptions-item :span="1" label="是否启用">{{ infoDialog.data.enabledDesc }}</el-descriptions-item>
<el-descriptions-item :span="1" label="执行时间">{{ dateFormat(infoDialog.data.lastTime) }}</el-descriptions-item>
<el-descriptions-item :span="1" label="执行时间">{{ formatDate(infoDialog.data.lastTime) }}</el-descriptions-item>
<el-descriptions-item :span="1" label="执行结果">{{ infoDialog.data.lastResult }}</el-descriptions-item>
</el-descriptions>
</el-dialog>
@@ -66,7 +66,7 @@ import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { SearchItem } from '@/components/SearchForm';
import { ElMessage, ElMessageBox } from 'element-plus';
import { dateFormat } from '@/common/utils/date';
import { formatDate } from '@/common/utils/format';
const DbRestoreEdit = defineAsyncComponent(() => import('./DbRestoreEdit.vue'));
const pageTableRef: Ref<any> = ref(null);

View File

@@ -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(
@@ -259,6 +360,7 @@ const handleSrcTableCheckChange = (data: { id: string; name: string }, checked:
}
}
if (data.id && (data.id + '').startsWith('list-item')) {
//
}
};
@@ -322,10 +424,4 @@ const cancel = () => {
emit('cancel');
};
</script>
<style lang="scss">
.db-transfer-edit {
.el-select {
width: 100%;
}
}
</style>
<style lang="scss"></style>

View 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>

View File

@@ -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(`确定删除任务?`, '提示', {

View File

@@ -1,171 +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="database" label="库" min-width="80">
<template #default="scope">
<el-popover placement="bottom" :width="200" trigger="click">
<template #reference>
<el-button @click="state.currentDbs = scope.row.database" type="primary" link>查看库</el-button>
</template>
<el-table :data="filterDbs" 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-table-column>
<el-table-column prop="remark" label="备注" show-overflow-tooltip min-width="120"> </el-table-column>
<el-table-column prop="flowProcdefKey" label="关联流程" min-width="120" show-overflow-tooltip> </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';
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,
currentDbs: '', // 当前数据库名,空格分割库名
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 filterDbs = computed(() => {
const dbsStr = state.currentDbs;
if (!dbsStr) {
return [];
}
const dbs = dbsStr.split(' ').map((db: any) => {
return { dbName: db };
});
return dbs.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>

View File

@@ -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,

View File

@@ -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>
@@ -53,10 +53,10 @@
<el-descriptions-item :span="3" label="SSH隧道">{{ infoDialog.data.sshTunnelMachineId > 0 ? '是' : '否' }} </el-descriptions-item>
<el-descriptions-item :span="2" label="创建时间">{{ dateFormat(infoDialog.data.createTime) }} </el-descriptions-item>
<el-descriptions-item :span="2" label="创建时间">{{ formatDate(infoDialog.data.createTime) }} </el-descriptions-item>
<el-descriptions-item :span="1" label="创建者">{{ infoDialog.data.creator }}</el-descriptions-item>
<el-descriptions-item :span="2" label="更新时间">{{ dateFormat(infoDialog.data.updateTime) }} </el-descriptions-item>
<el-descriptions-item :span="2" label="更新时间">{{ formatDate(infoDialog.data.updateTime) }} </el-descriptions-item>
<el-descriptions-item :span="1" label="修改者">{{ infoDialog.data.modifier }}</el-descriptions-item>
</el-descriptions>
</el-dialog>
@@ -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>
@@ -76,7 +76,7 @@
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { dbApi } from './api';
import { dateFormat } from '@/common/utils/date';
import { formatDate } from '@/common/utils/format';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { hasPerms } from '@/components/auth/auth';
@@ -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;
};

View File

@@ -47,10 +47,8 @@
</template>
<template #suffix="{ data }">
<span class="db-table-size" v-if="data.type.value == SqlExecNodeType.Table && data.params.size">{{ ` ${data.params.size}` }}</span>
<span class="db-table-size" v-if="data.type.value == SqlExecNodeType.TableMenu && data.params.dbTableSize">{{
` ${data.params.dbTableSize}`
}}</span>
<span v-if="data.type.value == SqlExecNodeType.Table && data.params.size">{{ ` ${data.params.size}` }}</span>
<span v-if="data.type.value == SqlExecNodeType.TableMenu && data.params.dbTableSize">{{ ` ${data.params.dbTableSize}` }}</span>
</template>
</tag-tree>
</Pane>
@@ -60,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>
@@ -105,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">
@@ -132,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
@@ -140,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>
@@ -148,7 +205,6 @@
:db-id="dt.params.id"
:db="dt.params.db"
:db-type="dt.params.type"
:flow-procdef-key="dt.params.flowProcdefKey"
:height="state.tablesOpHeight"
/>
</el-tab-pane>
@@ -163,33 +219,41 @@
:dbId="tableCreateDialog.dbId"
:db="tableCreateDialog.db"
:dbType="tableCreateDialog.dbType"
:flow-procdef-key="tableCreateDialog.flowProcdefKey"
: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 { 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'));
@@ -232,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}`,
@@ -243,7 +307,6 @@ const nodeClickChangeDb = (nodeData: TagTreeNode) => {
type: params.type,
tagPath: params.tagPath,
databases: params.dbs,
flowProcdefKey: params.flowProcdefKey,
},
params.db
);
@@ -271,9 +334,12 @@ const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath)
.withContextMenuItems([ContextmenuItemRefresh]);
// 数据库实例节点类型
const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((parentNode: TagTreeNode) => {
const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
const params = parentNode.params;
const dbs = params.database.split(' ')?.sort();
const dbs = (await DbInst.getDbNames(params))?.sort();
// 查询数据库版本信息
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)
@@ -282,10 +348,10 @@ const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((p
id: params.id,
name: params.name,
type: params.type,
version: version || 'unset',
host: `${params.host}:${params.port}`,
dbs: dbs,
db: x,
flowProcdefKey: params.flowProcdefKey,
})
.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, flowProcdefKey, 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,
flowProcdefKey: flowProcdefKey,
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: '',
flowProcdefKey: '',
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];
@@ -497,7 +598,7 @@ const autoOpenDb = (codePath: string) => {
// 置空
autoOpenResourceStore.setDbCodePath('');
tagTreeRef.value.setCurrentKey(dbCode);
}, 600);
}, 1000);
};
/**
@@ -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;
}
};
@@ -669,6 +777,32 @@ const onTabChange = () => {
// 注册sql提示
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);
};
const reloadSqls = (dbId: number, db: string) => {
@@ -700,7 +834,7 @@ const reloadNode = (nodeKey: string) => {
};
const onEditTable = async (data: any) => {
let { db, id, tableName, tableComment, type, parentKey, key, flowProcdefKey } = data.params;
let { db, id, tableName, tableComment, type, parentKey, key, version } = data.params;
// data.label就是表名
if (tableName) {
state.tableCreateDialog.title = '修改表';
@@ -717,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.flowProcdefKey = flowProcdefKey;
state.tableCreateDialog.visible = true;
};
const onDeleteTable = async (data: any) => {
let { db, id, tableName, parentKey, flowProcdefKey, schema } = data.params;
let { db, id, tableName, parentKey, schema } = data.params;
await ElMessageBox.confirm(`此操作是永久性且无法撤销,确定删除【${tableName}】? `, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
@@ -735,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 (flowProcdefKey) {
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, flowProcdefKey } = data.params;
let { db, id, tableName, parentKey } = data.params;
let tableData = { db, oldTableName: tableName, tableName };
let value = ref(tableName);
@@ -771,7 +918,6 @@ const onRenameTable = async (data: any) => {
dbId: id as any,
db: db as any,
dbType: nowDbInst.value.getDialect().getInfo().formatSqlDialect,
flowProcdefKey: flowProcdefKey,
runSuccessCallback: () => {
setTimeout(() => {
parentKey && reloadNode(parentKey);
@@ -831,7 +977,6 @@ const getNowDbInfo = () => {
name: di.name,
type: di.type,
host: di.host,
flowProcdefKey: di.flowProcdefKey,
dbName: state.db,
};
};
@@ -839,11 +984,6 @@ const getNowDbInfo = () => {
<style lang="scss">
.db-sql-exec {
.db-table-size {
color: #c4c9c4;
font-size: 9px;
}
.db-op {
height: calc(100vh - 106px);
}
@@ -857,7 +997,7 @@ const getNowDbInfo = () => {
margin: 0 0 5px;
.el-tabs__item {
padding: 0 10px;
padding: 0 5px;
}
}

View File

@@ -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>
@@ -115,7 +139,7 @@
<el-option
v-for="item in state.targetColumnList"
:key="item.columnName"
:label="item.columnName + ` ${item.showDataType}` + (item.columnComment && ' - ' + item.columnComment)"
:label="item.columnName + ` ${item.columnType}` + (item.columnComment && ' - ' + item.columnComment)"
:value="item.columnName"
/>
</el-select>
@@ -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;

View File

@@ -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,10 +26,13 @@ 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}'),
getAllDatabase: Api.newPost('/instances/databases'),
getDbNamesByAc: Api.newGet('/instances/databases/{authCertName}'),
getInstanceServerInfo: Api.newGet('/instances/{instanceId}/server-info'),
testConn: Api.newPost('/instances/test-conn'),
saveInstance: Api.newPost('/instances'),
@@ -69,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'),
@@ -87,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;
};

View File

@@ -25,26 +25,15 @@ import SvgIcon from '@/components/svgIcon/index.vue';
import { getDbDialect, noSchemaTypes } from '@/views/ops/db/dialect';
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']);
/**
* 树节点类型
@@ -62,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: () => {
//
@@ -101,9 +90,9 @@ const noSchemaType = (type: string) => {
};
// 数据库实例节点类型
const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((parentNode: TagTreeNode) => {
const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
const params = parentNode.params;
const dbs = params.database.split(' ')?.sort();
const dbs = (await DbInst.getDbNames(params))?.sort();
let fn: NodeType;
if (noSchemaType(params.type)) {
fn = MysqlNodeTypes;
@@ -115,6 +104,7 @@ const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((p
.withParams({
tagPath: params.tagPath,
id: params.id,
code: params.code,
instanceId: params.instanceId,
name: params.name,
type: params.type,
@@ -155,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>

View File

@@ -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">
@@ -296,46 +296,50 @@ const onRunSql = async (newTab = false) => {
notBlank(sql && sql.trim(), '请选中需要执行的sql');
// 去除字符串前的空格、换行等
sql = sql.replace(/(^\s*)/g, '');
let execRemark = '';
let canRun = true;
const sqls = splitSql(sql);
// 简单截取前十个字符
const sqlPrefix = sql.slice(0, 10).toLowerCase();
if (
const nonQuery =
sqlPrefix.startsWith('update') ||
sqlPrefix.startsWith('insert') ||
sqlPrefix.startsWith('delete') ||
sqlPrefix.startsWith('alert') ||
sqlPrefix.startsWith('drop') ||
sqlPrefix.startsWith('create')
) {
const res: any = await ElMessageBox.prompt('请输入备注', 'Tip', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPattern: /^[\s\S]*.*[^\s][\s\S]*$/,
inputErrorMessage: '请输入执行该sql的备注信息',
});
execRemark = res.value;
if (!execRemark) {
canRun = false;
}
}
if (!canRun) {
return;
}
// 启用工单审批
if (execRemark && getNowDbInst().flowProcdefKey) {
try {
await getNowDbInst().runSql(props.dbName, sql, execRemark);
ElMessage.success('工单提交成功');
return;
} catch (e) {
ElMessage.success('工单提交失败');
return;
sqlPrefix.startsWith('create');
if (sqls.length == 1) {
let execRemark;
if (nonQuery) {
const res: any = await ElMessageBox.prompt('请输入备注', 'Tip', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputErrorMessage: '输入执行该sql的备注信息',
});
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);
}
}
}
};
/**
* 执行单条sql
*
* @param sql 单条sql
* @param newTab 是否新建tab
*/
const runSql = async (sql: string, remark = '', newTab = false) => {
let execRes: ExecResTab;
let i = 0;
let id;
@@ -363,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 = '查无数据';
}
@@ -388,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;
@@ -410,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如果有鼠标选中则返回选中内容否则返回输入框内所有内容
*/
@@ -707,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">

View File

@@ -6,7 +6,7 @@ export type SqlExecProps = {
dbId: number;
db: string;
dbType?: string;
flowProcdefKey?: string;
flowProcdef?: any;
runSuccessCallback?: Function;
cancelCallback?: Function;
};

View File

@@ -1,19 +1,8 @@
<template>
<div>
<el-dialog title="待执行SQL" v-model="dialogVisible" :show-close="false" width="600px">
<el-dialog title="待执行SQL" v-model="dialogVisible" :show-close="false" width="600px" :close-on-click-modal="false">
<monaco-editor height="300px" class="codesql" language="sql" v-model="sqlValue" />
<el-input
@keyup.enter="runSql"
ref="remarkInputRef"
v-model="remark"
:placeholder="props.flowProcdefKey ? '执行备注(必填)' : '执行备注(选填)'"
class="mt5"
/>
<div v-if="props.flowProcdefKey">
<el-divider content-position="left">审批节点</el-divider>
<procdef-tasks :procdef-key="props.flowProcdefKey" />
</div>
<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,14 +47,10 @@ onMounted(() => {
* 执行sql
*/
const runSql = async () => {
// 存在流程审批,则备注为必填
if (!state.remark && props.flowProcdefKey) {
ElMessage.error('请输入执行的备注信息');
return;
}
try {
state.btnLoading = true;
runSuccess = true;
const res = await dbApi.sqlExec.request({
id: props.dbId,
db: props.db,
@@ -73,21 +58,15 @@ const runSql = async () => {
sql: state.sqlValue.trim(),
});
// 存在流程审批
if (props.flowProcdefKey) {
runSuccess = false;
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}`);
}
}
runSuccess = true;
isTrue(isSuccess, '存在执行失败sql');
ElMessage.success('执行成功');
} catch (e) {
runSuccess = false;
@@ -96,9 +75,9 @@ const runSql = async () => {
if (props.runSuccessCallback) {
props.runSuccessCallback();
}
cancel();
}
state.btnLoading = false;
cancel();
}
};
@@ -113,7 +92,7 @@ const cancel = () => {
};
const open = () => {
state.sqlValue = sqlFormatter(props.sql, { language: props.dbType || 'mysql' });
state.sqlValue = sqlFormatter(props.sql, { language: (props.dbType || 'mysql') as any });
state.dialogVisible = true;
setTimeout(() => {
remarkInputRef.value?.focus();

View File

@@ -1,7 +1,6 @@
<template>
<div class="string-input-container w100" v-if="dataType == DataType.String">
<div class="string-input-container w100" v-if="dataType == DataType.String || dataType == DataType.Number">
<el-input
v-if="dataType == DataType.String"
:ref="(el: any) => focus && el?.focus()"
:disabled="disabled"
@blur="handleBlur"
@@ -13,18 +12,6 @@
<SvgIcon v-if="showEditorIcon" @mousedown="openEditor" class="string-input-container-icon" name="FullScreen" :size="10" />
</div>
<el-input
v-else-if="dataType == DataType.Number"
:ref="(el: any) => focus && el?.focus()"
:disabled="disabled"
@blur="handleBlur"
class="w100 mb4"
size="small"
v-model.number="itemValue"
:placeholder="placeholder"
type="number"
/>
<el-date-picker
v-else-if="dataType == DataType.Date"
:ref="(el: any) => focus && el?.focus()"
@@ -38,7 +25,7 @@
:clearable="false"
type="Date"
value-format="YYYY-MM-DD"
placeholder="选择日期"
:placeholder="`选择日期-${placeholder}`"
/>
<el-date-picker
@@ -54,7 +41,7 @@
:clearable="false"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="选择日期时间"
:placeholder="`选择日期时间-${placeholder}`"
/>
<el-time-picker
@@ -69,13 +56,13 @@
v-model="itemValue"
:clearable="false"
value-format="HH:mm:ss"
placeholder="选择时间"
:placeholder="`选择时间-${placeholder}`"
/>
</template>
<script lang="ts" setup>
import { computed, ref, Ref } from 'vue';
import { ElInput } from 'element-plus';
import { ElInput, ElMessage } from 'element-plus';
import { DataType } from '../../dialect/index';
import SvgIcon from '@/components/svgIcon/index.vue';
import MonacoEditorDialog from '@/components/monaco/MonacoEditorDialog';
@@ -130,6 +117,10 @@ const handleBlur = () => {
if (editorOpening.value) {
return;
}
if (props.dataType == DataType.Number && itemValue.value && !/^-?\d*\.?\d+$/.test(itemValue.value)) {
ElMessage.error('输入内容与类型不匹配');
return;
}
emit('update:modelValue', itemValue.value);
emit('blur');
};
@@ -161,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 {
@@ -183,6 +178,10 @@ const getEditorLangByValue = (value: any) => {
.el-input__prefix {
display: none;
}
.el-input__wrapper {
padding: 1px 3px;
}
}
.edit-time-picker-popper {

View File

@@ -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">
@@ -36,7 +37,7 @@
<!-- 字段列的数据类型 -->
<div class="column-type">
<span v-if="column.dataTypeSubscript === 'icon-clock'">
<SvgIcon :size="10" name="Clock" style="cursor: unset" />
<SvgIcon :size="9" name="Clock" style="cursor: unset" />
</span>
<span class="font8" v-else>{{ column.dataTypeSubscript }}</span>
</div>
@@ -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,11 +156,11 @@
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';
import { dateStrFormat } from '@/common/utils/date';
import { formatDate } from '@/common/utils/format';
import { useIntervalFn, useStorage } from '@vueuse/core';
import { ColumnTypeSubscript, compatibleMysql, DataType, DbDialect, getDbDialect } from '../../dialect/index';
import ColumnFormItem from './ColumnFormItem.vue';
@@ -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);
/**
* 行号字段列
@@ -476,9 +473,9 @@ const setTableColumns = (columns: any) => {
state.columns = columns.map((x: any) => {
const columnName = x.columnName;
// 数据类型
x.dataType = dbDialect.getDataType(x.dataType);
x.dataType = dbDialect.getDataType(x.columnType);
x.dataTypeSubscript = ColumnTypeSubscript[x.dataType];
x.remark = `${x.showDataType} ${x.columnComment ? ' | ' + x.columnComment : ''}`;
x.remark = `${x.columnType} ${x.columnComment ? ' | ' + x.columnComment : ''}`;
return {
...x,
@@ -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 });
};
@@ -624,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.data = { ...data };
state.tableDataFormDialog.title = state.table ? `'${props.table}'表单数据` : '表单视图';
state.tableDataFormDialog.visible = true;
};
@@ -645,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];
@@ -674,13 +671,13 @@ const onExportCsv = () => {
columnNames.push(column.columnName);
}
}
exportCsv(`数据导出-${state.table}-${dateStrFormat('yyyyMMddHHmm', new Date().toString())}`, columnNames, dataList);
exportCsv(`数据导出-${state.table}-${formatDate(new Date(), 'yyyyMMddHHmm')}`, columnNames, dataList);
};
const onExportSql = async () => {
const selectionDatas = state.datas;
exportFile(
`数据导出-${state.table}-${dateStrFormat('yyyyMMddHHmm', new Date().toString())}.sql`,
`数据导出-${state.table}-${formatDate(new Date(), 'yyyyMMddHHmm')}.sql`,
await getNowDbInst().genInsertSql(state.db, state.table, selectionDatas)
);
};
@@ -749,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);
@@ -763,7 +760,7 @@ const submitUpdateFields = async () => {
res += await dbInst.genUpdateSql(db, state.table, updateColumnValue, rowData);
}
dbInst.promptExeSql(db, res, cancelUpdateFields, () => {
dbInst.promptExeSql(db, res, null, () => {
triggerRefresh();
cellUpdateMap.clear();
changeUpdatedField();
@@ -810,11 +807,11 @@ const getFormatTimeValue = (dataType: DataType, originValue: string): string =>
switch (dataType) {
case DataType.Time:
return dateStrFormat('HH:mm:ss', originValue);
return formatDate(originValue, 'HH:mm:ss');
case DataType.Date:
return dateStrFormat('yyyy-MM-dd', originValue);
return formatDate(originValue, 'YYYY-MM-DD');
case DataType.DateTime:
return dateStrFormat('yyyy-MM-dd HH:mm:ss', originValue);
return formatDate(originValue, 'YYYY-MM-DD HH:mm:ss');
default:
return originValue;
}
@@ -832,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,
});
@@ -880,8 +889,8 @@ defineExpose({
color: var(--el-color-info-light-3);
font-weight: bold;
position: absolute;
top: -5px;
padding: 2px;
top: -7px;
padding: 1px;
}
.column-right {

View File

@@ -6,10 +6,10 @@
:key="column.columnName"
class="w100 mb5"
:prop="column.columnName"
:required="column.nullable != 'YES' && !column.isPrimaryKey && !column.isIdentity"
:required="props.tableName != '' && !column.nullable && !column.isPrimaryKey && !column.isIdentity"
>
<template #label>
<span class="pointer" :title="`${column.showDataType} | ${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.showDataType} ${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>
@@ -37,7 +37,6 @@ import { ref, watch, onMounted } from 'vue';
import ColumnFormItem from './ColumnFormItem.vue';
import { DbInst } from '../../db';
import { ElMessage } from 'element-plus';
import { getDbDialect } from '@/views/ops/db/dialect';
export interface ColumnFormItemProps {
dbInst: DbInst;
@@ -86,35 +85,35 @@ const closeDialog = () => {
};
const confirm = async () => {
dataForm.value.validate(async (valid: boolean) => {
if (!valid) {
ElMessage.error('请正确填写数据信息');
return false;
}
try {
await dataForm.value.validate();
} catch (e: any) {
ElMessage.error('请正确填写数据信息');
return false;
}
const dbInst = props.dbInst;
const data = modelValue.value;
const db = props.dbName;
const tableName = props.tableName;
const dbInst = props.dbInst;
const data = modelValue.value;
const db = props.dbName;
const tableName = props.tableName;
let sql = '';
if (oldValue) {
const updateColumnValue = {};
Object.keys(oldValue).forEach((key) => {
// 如果新旧值不相等,则为需要更新的字段
if (oldValue[key] !== modelValue.value[key]) {
updateColumnValue[key] = modelValue.value[key];
}
});
sql = await dbInst.genUpdateSql(db, tableName, updateColumnValue, oldValue);
} else {
sql = await dbInst.genInsertSql(db, tableName, [data], true);
}
dbInst.promptExeSql(db, sql, null, () => {
closeDialog();
emit('submitSuccess');
let sql = '';
if (oldValue) {
const updateColumnValue: any = {};
Object.keys(oldValue).forEach((key) => {
// 如果新旧值不相等,则为需要更新的字段
if (oldValue[key] !== modelValue.value[key]) {
updateColumnValue[key] = modelValue.value[key];
}
});
sql = await dbInst.genUpdateSql(db, tableName, updateColumnValue, oldValue);
} else {
sql = await dbInst.genInsertSql(db, tableName, [data], true);
}
dbInst.promptExeSql(db, sql, null, () => {
closeDialog();
emit('submitSuccess');
});
};
</script>

View File

@@ -12,15 +12,29 @@
width="auto"
title="表格字段配置"
trigger="click"
@hide="triggerCheckedColumns"
>
<div v-for="(item, index) in columns" :key="index">
<div><el-input v-model="checkedShowColumns.searchKey" size="small" placeholder="输入列名或备注过滤" /></div>
<div>
<el-checkbox
v-model="item.show"
:label="`${!item.columnComment ? item.columnName : item.columnName + ' [' + item.columnComment + ']'}`"
:true-value="true"
:false-value="false"
v-model="checkedShowColumns.checkedAllColumn"
:indeterminate="checkedShowColumns.isIndeterminate"
@change="handleCheckAllColumnChange"
size="small"
/>
>
选择所有
</el-checkbox>
<el-checkbox-group v-model="checkedShowColumns.columnNames" @change="handleCheckedColumnChange">
<div v-for="(item, index) in filterCheckedColumns" :key="index">
<el-checkbox
:key="index"
:label="`${!item.columnComment ? item.columnName : item.columnName + ' [' + item.columnComment + ']'}`"
:value="item.columnName"
size="small"
/>
</div>
</el-checkbox-group>
</div>
<template #reference>
<el-link icon="Operation" size="small" :underline="false"></el-link>
@@ -36,33 +50,6 @@
</el-tooltip>
<el-divider direction="vertical" border-style="dashed" />
<el-tooltip :show-after="500" class="box-item" effect="dark" content="commit" placement="top">
<template #content>
1. 右击数据/表头可显示操作菜单 <br />
2. 按住Ctrl点击数据则为多选 <br />
3. 双击单元格可编辑数据 <br />
4. 鼠标悬停字段名或标签树的表名可提示相关备注
</template>
<el-link icon="QuestionFilled" :underline="false"> </el-link>
</el-tooltip>
<el-divider direction="vertical" border-style="dashed" />
<!-- 表数据展示配置 -->
<el-popover
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>
@@ -98,7 +85,7 @@
<el-divider direction="vertical" />
<span style="color: var(--el-color-info-light-3)">
{{ item.showDataType }}
{{ item.columnType }}
<template v-if="item.columnComment">
<el-divider direction="vertical" />
@@ -255,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({
@@ -285,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
@@ -329,9 +314,17 @@ const state = reactive({
tableHeight: '600px',
hasUpdatedFileds: false,
dbDialect: {} as DbDialect,
checkedShowColumns: {
searchKey: '',
checkedAllColumn: true,
isIndeterminate: false,
columnNames: [] as any,
},
});
const { datas, condition, loading, columns, pageNum, pageSize, pageSizes, sql, hasUpdatedFileds, conditionDialog, addDataDialog } = toRefs(state);
const { datas, condition, loading, columns, checkedShowColumns, pageNum, pageSize, pageSizes, sql, hasUpdatedFileds, conditionDialog, addDataDialog } =
toRefs(state);
watch(
() => props.tableHeight,
@@ -351,6 +344,8 @@ onMounted(async () => {
state.dbDialect = getNowDbInst().getDialect();
useEventListener('click', handlerWindowClick);
state.checkedShowColumns.columnNames = state.columns.map((item: any) => item.columnName);
});
const handlerWindowClick = () => {
@@ -391,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;
@@ -414,6 +410,7 @@ const handleSetPageNum = async () => {
state.pageNum = state.setPageNum;
await selectData();
};
const handleCount = async () => {
state.counting = true;
@@ -421,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) {
@@ -431,6 +429,24 @@ const handleCount = async () => {
state.counting = false;
};
const handleCheckAllColumnChange = (val: boolean) => {
state.checkedShowColumns.columnNames = val ? state.columns.map((x: any) => x.columnName) : [];
state.checkedShowColumns.isIndeterminate = false;
};
const handleCheckedColumnChange = (value: string[]) => {
const checkedCount = value.length;
state.checkedShowColumns.checkedAllColumn = checkedCount === state.columns.length;
state.checkedShowColumns.isIndeterminate = checkedCount > 0 && checkedCount < state.columns.length;
};
const triggerCheckedColumns = () => {
const checkedColumnNames = state.checkedShowColumns.columnNames;
for (let column of state.columns) {
column.show = checkedColumnNames.includes(column.columnName);
}
};
// 完整的条件,每次选中后会重置条件框内容,故需要这个变量在获取建议时将文本框内容保存
let completeCond = '';
// 是否存在列建议
@@ -444,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;
@@ -490,16 +503,25 @@ const chooseCondColumnName = () => {
* 过滤条件列名
*/
const filterCondColumns = computed(() => {
return filterColumns(state.columnNameSearch);
});
const filterCheckedColumns = computed(() => {
return filterColumns(state.checkedShowColumns.searchKey);
});
const filterColumns = (searchKey: string) => {
const columns = state.columns;
let columnNameSearch = state.columnNameSearch;
if (!columnNameSearch) {
if (!searchKey) {
return columns;
}
columnNameSearch = columnNameSearch.toLowerCase();
return columns.filter((data: any) => {
return data.columnName.toLowerCase().includes(columnNameSearch) || data.columnComment.toLowerCase().includes(columnNameSearch);
});
});
return fuzzyMatchField(
searchKey,
columns,
(x: any) => x.columnName,
(x: any) => x.columnComment
);
};
/**
* 条件查询,点击列信息后显示输入对应的值
@@ -507,7 +529,7 @@ const filterCondColumns = computed(() => {
const onConditionRowClick = (event: any) => {
const row = event[0];
state.conditionDialog.title = `请输入 [${row.columnName}] 的值`;
state.conditionDialog.placeholder = `${row.showDataType} ${row.columnComment}`;
state.conditionDialog.placeholder = `${row.columnType} ${row.columnComment}`;
state.conditionDialog.columnRow = row;
state.conditionDialog.visible = true;
setTimeout(() => {
@@ -583,6 +605,10 @@ const onShowAddDataDialog = async () => {
state.addDataDialog.title = `添加'${props.tableName}'表数据`;
state.addDataDialog.visible = true;
};
defineExpose({
active: () => dbTableRef.value.active(),
});
</script>
<style lang="scss">

View File

@@ -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,7 +152,7 @@ const props = defineProps({
dbType: {
type: String,
},
flowProcdefKey: {
version: {
type: String,
},
});
@@ -160,7 +160,7 @@ const props = defineProps({
//定义事件
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,
flowProcdefKey: props.flowProcdefKey,
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);

View File

@@ -1,9 +1,9 @@
<template>
<div class="db-table">
<el-row class="mb5">
<el-popover v-model:visible="showDumpInfo" :width="470" placement="right" trigger="click">
<el-popover v-model:visible="state.dumpInfo.visible" trigger="click" :width="470" placement="right">
<template #reference>
<el-button class="ml5" type="success" size="small">导出</el-button>
<el-button :disabled="state.dumpInfo.tables?.length == 0" class="ml5" type="success" size="small">导出</el-button>
</template>
<el-form-item label="导出内容: ">
<el-radio-group v-model="dumpInfo.type">
@@ -13,16 +13,15 @@
</el-radio-group>
</el-form-item>
<el-form-item label="导出表: ">
<el-table @selection-change="handleDumpTableSelectionChange" max-height="300" size="small" :data="tables">
<el-table-column type="selection" width="45" />
<el-form-item>
<el-table :data="state.dumpInfo.tables" empty-text="请先选择要导出的表" max-height="300" size="small">
<el-table-column property="tableName" label="表名" min-width="150" show-overflow-tooltip> </el-table-column>
<el-table-column property="tableComment" label="备注" min-width="150" show-overflow-tooltip> </el-table-column>
</el-table>
</el-form-item>
<div style="text-align: right">
<el-button @click="showDumpInfo = false" size="small">取消</el-button>
<el-button @click="state.dumpInfo.visible = false" size="small">取消</el-button>
<el-button @click="dump(db)" type="success" size="small">确定</el-button>
</div>
</el-popover>
@@ -30,7 +29,9 @@
<el-button type="primary" size="small" @click="openEditTable(false)">创建表</el-button>
</el-row>
<el-table v-loading="loading" border stripe :data="filterTableInfos" size="small" :height="height">
<el-table v-loading="loading" @selection-change="handleDumpTableSelectionChange" border stripe :data="filterTableInfos" size="small" :height="height">
<el-table-column type="selection" width="30" />
<el-table-column property="tableName" label="表名" min-width="150" show-overflow-tooltip>
<template #header>
<el-input v-model="tableNameSearch" size="small" placeholder="表名: 输入可过滤" clearable />
@@ -82,7 +83,7 @@
<el-dialog width="40%" :title="`${chooseTableName} 字段信息`" v-model="columnDialog.visible">
<el-table border stripe :data="columnDialog.columns" size="small">
<el-table-column prop="columnName" label="名称" show-overflow-tooltip> </el-table-column>
<el-table-column width="120" prop="showDataType" label="类型" show-overflow-tooltip> </el-table-column>
<el-table-column width="120" prop="columnType" label="类型" show-overflow-tooltip> </el-table-column>
<el-table-column width="80" prop="nullable" label="是否可为空" show-overflow-tooltip> </el-table-column>
<el-table-column prop="columnComment" label="备注" show-overflow-tooltip> </el-table-column>
</el-table>
@@ -108,7 +109,6 @@
:dbId="dbId"
:db="db"
:dbType="dbType"
:flow-procdef-key="props.flowProcdefKey"
:data="tableCreateDialog.data"
v-model:visible="tableCreateDialog.visible"
@submit-sql="onSubmitSql"
@@ -130,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'));
@@ -150,9 +151,6 @@ const props = defineProps({
type: [String],
required: true,
},
flowProcdefKey: {
type: [String],
},
});
const state = reactive({
@@ -161,8 +159,8 @@ const state = reactive({
tables: [],
tableNameSearch: '',
tableCommentSearch: '',
showDumpInfo: false,
dumpInfo: {
visible: false,
id: 0,
db: '',
type: 3,
@@ -201,19 +199,7 @@ const state = reactive({
},
});
const {
loading,
tables,
tableNameSearch,
tableCommentSearch,
showDumpInfo,
dumpInfo,
chooseTableName,
columnDialog,
indexDialog,
ddlDialog,
tableCreateDialog,
} = toRefs(state);
const { loading, tableNameSearch, tableCommentSearch, dumpInfo, chooseTableName, columnDialog, indexDialog, ddlDialog, tableCreateDialog } = toRefs(state);
onMounted(async () => {
getTables();
@@ -230,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 () => {
@@ -259,21 +239,22 @@ const getTables = async () => {
* 选择导出数据库表
*/
const handleDumpTableSelectionChange = (vals: any) => {
state.dumpInfo.tables = vals.map((x: any) => x.tableName);
state.dumpInfo.tables = vals;
};
/**
* 数据库信息导出
*/
const dump = (db: string) => {
isTrue(state.dumpInfo.tables.length > 0, '请选择要导出的表');
isTrue(state.dumpInfo.tables.length > 0, '请选择要导出的表');
const tableNames = state.dumpInfo.tables.map((x: any) => x.tableName);
const a = document.createElement('a');
a.setAttribute(
'href',
`${config.baseApiUrl}/dbs/${props.dbId}/dump?db=${db}&type=${state.dumpInfo.type}&tables=${state.dumpInfo.tables.join(',')}&${joinClientParams()}`
`${config.baseApiUrl}/dbs/${props.dbId}/dump?db=${db}&type=${state.dumpInfo.type}&tables=${tableNames.join(',')}&${joinClientParams()}`
);
a.click();
state.showDumpInfo = false;
state.dumpInfo.visible = false;
};
const showColumns = async (row: any) => {
@@ -327,7 +308,6 @@ const dropTable = async (row: any) => {
sql: `DROP TABLE ${tableName}`,
dbId: props.dbId as any,
db: props.db as any,
flowProcdefKey: props.flowProcdefKey,
runSuccessCallback: async () => {
await getTables();
},

View File

@@ -8,6 +8,8 @@ import { editor, languages, Position } from 'monaco-editor';
import { registerCompletionItemProvider } from '@/components/monaco/completionItemProvider';
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());
@@ -40,11 +42,8 @@ export class DbInst {
*/
type: string;
/**
* 流程定义key若存在则需要审批执行
*/
flowProcdefKey: string;
/** 兼容版本 */
version: string;
/**
* dbName -> db
*/
@@ -225,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;
}
/**
@@ -310,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) {
@@ -359,7 +364,6 @@ export class DbInst {
dbType: this.getDialect().getInfo().formatSqlDialect,
runSuccessCallback: successFunc,
cancelCallback: cancelFunc,
flowProcdefKey: this.flowProcdefKey,
});
};
@@ -377,12 +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.tagPath) {
dbInst.tagPath = inst.tagPath;
}
return dbInst;
}
console.info(`new dbInst: ${inst.id}, tagPath: ${inst.tagPath}`);
@@ -393,7 +402,10 @@ export class DbInst {
dbInst.name = inst.name;
dbInst.type = inst.type;
dbInst.databases = inst.databases;
dbInst.flowProcdefKey = inst.flowProcdefKey;
if (dbInst.databases?.[0]) {
dbInst.version = await dbApi.getCompatibleDbVersion.request({ id: inst.id, db: dbInst.databases?.[0] });
}
dbInstCache.set(dbInst.id, dbInst);
return dbInst;
@@ -402,7 +414,6 @@ export class DbInst {
/**
* 获取数据库实例id若不存在则新建一个并缓存
* @param dbId 数据库实例id
* @param dbType 第一次获取时为必传项,即第一次创建时
* @returns 数据库实例
*/
static getInst(dbId?: number): DbInst {
@@ -413,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));
}
/**
@@ -444,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;
@@ -465,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;
};
@@ -477,17 +507,17 @@ export class DbInst {
}
for (let col of columns) {
if (col.charMaxLength > 0) {
col.showDataType = `${col.dataType}(${col.charMaxLength})`;
col.columnType = `${col.dataType}(${col.charMaxLength})`;
col.showLength = col.charMaxLength;
col.showScale = null;
continue;
}
if (col.numPrecision > 0) {
if (col.numScale > 0) {
col.showDataType = `${col.dataType}(${col.numPrecision},${col.numScale})`;
col.columnType = `${col.dataType}(${col.numPrecision},${col.numScale})`;
col.showScale = col.numScale;
} else {
col.showDataType = `${col.dataType}(${col.numPrecision})`;
col.columnType = `${col.dataType}(${col.numPrecision})`;
col.showScale = null;
}
@@ -495,9 +525,22 @@ export class DbInst {
continue;
}
col.showDataType = col.dataType;
col.columnType = col.dataType;
}
}
/**
* 根据数据库配置信息获取对应的库名列表
* @param db db配置信息
* @returns 库名列表
*/
static async getDbNames(db: any) {
if (db.getDatabaseMode == DbGetDbNamesMode.Assign.value) {
return db.database.split(' ');
}
return await dbApi.getDbNamesByAc.request({ authCertName: db.authCertName });
}
}
/**
@@ -582,6 +625,11 @@ export class TabInfo {
*/
params: any;
/**
* 组件ref
*/
componentRef: any;
getNowDbInst() {
return DbInst.getInst(this.dbId);
}
@@ -625,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;
@@ -818,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,
};

View File

@@ -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());

View 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;
}
}

View File

@@ -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)}'`);
}

View File

@@ -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(';');

View File

@@ -1,11 +1,17 @@
import { EnumValue } from '@/common/Enum';
export const DbGetDbNamesMode = {
Auto: EnumValue.of(-1, '实时获取').setTagType('warning'),
Assign: EnumValue.of(1, '指定库名').setTagType('primary'),
};
// 数据库sql执行类型
export const DbSqlExecTypeEnum = {
Update: EnumValue.of(1, 'UPDATE').setTagColor('#E4F5EB'),
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'),
};
@@ -37,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'),
};

View File

@@ -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,

Some files were not shown because too many files have changed in this diff Show More