mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-02 15:30:25 +08:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f43851698e | ||
|
|
73884bb693 | ||
|
|
1b5bb1de8b | ||
|
|
4814793546 | ||
|
|
d85bbff270 | ||
|
|
bb1522f4dc | ||
|
|
a7632fbf58 | ||
|
|
c4cb4234fd | ||
|
|
89e12678eb | ||
|
|
137ebb8e9e | ||
|
|
05625bd8c1 | ||
|
|
4afeac5fdd | ||
|
|
1d0e91f1af | ||
|
|
cf5111a325 | ||
|
|
78957a8ebd | ||
|
|
29fd5a25d2 |
@@ -22,7 +22,7 @@
|
||||
|
||||
### 介绍
|
||||
|
||||
web 版 **linux(终端[终端回放、命令过滤] 文件 脚本 进程 计划任务)、数据库(mysql postgres oracle sqlserver 达梦 高斯 sqlite)数据同步 数据迁移、redis(单机 哨兵 集群)、mongo 等集工单流程审批于一体的统一管理操作平台**
|
||||
web 版 **linux(终端[终端回放、命令过滤] 文件 脚本 进程 计划任务)、数据库(mysql postgres oracle sqlserver 达梦 高斯 sqlite)数据操作 数据同步 数据迁移、redis(单机 哨兵 集群)、mongo 等集工单流程审批于一体的统一管理操作平台**
|
||||
|
||||
### 开发语言与主要框架
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -10,19 +10,20 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@vueuse/core": "^10.9.0",
|
||||
"@vueuse/core": "^10.10.0",
|
||||
"asciinema-player": "^3.7.1",
|
||||
"axios": "^1.6.2",
|
||||
"clipboard": "^2.0.11",
|
||||
"cropperjs": "^1.6.1",
|
||||
"dayjs": "^1.11.11",
|
||||
"echarts": "^5.5.0",
|
||||
"element-plus": "^2.7.2",
|
||||
"element-plus": "^2.7.4",
|
||||
"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-editor": "^0.49.0",
|
||||
"monaco-sql-languages": "^0.12.0",
|
||||
"monaco-themes": "^0.4.4",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^2.1.7",
|
||||
@@ -33,7 +34,7 @@
|
||||
"sql-formatter": "^15.0.2",
|
||||
"trzsz": "^1.1.5",
|
||||
"uuid": "^9.0.1",
|
||||
"vue": "^3.4.25",
|
||||
"vue": "^3.4.27",
|
||||
"vue-router": "^4.3.2",
|
||||
"xterm": "^5.3.0",
|
||||
"xterm-addon-fit": "^0.8.0",
|
||||
@@ -48,15 +49,15 @@
|
||||
"@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",
|
||||
"@vue/compiler-sfc": "^3.4.27",
|
||||
"code-inspector-plugin": "^0.4.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint": "^8.35.0",
|
||||
"eslint-plugin-vue": "^9.25.0",
|
||||
"prettier": "^3.2.5",
|
||||
"sass": "^1.75.0",
|
||||
"sass": "^1.77.1",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.2.10",
|
||||
"vite": "^5.2.12",
|
||||
"vue-eslint-parser": "^9.4.2"
|
||||
},
|
||||
"browserslist": [
|
||||
|
||||
@@ -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.8.7',
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
export function dateFormat2(fmt: string, date: Date) {
|
||||
let ret;
|
||||
const opt = {
|
||||
'y+': date.getFullYear().toString(), // 年
|
||||
'M+': (date.getMonth() + 1).toString(), // 月
|
||||
'd+': date.getDate().toString(), // 日
|
||||
'H+': date.getHours().toString(), // 时
|
||||
'm+': date.getMinutes().toString(), // 分
|
||||
's+': date.getSeconds().toString(), // 秒
|
||||
'S+': date.getMilliseconds() ? date.getMilliseconds().toString() : '', // 毫秒
|
||||
// 有其他格式化字符需求可以继续添加,必须转化成字符串
|
||||
};
|
||||
for (const k in opt) {
|
||||
ret = new RegExp('(' + k + ')').exec(fmt);
|
||||
if (ret) {
|
||||
fmt = fmt.replace(ret[1], ret[1].length == 1 ? opt[k] : opt[k].padStart(ret[1].length, '0'));
|
||||
}
|
||||
}
|
||||
return fmt;
|
||||
}
|
||||
|
||||
export function dateStrFormat(fmt: string, dateStr: string) {
|
||||
return dateFormat2(fmt, new Date(dateStr));
|
||||
}
|
||||
|
||||
export function dateFormat(dateStr: string) {
|
||||
if (!dateStr) {
|
||||
return '';
|
||||
}
|
||||
return dateFormat2('yyyy-MM-dd HH:mm:ss', new Date(dateStr));
|
||||
}
|
||||
@@ -1,3 +1,18 @@
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
* @param date 日期 字符串 Date 时间戳等
|
||||
* @param format 格式化格式 默认 YYYY-MM-DD HH:mm:ss
|
||||
* @returns 格式化后内容
|
||||
*/
|
||||
export function formatDate(date: any, format: string = 'YYYY-MM-DD HH:mm:ss') {
|
||||
if (!date) {
|
||||
return '';
|
||||
}
|
||||
return dayjs(date).format(format);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化字节单位
|
||||
* @param size byte size
|
||||
@@ -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为秒单位)
|
||||
*
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -97,43 +97,6 @@ export function getTextWidth(str: string) {
|
||||
return width;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取内容所需要占用的宽度
|
||||
*/
|
||||
export function getContentWidth(content: any): number {
|
||||
if (!content) {
|
||||
return 50;
|
||||
}
|
||||
// 以下分配的单位长度可根据实际需求进行调整
|
||||
let flexWidth = 0;
|
||||
for (const char of content) {
|
||||
if (flexWidth > 500) {
|
||||
break;
|
||||
}
|
||||
if ((char >= '0' && char <= '9') || (char >= 'a' && char <= 'z')) {
|
||||
// 小写字母、数字字符
|
||||
flexWidth += 9.3;
|
||||
continue;
|
||||
}
|
||||
if (char >= 'A' && char <= 'Z') {
|
||||
flexWidth += 9;
|
||||
continue;
|
||||
}
|
||||
if (char >= '\u4e00' && char <= '\u9fa5') {
|
||||
// 如果是中文字符,为字符分配16个单位宽度
|
||||
flexWidth += 20;
|
||||
} else {
|
||||
// 其他种类字符
|
||||
flexWidth += 8;
|
||||
}
|
||||
}
|
||||
// if (flexWidth > 450) {
|
||||
// // 设置最大宽度
|
||||
// flexWidth = 450;
|
||||
// }
|
||||
return flexWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns uuid
|
||||
@@ -179,3 +142,38 @@ export async function copyToClipboard(txt: string, selector: string = '#copyValu
|
||||
clipboard.destroy();
|
||||
});
|
||||
}
|
||||
|
||||
export function fuzzyMatchField(keyword: string, fields: any[], ...valueExtractFuncs: Function[]) {
|
||||
keyword = keyword?.toLowerCase();
|
||||
return fields.filter((field) => {
|
||||
for (let valueExtractFunc of valueExtractFuncs) {
|
||||
const value = valueExtractFunc(field)?.toLowerCase();
|
||||
if (isPrefixSubsequence(keyword, value)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 匹配是否为前缀子序列 targetTemplate=username prefix=uname -> true,prefix=uname2 -> false
|
||||
* @param prefix 字符串前缀(不连续也可以,但不改变字符的相对顺序)
|
||||
* @param targetTemplate 目标模板
|
||||
* @returns 是否匹配
|
||||
*/
|
||||
export function isPrefixSubsequence(prefix: string, targetTemplate: string) {
|
||||
let i = 0; // 指向prefix的索引
|
||||
let j = 0; // 指向targetTemplate的索引
|
||||
|
||||
while (i < prefix.length && j < targetTemplate.length) {
|
||||
if (prefix[i] === targetTemplate[j]) {
|
||||
// 字符匹配,两个指针都向前移动
|
||||
i++;
|
||||
}
|
||||
j++; // 目标字符串指针始终向前移动
|
||||
}
|
||||
|
||||
// 如果prefix的所有字符都被找到,返回true
|
||||
return i === prefix.length;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -67,7 +67,7 @@ const state = reactive({
|
||||
search: null as any,
|
||||
weblinks: null as any,
|
||||
},
|
||||
status: TerminalStatus.NoConnected,
|
||||
status: -11,
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
@@ -96,6 +96,7 @@ onBeforeUnmount(() => {
|
||||
});
|
||||
|
||||
function init() {
|
||||
state.status = TerminalStatus.NoConnected;
|
||||
if (term) {
|
||||
console.log('重新连接...');
|
||||
close();
|
||||
@@ -105,7 +106,7 @@ function init() {
|
||||
});
|
||||
}
|
||||
|
||||
function initTerm() {
|
||||
async function initTerm() {
|
||||
term = new Terminal({
|
||||
fontSize: themeConfig.value.terminalFontSize || 15,
|
||||
fontWeight: themeConfig.value.terminalFontWeight || 'normal',
|
||||
@@ -155,6 +156,7 @@ function initSocket() {
|
||||
state.status = TerminalStatus.Connected;
|
||||
|
||||
focus();
|
||||
fitTerminal();
|
||||
|
||||
// 如果有初始要执行的命令,则发送执行命令
|
||||
if (props.cmd) {
|
||||
@@ -209,7 +211,6 @@ function loadAddon() {
|
||||
// tell trzsz the terminal columns has been changed
|
||||
trzsz.setTerminalColumns(size.cols);
|
||||
});
|
||||
window.addEventListener('resize', () => state.addon.fit.fit());
|
||||
// enable drag files or directories to upload
|
||||
terminalRef.value.addEventListener('dragover', (event: Event) => event.preventDefault());
|
||||
terminalRef.value.addEventListener('drop', (event: any) => {
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import EnumValue from '@/common/Enum';
|
||||
|
||||
export enum TerminalStatus {
|
||||
Error = -1,
|
||||
NoConnected = 0,
|
||||
Connected = 1,
|
||||
Disconnected = 2,
|
||||
}
|
||||
|
||||
export const TerminalStatusEnum = {
|
||||
Error: EnumValue.of(TerminalStatus.Error, '连接出错').setExtra({ iconColor: 'var(--el-color-error)' }),
|
||||
NoConnected: EnumValue.of(TerminalStatus.NoConnected, '未连接').setExtra({ iconColor: 'var(--el-color-primary)' }),
|
||||
Connected: EnumValue.of(TerminalStatus.Connected, '连接成功').setExtra({ iconColor: 'var(--el-color-success)' }),
|
||||
Disconnected: EnumValue.of(TerminalStatus.Disconnected, '连接失败').setExtra({ iconColor: 'var(--el-color-error)' }),
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
@@ -21,6 +21,10 @@
|
||||
<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 +74,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: {
|
||||
@@ -115,6 +121,7 @@ const state = reactive({
|
||||
remark: null,
|
||||
// 流程的审批节点任务
|
||||
tasks: '',
|
||||
codePaths: [],
|
||||
},
|
||||
sortable: '' as any,
|
||||
});
|
||||
@@ -126,6 +133,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);
|
||||
@@ -160,25 +168,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 = () => {
|
||||
|
||||
@@ -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,6 +60,7 @@ 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(),
|
||||
];
|
||||
|
||||
@@ -17,11 +17,11 @@
|
||||
<AccountInfo :account-id="procinst.creatorId" :username="procinst.creator" />
|
||||
<!-- {{ procinst.creator }} -->
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="发起时间">{{ dateFormat(procinst.createTime) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="发起时间">{{ formatDate(procinst.createTime) }}</el-descriptions-item>
|
||||
|
||||
<div v-if="procinst.duration">
|
||||
<el-descriptions-item label="持续时间">{{ formatTime(procinst.duration) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="结束时间">{{ dateFormat(procinst.endTime) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="结束时间">{{ formatDate(procinst.endTime) }}</el-descriptions-item>
|
||||
</div>
|
||||
|
||||
<el-descriptions-item label="流程状态">
|
||||
@@ -86,11 +86,11 @@ 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'));
|
||||
|
||||
@@ -2,7 +2,7 @@ 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}'),
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -35,7 +35,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,7 +84,7 @@
|
||||
<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>
|
||||
@@ -118,7 +118,7 @@
|
||||
<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>
|
||||
@@ -159,7 +159,7 @@
|
||||
<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>
|
||||
@@ -198,7 +198,7 @@
|
||||
<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>
|
||||
@@ -228,7 +228,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>
|
||||
@@ -257,7 +257,7 @@ 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 { formatDate } from '@/common/utils/format';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import { resourceOpLogApi } from '../ops/tag/api';
|
||||
|
||||
@@ -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';
|
||||
@@ -279,19 +279,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,22 +328,23 @@ 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) => {
|
||||
@@ -366,7 +368,7 @@ const loginResDeal = (loginRes: any) => {
|
||||
const token = loginRes.token;
|
||||
// 如果不需要 otp校验,则该token即为accessToken,否则为otp校验token
|
||||
if (loginRes.otp == -1) {
|
||||
signInSuccess(token);
|
||||
signInSuccess(token, loginRes.refresh_token);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -379,12 +381,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 +421,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 = () => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 { onMounted, reactive, ref, watch, toRefs, nextTick } 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,6 +141,8 @@ const loadNode = async (node: any, resolve: any) => {
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
// 调用 reject 以保持节点状态,并允许远程加载继续。
|
||||
return reject();
|
||||
}
|
||||
return resolve(nodes);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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.code }}
|
||||
<span style="color: #3c8dbc">【</span>
|
||||
{{ data.name }}
|
||||
<span style="color: #3c8dbc">】</span>
|
||||
<el-tag v-if="data.children !== null" size="small">{{ data.children.length }} </el-tag>
|
||||
</span>
|
||||
</span>
|
||||
</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>
|
||||
|
||||
@@ -199,3 +199,16 @@ export function getTagTypeCodeByPath(codePath: string) {
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function expandCodePath(codePath: string) {
|
||||
const parts = codePath.split('/');
|
||||
const result = [];
|
||||
let currentPath = '';
|
||||
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
currentPath += parts[i] + '/';
|
||||
result.push(currentPath);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
</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 +39,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 +56,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 +70,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,12 +83,10 @@
|
||||
</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';
|
||||
|
||||
@@ -90,6 +94,7 @@ 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: {
|
||||
@@ -151,10 +156,10 @@ const rules = {
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
database: [
|
||||
getDatabaseMode: [
|
||||
{
|
||||
required: true,
|
||||
message: '请添加数据库',
|
||||
message: '请选择库名获取方式',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
@@ -176,39 +181,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 +233,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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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 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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -259,6 +259,7 @@ const handleSrcTableCheckChange = (data: { id: string; name: string }, checked:
|
||||
}
|
||||
}
|
||||
if (data.id && (data.id + '').startsWith('list-item')) {
|
||||
//
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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: {
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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,61 @@
|
||||
<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>
|
||||
|
||||
<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 +148,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 class="font12">{{ dt.label }}</span>
|
||||
</template>
|
||||
<template #default>
|
||||
<el-descriptions :column="1" size="small">
|
||||
<el-descriptions-item label="tagPath">
|
||||
@@ -132,6 +177,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 +186,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 +195,7 @@
|
||||
:db-id="dt.params.id"
|
||||
:db="dt.params.db"
|
||||
:db-type="dt.params.type"
|
||||
:flow-procdef-key="dt.params.flowProcdefKey"
|
||||
:flow-procdef="dt.params.flowProcdef"
|
||||
:height="state.tablesOpHeight"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
@@ -163,7 +210,7 @@
|
||||
:dbId="tableCreateDialog.dbId"
|
||||
:db="tableCreateDialog.db"
|
||||
:dbType="tableCreateDialog.dbType"
|
||||
:flow-procdef-key="tableCreateDialog.flowProcdefKey"
|
||||
:flow-procdef="tableCreateDialog.flowProcdef"
|
||||
:data="tableCreateDialog.data"
|
||||
v-model:visible="tableCreateDialog.visible"
|
||||
@submit-sql="onSubmitEditTableSql"
|
||||
@@ -186,10 +233,11 @@ import { getDbDialect, schemaDbTypes } from './dialect/index';
|
||||
import { sleep } from '@/common/utils/loading';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import { Pane, Splitpanes } from 'splitpanes';
|
||||
import { useEventListener } from '@vueuse/core';
|
||||
import { useEventListener, useStorage } from '@vueuse/core';
|
||||
import SqlExecBox from '@/views/ops/db/component/sqleditor/SqlExecBox';
|
||||
import { useAutoOpenResource } from '@/store/autoOpenResource';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { procdefApi } from '@/views/flow/api';
|
||||
|
||||
const DbTableOp = defineAsyncComponent(() => import('./component/table/DbTableOp.vue'));
|
||||
const DbSqlEditor = defineAsyncComponent(() => import('./component/sqleditor/DbSqlEditor.vue'));
|
||||
@@ -243,7 +291,7 @@ const nodeClickChangeDb = (nodeData: TagTreeNode) => {
|
||||
type: params.type,
|
||||
tagPath: params.tagPath,
|
||||
databases: params.dbs,
|
||||
flowProcdefKey: params.flowProcdefKey,
|
||||
flowProcdef: params.flowProcdef,
|
||||
},
|
||||
params.db
|
||||
);
|
||||
@@ -271,10 +319,11 @@ 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 flowProcdef = await procdefApi.getByResource.request({ resourceType: TagResourceTypeEnum.DbName.value, resourceCode: params.code });
|
||||
return dbs.map((x: any) => {
|
||||
return new TagTreeNode(`${parentNode.key}.${x}`, x, NodeTypeDb)
|
||||
.withParams({
|
||||
@@ -285,7 +334,7 @@ const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((p
|
||||
host: `${params.host}:${params.port}`,
|
||||
dbs: dbs,
|
||||
db: x,
|
||||
flowProcdefKey: params.flowProcdefKey,
|
||||
flowProcdef: flowProcdef,
|
||||
})
|
||||
.withIcon(DbIcon);
|
||||
});
|
||||
@@ -346,7 +395,7 @@ 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, flowProcdef, schema } = params;
|
||||
// 获取当前库的所有表信息
|
||||
let tables = await DbInst.getInst(id).loadTables(db, state.reloadStatus);
|
||||
state.reloadStatus = false;
|
||||
@@ -362,7 +411,7 @@ const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu)
|
||||
db,
|
||||
type,
|
||||
schema,
|
||||
flowProcdefKey: flowProcdefKey,
|
||||
flowProcdef: flowProcdef,
|
||||
key: key,
|
||||
parentKey: parentNode.key,
|
||||
tableName: x.tableName,
|
||||
@@ -448,7 +497,7 @@ const state = reactive({
|
||||
dbId: 0,
|
||||
db: '',
|
||||
dbType: '',
|
||||
flowProcdefKey: '',
|
||||
flowProcdef: null as any,
|
||||
data: {},
|
||||
parentKey: '',
|
||||
},
|
||||
@@ -456,6 +505,8 @@ const state = reactive({
|
||||
|
||||
const { nowDbInst, tableCreateDialog } = toRefs(state);
|
||||
|
||||
const dbConfig = useStorage('dbConfig', { showColumnComment: false, locationTreeNode: false });
|
||||
|
||||
const serverInfoReqParam = ref({
|
||||
instanceId: 0,
|
||||
});
|
||||
@@ -497,7 +548,7 @@ const autoOpenDb = (codePath: string) => {
|
||||
// 置空
|
||||
autoOpenResourceStore.setDbCodePath('');
|
||||
tagTreeRef.value.setCurrentKey(dbCode);
|
||||
}, 600);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -530,7 +581,7 @@ const loadTableData = async (db: any, dbName: string, tableName: string) => {
|
||||
}
|
||||
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,则直接返回
|
||||
@@ -565,7 +616,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 +625,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);
|
||||
@@ -611,7 +662,7 @@ const addTablesOpTab = async (db: any) => {
|
||||
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 +693,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 +727,23 @@ 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);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 定位至当前树节点
|
||||
*/
|
||||
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 +775,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, flowProcdef } = data.params;
|
||||
// data.label就是表名
|
||||
if (tableName) {
|
||||
state.tableCreateDialog.title = '修改表';
|
||||
@@ -719,12 +794,12 @@ const onEditTable = async (data: any) => {
|
||||
state.tableCreateDialog.dbId = id;
|
||||
state.tableCreateDialog.db = db;
|
||||
state.tableCreateDialog.dbType = type;
|
||||
state.tableCreateDialog.flowProcdefKey = flowProcdefKey;
|
||||
state.tableCreateDialog.flowProcdef = flowProcdef;
|
||||
state.tableCreateDialog.visible = true;
|
||||
};
|
||||
|
||||
const onDeleteTable = async (data: any) => {
|
||||
let { db, id, tableName, parentKey, flowProcdefKey, schema } = data.params;
|
||||
let { db, id, tableName, parentKey, flowProcdef, schema } = data.params;
|
||||
await ElMessageBox.confirm(`此操作是永久性且无法撤销,确定删除【${tableName}】? `, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
@@ -736,7 +811,7 @@ const onDeleteTable = async (data: any) => {
|
||||
let schemaStr = schema ? `${dialect.quoteIdentifier(schema)}.` : '';
|
||||
|
||||
dbApi.sqlExec.request({ id, db, sql: `drop table ${schemaStr + dialect.quoteIdentifier(tableName)}` }).then(() => {
|
||||
if (flowProcdefKey) {
|
||||
if (flowProcdef) {
|
||||
ElMessage.success('工单提交成功');
|
||||
return;
|
||||
}
|
||||
@@ -748,7 +823,7 @@ const onDeleteTable = async (data: any) => {
|
||||
};
|
||||
|
||||
const onRenameTable = async (data: any) => {
|
||||
let { db, id, tableName, parentKey, flowProcdefKey } = data.params;
|
||||
let { db, id, tableName, parentKey, flowProcdef } = data.params;
|
||||
let tableData = { db, oldTableName: tableName, tableName };
|
||||
|
||||
let value = ref(tableName);
|
||||
@@ -771,7 +846,7 @@ const onRenameTable = async (data: any) => {
|
||||
dbId: id as any,
|
||||
db: db as any,
|
||||
dbType: nowDbInst.value.getDialect().getInfo().formatSqlDialect,
|
||||
flowProcdefKey: flowProcdefKey,
|
||||
flowProcdef: flowProcdef,
|
||||
runSuccessCallback: () => {
|
||||
setTimeout(() => {
|
||||
parentKey && reloadNode(parentKey);
|
||||
@@ -831,7 +906,7 @@ const getNowDbInfo = () => {
|
||||
name: di.name,
|
||||
type: di.type,
|
||||
host: di.host,
|
||||
flowProcdefKey: di.flowProcdefKey,
|
||||
flowProcdef: di.flowProcdef,
|
||||
dbName: state.db,
|
||||
};
|
||||
};
|
||||
@@ -839,11 +914,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 +927,7 @@ const getNowDbInfo = () => {
|
||||
margin: 0 0 5px;
|
||||
|
||||
.el-tabs__item {
|
||||
padding: 0 10px;
|
||||
padding: 0 5px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -115,7 +115,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>
|
||||
|
||||
@@ -23,7 +23,10 @@ export const dbApi = {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log(param.sql);
|
||||
}
|
||||
param.sql = Base64.encode(param.sql);
|
||||
// 非base64编码sql,则进行base64编码(refreshToken时,会重复调用该方法,故简单判断下)
|
||||
if (!Base64.isValid(param.sql)) {
|
||||
param.sql = Base64.encode(param.sql);
|
||||
}
|
||||
}
|
||||
return param;
|
||||
}),
|
||||
@@ -40,6 +43,7 @@ export const dbApi = {
|
||||
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'),
|
||||
|
||||
@@ -25,6 +25,7 @@ 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: {
|
||||
@@ -101,9 +102,9 @@ const noSchemaType = (type: string) => {
|
||||
};
|
||||
|
||||
// 数据库实例节点类型
|
||||
const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((parentNode: TagTreeNode) => {
|
||||
const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const params = parentNode.params;
|
||||
const dbs = params.database.split(' ')?.sort();
|
||||
const dbs = (await DbInst.getDbNames(params))?.sort();
|
||||
let fn: NodeType;
|
||||
if (noSchemaType(params.type)) {
|
||||
fn = MysqlNodeTypes;
|
||||
|
||||
@@ -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,44 +296,37 @@ const onRunSql = async (newTab = false) => {
|
||||
notBlank(sql && sql.trim(), '请选中需要执行的sql');
|
||||
// 去除字符串前的空格、换行等
|
||||
sql = sql.replace(/(^\s*)/g, '');
|
||||
let execRemark = '';
|
||||
let canRun = true;
|
||||
|
||||
// 简单截取前十个字符
|
||||
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;
|
||||
sqlPrefix.startsWith('create');
|
||||
|
||||
// 启用工单审批
|
||||
if (nonQuery && getNowDbInst().flowProcdef) {
|
||||
try {
|
||||
getNowDbInst().promptExeSql(props.dbName, sql, null, () => {
|
||||
ElMessage.success('工单提交成功');
|
||||
});
|
||||
} catch (e) {
|
||||
ElMessage.success('工单提交失败');
|
||||
}
|
||||
}
|
||||
if (!canRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 启用工单审批
|
||||
if (execRemark && getNowDbInst().flowProcdefKey) {
|
||||
try {
|
||||
await getNowDbInst().runSql(props.dbName, sql, execRemark);
|
||||
ElMessage.success('工单提交成功');
|
||||
return;
|
||||
} catch (e) {
|
||||
ElMessage.success('工单提交失败');
|
||||
return;
|
||||
}
|
||||
let execRemark;
|
||||
if (nonQuery) {
|
||||
const res: any = await ElMessageBox.prompt('请输入备注', 'Tip', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputErrorMessage: '输入执行该sql的备注信息',
|
||||
});
|
||||
execRemark = res.value;
|
||||
}
|
||||
|
||||
let execRes: ExecResTab;
|
||||
@@ -707,6 +700,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">
|
||||
|
||||
@@ -6,7 +6,7 @@ export type SqlExecProps = {
|
||||
dbId: number;
|
||||
db: string;
|
||||
dbType?: string;
|
||||
flowProcdefKey?: string;
|
||||
flowProcdef?: any;
|
||||
runSuccessCallback?: Function;
|
||||
cancelCallback?: Function;
|
||||
};
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
<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 ? '执行备注(必填)' : '执行备注(选填)'"
|
||||
:placeholder="props.flowProcdef ? '执行备注(必填)' : '执行备注(选填)'"
|
||||
class="mt5"
|
||||
/>
|
||||
|
||||
<div v-if="props.flowProcdefKey">
|
||||
<div v-if="props.flowProcdef">
|
||||
<el-divider content-position="left">审批节点</el-divider>
|
||||
<procdef-tasks :procdef-key="props.flowProcdefKey" />
|
||||
<procdef-tasks :procdef="props.flowProcdef" />
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
@@ -59,13 +59,15 @@ onMounted(() => {
|
||||
*/
|
||||
const runSql = async () => {
|
||||
// 存在流程审批,则备注为必填
|
||||
if (!state.remark && props.flowProcdefKey) {
|
||||
if (!state.remark && props.flowProcdef) {
|
||||
ElMessage.error('请输入执行的备注信息');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
state.btnLoading = true;
|
||||
runSuccess = true;
|
||||
|
||||
const res = await dbApi.sqlExec.request({
|
||||
id: props.dbId,
|
||||
db: props.db,
|
||||
@@ -74,8 +76,7 @@ const runSql = async () => {
|
||||
});
|
||||
|
||||
// 存在流程审批
|
||||
if (props.flowProcdefKey) {
|
||||
runSuccess = false;
|
||||
if (props.flowProcdef) {
|
||||
ElMessage.success('工单提交成功');
|
||||
return;
|
||||
}
|
||||
@@ -87,7 +88,6 @@ const runSql = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
runSuccess = true;
|
||||
ElMessage.success('执行成功');
|
||||
} catch (e) {
|
||||
runSuccess = false;
|
||||
@@ -96,9 +96,9 @@ const runSql = async () => {
|
||||
if (props.runSuccessCallback) {
|
||||
props.runSuccessCallback();
|
||||
}
|
||||
cancel();
|
||||
}
|
||||
state.btnLoading = false;
|
||||
cancel();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -113,7 +113,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();
|
||||
|
||||
@@ -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()"
|
||||
@@ -75,7 +62,7 @@
|
||||
|
||||
<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 {
|
||||
|
||||
@@ -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>
|
||||
@@ -161,7 +160,7 @@ import { DbInst } 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';
|
||||
@@ -476,9 +475,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 +485,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,
|
||||
@@ -617,6 +616,10 @@ const onDeleteData = async () => {
|
||||
const db = state.db;
|
||||
const dbInst = getNowDbInst();
|
||||
dbInst.promptExeSql(db, await dbInst.genDeleteByPrimaryKeysSql(db, state.table, deleteDatas as any), null, () => {
|
||||
// 存在流程则恢复原值,需工单流程审批完后自动执行
|
||||
if (dbInst.flowProcdef) {
|
||||
return;
|
||||
}
|
||||
emits('dataDelete', deleteDatas);
|
||||
});
|
||||
};
|
||||
@@ -628,7 +631,7 @@ const onEditRowData = () => {
|
||||
return;
|
||||
}
|
||||
const data = selectionDatas[0];
|
||||
state.tableDataFormDialog.data = data;
|
||||
state.tableDataFormDialog.data = { ...data };
|
||||
state.tableDataFormDialog.title = `编辑表'${props.table}'数据`;
|
||||
state.tableDataFormDialog.visible = true;
|
||||
};
|
||||
@@ -674,13 +677,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)
|
||||
);
|
||||
};
|
||||
@@ -763,7 +766,12 @@ const submitUpdateFields = async () => {
|
||||
res += await dbInst.genUpdateSql(db, state.table, updateColumnValue, rowData);
|
||||
}
|
||||
|
||||
dbInst.promptExeSql(db, res, cancelUpdateFields, () => {
|
||||
dbInst.promptExeSql(db, res, null, () => {
|
||||
// 存在流程则恢复原值,需工单流程审批完后自动执行
|
||||
if (dbInst.flowProcdef) {
|
||||
cancelUpdateFields();
|
||||
return;
|
||||
}
|
||||
triggerRefresh();
|
||||
cellUpdateMap.clear();
|
||||
changeUpdatedField();
|
||||
@@ -810,11 +818,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 +840,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 +900,8 @@ defineExpose({
|
||||
color: var(--el-color-info-light-3);
|
||||
font-weight: bold;
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
padding: 2px;
|
||||
top: -7px;
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
.column-right {
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
:key="column.columnName"
|
||||
class="w100 mb5"
|
||||
:prop="column.columnName"
|
||||
:required="column.nullable != 'YES' && !column.isPrimaryKey && !column.isIdentity"
|
||||
:required="!column.nullable && !column.isPrimaryKey && !column.isIdentity"
|
||||
>
|
||||
<template #label>
|
||||
<span class="pointer" :title="`${column.showDataType} | ${column.columnComment}`">
|
||||
<span class="pointer" :title="`${column.columnType} | ${column.columnComment}`">
|
||||
{{ column.columnName }}
|
||||
</span>
|
||||
</template>
|
||||
@@ -17,7 +17,7 @@
|
||||
<ColumnFormItem
|
||||
v-model="modelValue[`${column.columnName}`]"
|
||||
:data-type="dbInst.getDialect().getDataType(column.dataType)"
|
||||
:placeholder="`${column.showDataType} ${column.columnComment}`"
|
||||
:placeholder="`${column.columnType} ${column.columnComment}`"
|
||||
:column-name="column.columnName"
|
||||
:disabled="column.isIdentity"
|
||||
/>
|
||||
@@ -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 = {};
|
||||
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>
|
||||
|
||||
@@ -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,17 +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"
|
||||
@@ -98,7 +101,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" />
|
||||
@@ -256,7 +259,7 @@ 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 { copyToClipboard, fuzzyMatchField } from '@/common/utils/string';
|
||||
import DbTableDataForm from './DbTableDataForm.vue';
|
||||
|
||||
const props = defineProps({
|
||||
@@ -329,9 +332,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 +362,8 @@ onMounted(async () => {
|
||||
|
||||
state.dbDialect = getNowDbInst().getDialect();
|
||||
useEventListener('click', handlerWindowClick);
|
||||
|
||||
state.checkedShowColumns.columnNames = state.columns.map((item: any) => item.columnName);
|
||||
});
|
||||
|
||||
const handlerWindowClick = () => {
|
||||
@@ -414,6 +427,7 @@ const handleSetPageNum = async () => {
|
||||
state.pageNum = state.setPageNum;
|
||||
await selectData();
|
||||
};
|
||||
|
||||
const handleCount = async () => {
|
||||
state.counting = true;
|
||||
|
||||
@@ -431,6 +445,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 +476,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 +519,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 +545,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 +621,10 @@ const onShowAddDataDialog = async () => {
|
||||
state.addDataDialog.title = `添加'${props.tableName}'表数据`;
|
||||
state.addDataDialog.visible = true;
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
active: () => dbTableRef.value.active(),
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -152,8 +152,8 @@ const props = defineProps({
|
||||
dbType: {
|
||||
type: String,
|
||||
},
|
||||
flowProcdefKey: {
|
||||
type: String,
|
||||
flowProcdef: {
|
||||
type: Object,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -335,7 +335,7 @@ const submit = async () => {
|
||||
dbId: props.dbId as any,
|
||||
db: props.db as any,
|
||||
dbType: dbDialect.getInfo().formatSqlDialect,
|
||||
flowProcdefKey: props.flowProcdefKey,
|
||||
flowProcdef: props.flowProcdef,
|
||||
runSuccessCallback: () => {
|
||||
emit('submit-sql', { tableName: state.tableData.tableName });
|
||||
// cancel();
|
||||
|
||||
@@ -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,7 @@
|
||||
:dbId="dbId"
|
||||
:db="db"
|
||||
:dbType="dbType"
|
||||
:flow-procdef-key="props.flowProcdefKey"
|
||||
:flow-procdef="props.flowProcdef"
|
||||
:data="tableCreateDialog.data"
|
||||
v-model:visible="tableCreateDialog.visible"
|
||||
@submit-sql="onSubmitSql"
|
||||
@@ -130,6 +131,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,8 +152,8 @@ const props = defineProps({
|
||||
type: [String],
|
||||
required: true,
|
||||
},
|
||||
flowProcdefKey: {
|
||||
type: [String],
|
||||
flowProcdef: {
|
||||
type: [Object],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -161,8 +163,8 @@ const state = reactive({
|
||||
tables: [],
|
||||
tableNameSearch: '',
|
||||
tableCommentSearch: '',
|
||||
showDumpInfo: false,
|
||||
dumpInfo: {
|
||||
visible: false,
|
||||
id: 0,
|
||||
db: '',
|
||||
type: 3,
|
||||
@@ -201,19 +203,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 +220,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 +243,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 +312,7 @@ const dropTable = async (row: any) => {
|
||||
sql: `DROP TABLE ${tableName}`,
|
||||
dbId: props.dbId as any,
|
||||
db: props.db as any,
|
||||
flowProcdefKey: props.flowProcdefKey,
|
||||
flowProcdef: props.flowProcdef,
|
||||
runSuccessCallback: async () => {
|
||||
await getTables();
|
||||
},
|
||||
|
||||
@@ -8,6 +8,7 @@ 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';
|
||||
|
||||
const hintsStorage: RemovableRef<Map<string, any>> = useLocalStorage('db-table-hints', new Map());
|
||||
const tableStorage: RemovableRef<Map<string, any>> = useLocalStorage('db-tables', new Map());
|
||||
@@ -41,9 +42,9 @@ export class DbInst {
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* 流程定义key,若存在则需要审批执行
|
||||
* 流程定义,若存在则需要审批执行
|
||||
*/
|
||||
flowProcdefKey: string;
|
||||
flowProcdef: any;
|
||||
|
||||
/**
|
||||
* dbName -> db
|
||||
@@ -359,7 +360,7 @@ export class DbInst {
|
||||
dbType: this.getDialect().getInfo().formatSqlDialect,
|
||||
runSuccessCallback: successFunc,
|
||||
cancelCallback: cancelFunc,
|
||||
flowProcdefKey: this.flowProcdefKey,
|
||||
flowProcdef: this.flowProcdef,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -383,6 +384,11 @@ export class DbInst {
|
||||
}
|
||||
let dbInst = dbInstCache.get(inst.id);
|
||||
if (dbInst) {
|
||||
// 更新可能更改的流程定义
|
||||
if (inst.flowProcdef !== undefined) {
|
||||
dbInst.flowProcdef = inst.flowProcdef;
|
||||
dbInstCache.set(dbInst.id, dbInst);
|
||||
}
|
||||
return dbInst;
|
||||
}
|
||||
console.info(`new dbInst: ${inst.id}, tagPath: ${inst.tagPath}`);
|
||||
@@ -393,7 +399,7 @@ export class DbInst {
|
||||
dbInst.name = inst.name;
|
||||
dbInst.type = inst.type;
|
||||
dbInst.databases = inst.databases;
|
||||
dbInst.flowProcdefKey = inst.flowProcdefKey;
|
||||
dbInst.flowProcdef = inst.flowProcdef;
|
||||
|
||||
dbInstCache.set(dbInst.id, dbInst);
|
||||
return dbInst;
|
||||
@@ -444,8 +450,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 +471,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 +483,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 +501,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 +601,11 @@ export class TabInfo {
|
||||
*/
|
||||
params: any;
|
||||
|
||||
/**
|
||||
* 组件ref
|
||||
*/
|
||||
componentRef: any;
|
||||
|
||||
getNowDbInst() {
|
||||
return DbInst.getInst(this.dbId);
|
||||
}
|
||||
|
||||
@@ -71,9 +71,9 @@ export enum DataType {
|
||||
/** 列数据类型角标 */
|
||||
export const ColumnTypeSubscript = {
|
||||
/** 字符串 */
|
||||
string: 'abc',
|
||||
string: 'ab',
|
||||
/** 数字 */
|
||||
number: '123',
|
||||
number: '12',
|
||||
/** 日期 */
|
||||
date: 'icon-clock',
|
||||
/** 时间 */
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
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'),
|
||||
|
||||
@@ -160,10 +160,10 @@
|
||||
<el-descriptions-item :span="1.5" label="SSH隧道">{{ infoDialog.data.sshTunnelMachineId > 0 ? '是' : '否' }} </el-descriptions-item>
|
||||
<el-descriptions-item :span="1.5" label="终端回放">{{ infoDialog.data.enableRecorder == 1 ? '是' : '否' }} </el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="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>
|
||||
@@ -236,12 +236,11 @@ import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { getMachineTerminalSocketUrl, machineApi } from './api';
|
||||
import { dateFormat } from '@/common/utils/date';
|
||||
import ResourceTags from '../component/ResourceTags.vue';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { hasPerms } from '@/components/auth/auth';
|
||||
import { formatByteSize } from '@/common/utils/format';
|
||||
import { formatByteSize, formatDate } from '@/common/utils/format';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import { getTagPathSearchItem } from '../component/tag';
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
</template>
|
||||
|
||||
<template #suffix="{ data }">
|
||||
<span style="color: #c4c9c4; font-size: 9px" v-if="data.type.value == MachineNodeType.AuthCert">{{
|
||||
<span v-if="data.type.value == MachineNodeType.AuthCert">{{
|
||||
` ${data.params.selectAuthCert.username}@${data.params.ip}:${data.params.port}`
|
||||
}}</span>
|
||||
</template>
|
||||
@@ -34,20 +34,15 @@
|
||||
|
||||
<Pane>
|
||||
<div class="machine-terminal-tabs card pd5">
|
||||
<el-tabs
|
||||
v-if="state.tabs.size > 0"
|
||||
type="card"
|
||||
@tab-remove="onRemoveTab"
|
||||
@tab-change="onTabChange"
|
||||
style="width: 100%"
|
||||
v-model="state.activeTermName"
|
||||
class="h100"
|
||||
>
|
||||
<el-tabs v-if="state.tabs.size > 0" type="card" @tab-remove="onRemoveTab" style="width: 100%" v-model="state.activeTermName" class="h100">
|
||||
<el-tab-pane class="h100" closable v-for="dt in state.tabs.values()" :label="dt.label" :name="dt.key" :key="dt.key">
|
||||
<template #label>
|
||||
<el-popconfirm @confirm="handleReconnect(dt, true)" title="确认重新连接?">
|
||||
<template #reference>
|
||||
<el-icon class="mr5" :color="dt.status == 1 ? '#67c23a' : '#f56c6c'" :title="dt.status == 1 ? '' : '点击重连'"
|
||||
<el-icon
|
||||
class="mr5"
|
||||
:color="EnumValue.getEnumByValue(TerminalStatusEnum, dt.status)?.extra?.iconColor"
|
||||
:title="dt.status == TerminalStatusEnum.Connected.value ? '' : '点击重连'"
|
||||
><Connection />
|
||||
</el-icon>
|
||||
</template>
|
||||
@@ -62,7 +57,7 @@
|
||||
<el-descriptions :column="1" size="small">
|
||||
<el-descriptions-item label="机器名"> {{ dt.params?.name }} </el-descriptions-item>
|
||||
<el-descriptions-item label="host"> {{ dt.params?.ip }} : {{ dt.params?.port }} </el-descriptions-item>
|
||||
<el-descriptions-item label="username"> {{ dt.params?.username }} </el-descriptions-item>
|
||||
<el-descriptions-item label="username"> {{ dt.params?.selectAuthCert.username }} </el-descriptions-item>
|
||||
<el-descriptions-item label="remark"> {{ dt.params?.remark }} </el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
@@ -103,10 +98,10 @@
|
||||
<el-descriptions-item :span="1.5" label="SSH隧道">{{ infoDialog.data.sshTunnelMachineId > 0 ? '是' : '否' }} </el-descriptions-item>
|
||||
<el-descriptions-item :span="1.5" label="终端回放">{{ infoDialog.data.enableRecorder == 1 ? '是' : '否' }} </el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="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>
|
||||
@@ -157,7 +152,7 @@
|
||||
import { defineAsyncComponent, nextTick, onMounted, reactive, ref, toRefs, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { getMachineTerminalSocketUrl, machineApi } from './api';
|
||||
import { dateFormat } from '@/common/utils/date';
|
||||
import { formatDate } from '@/common/utils/format';
|
||||
import { hasPerms } from '@/components/auth/auth';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import { NodeType, TagTreeNode, getTagTypeCodeByPath } from '../component/tag';
|
||||
@@ -165,13 +160,14 @@ import TagTree from '../component/TagTree.vue';
|
||||
import { Pane, Splitpanes } from 'splitpanes';
|
||||
import { ContextmenuItem } from '@/components/contextmenu/index';
|
||||
import TerminalBody from '@/components/terminal/TerminalBody.vue';
|
||||
import { TerminalStatus } from '@/components/terminal/common';
|
||||
import { TerminalStatus, TerminalStatusEnum } from '@/components/terminal/common';
|
||||
import MachineRdp from '@/components/terminal-rdp/MachineRdp.vue';
|
||||
import MachineFile from '@/views/ops/machine/file/MachineFile.vue';
|
||||
import ResourceTags from '../component/ResourceTags.vue';
|
||||
import { MachineProtocolEnum } from './enums';
|
||||
import { useAutoOpenResource } from '@/store/autoOpenResource';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import EnumValue from '@/common/Enum';
|
||||
|
||||
// 组件
|
||||
const ScriptManage = defineAsyncComponent(() => import('./ScriptManage.vue'));
|
||||
@@ -340,8 +336,13 @@ watch(
|
||||
watch(
|
||||
() => state.activeTermName,
|
||||
(newValue, oldValue) => {
|
||||
fitTerminal();
|
||||
|
||||
oldValue && terminalRefs[oldValue]?.blur && terminalRefs[oldValue]?.blur();
|
||||
terminalRefs[newValue]?.focus && terminalRefs[newValue]?.focus();
|
||||
|
||||
const nowTab = state.tabs.get(state.activeTermName);
|
||||
tagTreeRef.value.setCurrentKey(nowTab?.authCert);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -368,7 +369,7 @@ const autoOpenTerminal = (codePath: string) => {
|
||||
|
||||
const acNode = tagTreeRef.value.getNode(authCertName);
|
||||
openTerminal(acNode.data.params);
|
||||
}, 600);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const openTerminal = (machine: any, ex?: boolean) => {
|
||||
@@ -402,14 +403,14 @@ const openTerminal = (machine: any, ex?: boolean) => {
|
||||
}
|
||||
}
|
||||
|
||||
let { name, username } = machine;
|
||||
let { name } = machine;
|
||||
const labelName = `${machine.selectAuthCert.username}@${name}`;
|
||||
|
||||
// 同一个机器的终端打开多次,key后添加下划线和数字区分
|
||||
openIds[ac] = openIds[ac] ? ++openIds[ac] : 1;
|
||||
let sameIndex = openIds[ac];
|
||||
|
||||
let key = `${ac}_${username}_${sameIndex}`;
|
||||
let key = `${ac}_${sameIndex}`;
|
||||
// 只保留name的15个字,超出部分只保留前后10个字符,中间用省略号代替
|
||||
const label = labelName.length > 15 ? labelName.slice(0, 10) + '...' + labelName.slice(-10) : labelName;
|
||||
|
||||
@@ -496,20 +497,27 @@ const onRemoveTab = (targetName: string) => {
|
||||
if (tabName !== targetName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
state.tabs.delete(targetName);
|
||||
let info = state.tabs.get(targetName);
|
||||
if (info) {
|
||||
terminalRefs[info.key]?.close();
|
||||
}
|
||||
|
||||
if (activeTermName != targetName) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 如果删除的tab是当前激活的tab,则切换到前一个或后一个tab
|
||||
const nextTab = tabNames[i + 1] || tabNames[i - 1];
|
||||
if (nextTab) {
|
||||
activeTermName = nextTab;
|
||||
} else {
|
||||
activeTermName = '';
|
||||
}
|
||||
let info = state.tabs.get(targetName);
|
||||
if (info) {
|
||||
terminalRefs[info.key]?.close();
|
||||
}
|
||||
|
||||
state.tabs.delete(targetName);
|
||||
state.activeTermName = activeTermName;
|
||||
onTabChange();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -535,18 +543,13 @@ const onResizeTagTree = () => {
|
||||
fitTerminal();
|
||||
};
|
||||
|
||||
const onTabChange = () => {
|
||||
fitTerminal();
|
||||
};
|
||||
|
||||
const fitTerminal = () => {
|
||||
setTimeout(() => {
|
||||
let info = state.tabs.get(state.activeTermName);
|
||||
if (info) {
|
||||
terminalRefs[info.key]?.fitTerminal && terminalRefs[info.key]?.fitTerminal();
|
||||
terminalRefs[info.key]?.focus && terminalRefs[info.key]?.focus();
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
};
|
||||
|
||||
const handleReconnect = (tab: any, force = false) => {
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<el-table-column prop="cmd" label="命令" show-overflow-tooltip min-width="150px"> </el-table-column>
|
||||
<el-table-column prop="time" label="执行时间" min-width="80" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
{{ dateFormat(new Date(scope.row.time * 1000).toString()) }}
|
||||
{{ formatDate(new Date(scope.row.time * 1000).toString()) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -48,7 +48,7 @@ import * as AsciinemaPlayer from 'asciinema-player';
|
||||
import 'asciinema-player/dist/bundle/asciinema-player.css';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { dateFormat } from '@/common/utils/date';
|
||||
import { formatDate } from '@/common/utils/format';
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean },
|
||||
@@ -122,8 +122,8 @@ const playRec = async (rec: any) => {
|
||||
idleTimeLimit: 2,
|
||||
// fit: false,
|
||||
// terminalFontSize: 'small',
|
||||
cols: 144,
|
||||
rows: 32,
|
||||
// cols: 144,
|
||||
// rows: 32,
|
||||
});
|
||||
});
|
||||
} finally {
|
||||
|
||||
@@ -12,6 +12,8 @@ export const machineApi = {
|
||||
process: Api.newGet('/machines/{id}/process'),
|
||||
// 终止进程
|
||||
killProcess: Api.newDelete('/machines/{id}/process'),
|
||||
users: Api.newGet('/machines/{id}/users'),
|
||||
groups: Api.newGet('/machines/{id}/groups'),
|
||||
testConn: Api.newPost('/machines/test-conn'),
|
||||
// 保存按钮
|
||||
saveMachine: Api.newPost('/machines'),
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-popover placement="right" width="auto" title="机器详情" trigger="click">
|
||||
<template #reference>
|
||||
<el-link @click="getMachineDetail" type="primary">{{ props.code }}</el-link>
|
||||
</template>
|
||||
|
||||
<el-descriptions v-loading="state.loading" :column="3" border>
|
||||
<el-descriptions-item :span="1" label="机器id">{{ state.machineDetail.id }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="编号">{{ state.machineDetail.code }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="名称">{{ state.machineDetail.name }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="3" label="关联标签"><ResourceTags :tags="state.machineDetail.tags" /></el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="2" label="IP">{{ state.machineDetail.ip }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="端口">{{ state.machineDetail.port }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="3" label="备注">{{ state.machineDetail.remark }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="1.5" label="SSH隧道">{{ state.machineDetail.sshTunnelMachineId > 0 ? '是' : '否' }} </el-descriptions-item>
|
||||
<el-descriptions-item :span="1.5" label="终端回放">{{ state.machineDetail.enableRecorder == 1 ? '是' : '否' }} </el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="2" label="创建时间">{{ formatDate(state.machineDetail.createTime) }} </el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="创建者">{{ state.machineDetail.creator }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="2" label="更新时间">{{ formatDate(state.machineDetail.updateTime) }} </el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="修改者">{{ state.machineDetail.modifier }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive } from 'vue';
|
||||
import { machineApi } from '../api';
|
||||
import { formatDate } from '@/common/utils/format';
|
||||
import ResourceTags from '../../component/ResourceTags.vue';
|
||||
|
||||
const props = defineProps({
|
||||
code: {
|
||||
type: [String],
|
||||
requierd: true,
|
||||
},
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
loading: false,
|
||||
machineDetail: {} as any,
|
||||
});
|
||||
|
||||
const getMachineDetail = async () => {
|
||||
try {
|
||||
state.machineDetail = {};
|
||||
state.loading = true;
|
||||
const res = await machineApi.list.request({
|
||||
code: props.code,
|
||||
});
|
||||
if (res.total == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.machineDetail = res.list?.[0];
|
||||
} finally {
|
||||
state.loading = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
@@ -1,14 +1,18 @@
|
||||
<template>
|
||||
<div class="mock-data-dialog">
|
||||
<el-dialog
|
||||
<el-drawer
|
||||
:title="title"
|
||||
v-model="dialogVisible"
|
||||
:close-on-click-modal="false"
|
||||
:before-close="cancel"
|
||||
:show-close="true"
|
||||
:destroy-on-close="true"
|
||||
width="900px"
|
||||
size="40%"
|
||||
>
|
||||
<template #header>
|
||||
<DrawerHeader :header="title" :back="cancel" />
|
||||
</template>
|
||||
|
||||
<el-form :model="form" ref="formRef" :rules="rules" label-width="auto">
|
||||
<el-form-item prop="name" label="名称">
|
||||
<el-input v-model="form.name" placeholder="请输入名称"></el-input>
|
||||
@@ -34,19 +38,13 @@
|
||||
<el-input v-model="form.remark" placeholder="请输入备注"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="machineIds" label="关联机器">
|
||||
<el-select multiple v-model="form.machineIds" filterable placeholder="请选关联机器" style="width: 100%">
|
||||
<el-option v-for="ac in state.machines" :key="ac.id" :value="ac.id" :label="ac.ip">
|
||||
{{ ac.ip }}
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
{{ ac.tagPath }}{{ ac.name }}
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="script" label="执行脚本" required>
|
||||
<monaco-editor style="width: 100%" v-model="form.script" language="shell" height="300px"
|
||||
<monaco-editor style="width: 100%" v-model="form.script" language="shell" height="200px"
|
||||
/></el-form-item>
|
||||
|
||||
<el-form-item ref="tagSelectRef" prop="codePaths" label="关联机器">
|
||||
<tag-tree-check height="200px" :tag-type="TagResourceTypeEnum.Machine.value" v-model="form.codePaths" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
@@ -55,7 +53,7 @@
|
||||
<el-button v-auth="'machine:script:save'" type="primary" :loading="btnLoading" @click="btnOk" :disabled="submitDisabled">保 存</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -67,6 +65,9 @@ import { CronJobStatusEnum, CronJobSaveExecResTypeEnum } from '../enums';
|
||||
import { notEmpty } from '@/common/assert';
|
||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||
import CrontabInput from '@/components/crontab/CrontabInput.vue';
|
||||
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||
import TagTreeCheck from '../../component/TagTreeCheck.vue';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
@@ -130,11 +131,11 @@ const state = reactive({
|
||||
id: null,
|
||||
name: '',
|
||||
cron: '',
|
||||
machineIds: [],
|
||||
remark: '',
|
||||
script: '',
|
||||
status: 1,
|
||||
saveExecResType: -1,
|
||||
codePaths: [],
|
||||
},
|
||||
machines: [] as any,
|
||||
btnLoading: false,
|
||||
@@ -154,7 +155,7 @@ watch(props, async (newValue: any) => {
|
||||
}
|
||||
if (newValue.data) {
|
||||
state.form = { ...newValue.data };
|
||||
state.form.machineIds = await cronJobApi.relateMachineIds.request({ cronJobId: state.form.id });
|
||||
state.form.codePaths = newValue.data.tags?.map((tag: any) => tag.codePath);
|
||||
} else {
|
||||
state.form = { script: '', status: 1 } as any;
|
||||
state.chooseMachines = [];
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<el-dialog
|
||||
:title="title"
|
||||
v-model="dialogVisible"
|
||||
@open="search()"
|
||||
:close-on-click-modal="false"
|
||||
:before-close="cancel"
|
||||
:show-close="true"
|
||||
@@ -13,20 +14,13 @@
|
||||
ref="pageTableRef"
|
||||
:page-api="cronJobApi.execList"
|
||||
:lazy="true"
|
||||
:data-handler-fn="parseData"
|
||||
:search-items="searchItems"
|
||||
v-model:query-form="params"
|
||||
:data="state.data.list"
|
||||
:columns="columns"
|
||||
>
|
||||
<template #machineSelect>
|
||||
<el-select v-model="params.machineId" filterable placeholder="选择机器查询" clearable>
|
||||
<el-option v-for="ac in machineMap.values()" :key="ac.id" :value="ac.id" :label="ac.ip">
|
||||
{{ ac.ip }}
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
{{ ac.tagPath }}{{ ac.name }}
|
||||
</el-option>
|
||||
</el-select>
|
||||
<template #machineCode="{ data }">
|
||||
<MachineDetail :code="data.machineCode" />
|
||||
</template>
|
||||
</page-table>
|
||||
</el-dialog>
|
||||
@@ -34,12 +28,13 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch, ref, toRefs, reactive, Ref } from 'vue';
|
||||
import { cronJobApi, machineApi } from '../api';
|
||||
import { ref, toRefs, reactive, Ref } from 'vue';
|
||||
import { cronJobApi } from '../api';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { CronJobExecStatusEnum } from '../enums';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import MachineDetail from '../component/MachineDetail.vue';
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
@@ -53,13 +48,10 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:visible', 'update:data', 'cancel']);
|
||||
|
||||
const searchItems = [SearchItem.slot('machineId', '机器', 'machineSelect'), SearchItem.select('status', '状态').withEnum(CronJobExecStatusEnum)];
|
||||
const searchItems = [SearchItem.input('machineCode', '机器编号'), SearchItem.select('status', '状态').withEnum(CronJobExecStatusEnum)];
|
||||
|
||||
const columns = ref([
|
||||
TableColumn.new('machineIp', '机器IP').setMinWidth(120),
|
||||
TableColumn.new('machineName', '机器名称').setMinWidth(100),
|
||||
TableColumn.new('machineCode', '机器编号').isSlot(),
|
||||
TableColumn.new('status', '状态').typeTag(CronJobExecStatusEnum).setMinWidth(70),
|
||||
TableColumn.new('res', '执行结果').setMinWidth(250).canBeautify(),
|
||||
TableColumn.new('execTime', '执行时间').isTime().setMinWidth(150),
|
||||
@@ -72,10 +64,10 @@ const state = reactive({
|
||||
tags: [] as any,
|
||||
params: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 8,
|
||||
cronJobId: 0,
|
||||
status: null,
|
||||
machineId: null,
|
||||
machineCode: '',
|
||||
},
|
||||
// 列表数据
|
||||
data: {
|
||||
@@ -85,64 +77,17 @@ const state = reactive({
|
||||
machines: [],
|
||||
});
|
||||
|
||||
const machineMap: Map<number, any> = new Map();
|
||||
const { params } = toRefs(state);
|
||||
|
||||
const { dialogVisible, params } = toRefs(state);
|
||||
|
||||
watch(props, async (newValue: any) => {
|
||||
state.dialogVisible = newValue.visible;
|
||||
if (!newValue.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
const machineIds = await cronJobApi.relateMachineIds.request({
|
||||
cronJobId: props.data?.id,
|
||||
});
|
||||
const res = await machineApi.list.request({
|
||||
ids: machineIds?.join(','),
|
||||
});
|
||||
|
||||
res.list?.forEach((x: any) => {
|
||||
machineMap.set(x.id, x);
|
||||
});
|
||||
|
||||
state.params.cronJobId = props.data?.id;
|
||||
search();
|
||||
});
|
||||
const dialogVisible = defineModel<boolean>('visible');
|
||||
|
||||
const search = async () => {
|
||||
state.params.cronJobId = props.data?.id;
|
||||
pageTableRef.value.search();
|
||||
};
|
||||
|
||||
const parseData = async (res: any) => {
|
||||
const dataList = res.list;
|
||||
// 填充机器信息
|
||||
for (let x of dataList) {
|
||||
const machineId = x.machineId;
|
||||
let machine = machineMap.get(machineId);
|
||||
// 如果未找到,则可能被移除,则调接口查询机器信息
|
||||
if (!machine) {
|
||||
const machineRes = await machineApi.list.request({ ids: machineId });
|
||||
if (!machineRes.list) {
|
||||
machine = {
|
||||
id: machineId,
|
||||
ip: machineId,
|
||||
name: '该机器已被删除',
|
||||
};
|
||||
} else {
|
||||
machine = machineRes.list[0];
|
||||
}
|
||||
machineMap.set(machineId, machine);
|
||||
}
|
||||
|
||||
x.machineIp = machine?.ip;
|
||||
x.machineName = machine?.name;
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
emit('update:visible', false);
|
||||
dialogVisible.value = false;
|
||||
setTimeout(() => {
|
||||
initData();
|
||||
}, 500);
|
||||
@@ -152,7 +97,7 @@ const initData = () => {
|
||||
state.data.list = [];
|
||||
state.data.total = 0;
|
||||
state.params.pageNum = 1;
|
||||
state.params.machineId = null;
|
||||
state.params.machineCode = '';
|
||||
state.params.status = null;
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -19,6 +19,10 @@
|
||||
<el-tag v-else type="danger" effect="plain">未运行</el-tag>
|
||||
</template>
|
||||
|
||||
<template #codePaths="{ data }">
|
||||
<TagCodePath :path="data.tags?.map((tag: any) => tag.codePath)" />
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
<el-button :disabled="data.status == CronJobStatusEnum.Disable.value" v-auth="perms.saveCronJob" type="primary" @click="runCronJob(data)" link
|
||||
>执行</el-button
|
||||
@@ -41,6 +45,7 @@ import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { CronJobStatusEnum, CronJobSaveExecResTypeEnum } from '../enums';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import TagCodePath from '../../component/TagCodePath.vue';
|
||||
|
||||
const CronJobEdit = defineAsyncComponent(() => import('./CronJobEdit.vue'));
|
||||
const CronJobExecList = defineAsyncComponent(() => import('./CronJobExecList.vue'));
|
||||
@@ -61,6 +66,7 @@ const columns = ref([
|
||||
TableColumn.new('running', '运行状态').isSlot(),
|
||||
TableColumn.new('saveExecResType', '记录类型').typeTag(CronJobSaveExecResTypeEnum),
|
||||
TableColumn.new('remark', '备注'),
|
||||
TableColumn.new('codePaths', '关联机器').isSlot().setMinWidth('250px'),
|
||||
TableColumn.new('action', '操作').isSlot().setMinWidth(180).fixedRight().alignCenter(),
|
||||
]);
|
||||
|
||||
|
||||
@@ -18,12 +18,12 @@
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="path" label="路径" min-width="150px" show-overflow-tooltip>
|
||||
<el-table-column prop="path" label="路径" min-width="180" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<el-input v-model="scope.row.path" :disabled="scope.row.id != null" clearable> </el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-wdith="120px">
|
||||
<el-table-column label="操作" min-width="130">
|
||||
<template #default="scope">
|
||||
<el-button v-if="scope.row.id == null" @click="addFiles(scope.row)" type="success" icon="success-filled" plain></el-button>
|
||||
<el-button v-if="scope.row.id != null" @click="getConf(scope.row)" type="primary" icon="tickets" plain></el-button>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
>
|
||||
<el-table-column type="selection" width="30" />
|
||||
|
||||
<el-table-column prop="name" label="名称">
|
||||
<el-table-column prop="name" label="名称" min-width="380">
|
||||
<template #header>
|
||||
<div class="machine-file-table-header">
|
||||
<div>
|
||||
@@ -171,7 +171,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="size" label="大小" width="100" sortable>
|
||||
<el-table-column prop="size" label="大小" min-width="90" sortable>
|
||||
<template #default="scope">
|
||||
<span style="color: #67c23a; font-weight: bold" v-if="scope.row.type == '-'"> {{ formatByteSize(scope.row.size) }} </span>
|
||||
<span style="color: #67c23a; font-weight: bold" v-if="scope.row.type == 'd' && scope.row.dirSize"> {{ scope.row.dirSize }} </span>
|
||||
@@ -182,7 +182,11 @@
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="mode" label="属性" width="110"> </el-table-column>
|
||||
<el-table-column prop="modTime" label="修改时间" width="165" sortable> </el-table-column>
|
||||
<el-table-column v-if="$props.protocol == MachineProtocolEnum.Ssh.value" prop="username" label="用户" min-width="55" show-overflow-tooltip>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="$props.protocol == MachineProtocolEnum.Ssh.value" prop="groupname" label="组" min-width="55" show-overflow-tooltip>
|
||||
</el-table-column>
|
||||
<el-table-column prop="modTime" label="修改时间" width="160" sortable> </el-table-column>
|
||||
|
||||
<el-table-column width="100">
|
||||
<template #header>
|
||||
@@ -288,6 +292,8 @@ import MachineFileContent from './MachineFileContent.vue';
|
||||
import { getToken } from '@/common/utils/storage';
|
||||
import { convertToBytes, formatByteSize } from '@/common/utils/format';
|
||||
import { getMachineConfig } from '@/common/sysconfig';
|
||||
import { MachineProtocolEnum } from '../enums';
|
||||
import { fuzzyMatchField } from '@/common/utils/string';
|
||||
|
||||
const props = defineProps({
|
||||
machineId: { type: Number },
|
||||
@@ -303,6 +309,9 @@ const folderUploadRef: any = ref();
|
||||
|
||||
const folderType = 'd';
|
||||
|
||||
const userMap = new Map<number, any>();
|
||||
const groupMap = new Map<number, any>();
|
||||
|
||||
// 路径分隔符
|
||||
const pathSep = '/';
|
||||
|
||||
@@ -343,13 +352,27 @@ const { basePath, nowPath, loading, fileNameFilter, progressNum, uploadProgressS
|
||||
|
||||
onMounted(async () => {
|
||||
state.basePath = props.path;
|
||||
const machineId = props.machineId;
|
||||
|
||||
if (props.protocol == MachineProtocolEnum.Ssh.value) {
|
||||
machineApi.users.request({ id: machineId }).then((res: any) => {
|
||||
for (let user of res) {
|
||||
userMap.set(user.uid, user);
|
||||
}
|
||||
});
|
||||
|
||||
machineApi.groups.request({ id: machineId }).then((res: any) => {
|
||||
for (let group of res) {
|
||||
groupMap.set(group.gid, group);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setFiles(props.path);
|
||||
state.machineConfig = await getMachineConfig();
|
||||
});
|
||||
|
||||
const filterFiles = computed(() =>
|
||||
state.files.filter((data: any) => !state.fileNameFilter || data.name.toLowerCase().includes(state.fileNameFilter.toLowerCase()))
|
||||
);
|
||||
const filterFiles = computed(() => fuzzyMatchField(state.fileNameFilter, state.files, (file: any) => file.name));
|
||||
|
||||
const filePathNav = computed(() => {
|
||||
let basePath = state.basePath;
|
||||
@@ -517,6 +540,11 @@ const lsFile = async (path: string) => {
|
||||
path,
|
||||
});
|
||||
for (const file of res) {
|
||||
if (props.protocol == MachineProtocolEnum.Ssh.value) {
|
||||
file.username = userMap.get(file.uid)?.uname || file.uid;
|
||||
file.groupname = groupMap.get(file.gid)?.gname || file.gid;
|
||||
}
|
||||
|
||||
const type = file.type;
|
||||
if (type == folderType) {
|
||||
file.isFolder = true;
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="codePaths" label="关联机器" min-width="220px" show-overflow-tooltip>
|
||||
<el-table-column prop="codePaths" label="关联机器" min-width="250px" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<TagCodePath :path="scope.row.tags.map((tag: any) => tag.codePath)" />
|
||||
<TagCodePath :path="scope.row.tags?.map((tag: any) => tag.codePath)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" show-overflow-tooltip width="120px"> </el-table-column>
|
||||
@@ -173,7 +173,7 @@ const openFormDialog = (data: any) => {
|
||||
state.form = { ...DefaultForm };
|
||||
} else {
|
||||
state.form = _.cloneDeep(data);
|
||||
state.form.codePaths = data.tags.map((tag: any) => tag.codePath);
|
||||
state.form.codePaths = data.tags?.map((tag: any) => tag.codePath);
|
||||
}
|
||||
state.dialogVisible = true;
|
||||
};
|
||||
|
||||
@@ -36,13 +36,8 @@
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #label="{ data }">
|
||||
<span v-if="data.type.value == MongoNodeType.Dbs">
|
||||
{{ data.params.database }}
|
||||
<span style="color: #8492a6; font-size: 13px"> [{{ formatByteSize(data.params.size) }}] </span>
|
||||
</span>
|
||||
|
||||
<span v-else>{{ data.label }}</span>
|
||||
<template #suffix="{ data }">
|
||||
<span v-if="data.type.value == MongoNodeType.Dbs">{{ formatByteSize(data.params.size) }}</span>
|
||||
</template>
|
||||
</tag-tree>
|
||||
</Pane>
|
||||
|
||||
@@ -35,6 +35,10 @@
|
||||
|
||||
<SvgIcon v-if="data.type.value == RedisNodeType.Db" name="Coin" color="#67c23a" />
|
||||
</template>
|
||||
|
||||
<template #suffix="{ data }">
|
||||
<span v-if="data.type.value == RedisNodeType.Db">{{ data.params.keys }}</span>
|
||||
</template>
|
||||
</tag-tree>
|
||||
</Pane>
|
||||
|
||||
@@ -197,6 +201,7 @@ import { Splitpanes, Pane } from 'splitpanes';
|
||||
import { RedisInst } from './redis';
|
||||
import { useAutoOpenResource } from '@/store/autoOpenResource';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { procdefApi } from '@/views/flow/api';
|
||||
|
||||
const KeyDetail = defineAsyncComponent(() => import('./KeyDetail.vue'));
|
||||
|
||||
@@ -244,11 +249,13 @@ const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(asyn
|
||||
// redis实例节点类型
|
||||
const NodeTypeRedis = new NodeType(RedisNodeType.Redis).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const redisInfo = parentNode.params;
|
||||
const flowProcdef = await procdefApi.getByResource.request({ resourceType: TagResourceTypeEnum.Redis.value, resourceCode: redisInfo.code });
|
||||
|
||||
let dbs: TagTreeNode[] = redisInfo.db.split(',').map((x: string) => {
|
||||
return new TagTreeNode(x, `db${x}`, NodeTypeDb).withIsLeaf(true).withParams({
|
||||
id: redisInfo.id,
|
||||
db: x,
|
||||
flowProcdefKey: redisInfo.flowProcdefKey,
|
||||
flowProcdef: flowProcdef,
|
||||
name: `db${x}`,
|
||||
keys: 0,
|
||||
});
|
||||
@@ -268,7 +275,7 @@ const NodeTypeRedis = new NodeType(RedisNodeType.Redis).withLoadNodesFunc(async
|
||||
}
|
||||
// 替换label
|
||||
dbs.forEach((e: any) => {
|
||||
e.label = `${e.params.name} [${e.params.keys}]`;
|
||||
e.label = `${e.params.name}`;
|
||||
});
|
||||
return dbs;
|
||||
});
|
||||
@@ -281,7 +288,7 @@ const NodeTypeDb = new NodeType(RedisNodeType.Db).withNodeClickFunc((nodeData: T
|
||||
|
||||
redisInst.value.id = nodeData.params.id;
|
||||
redisInst.value.db = Number.parseInt(nodeData.params.db);
|
||||
redisInst.value.flowProcdefKey = nodeData.params.flowProcdefKey;
|
||||
redisInst.value.flowProcdef = nodeData.params.flowProcdef;
|
||||
|
||||
scan();
|
||||
});
|
||||
|
||||
@@ -58,13 +58,6 @@
|
||||
placeholder="请输入密码, 修改操作可不填"
|
||||
autocomplete="new-password"
|
||||
>
|
||||
<!-- <template v-if="form.id && form.id != 0" #suffix>
|
||||
<el-popover @hide="pwd = ''" placement="right" title="原密码" :width="200" trigger="click" :content="pwd">
|
||||
<template #reference>
|
||||
<el-link @click="getPwd" :underline="false" type="primary" class="mr5">原密码</el-link>
|
||||
</template>
|
||||
</el-popover>
|
||||
</template> -->
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="db" label="库号" required>
|
||||
@@ -84,8 +77,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-tab-pane>
|
||||
|
||||
<el-tab-pane label="其他配置" name="other">
|
||||
@@ -108,12 +99,11 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, reactive, ref, watchEffect } from 'vue';
|
||||
import { toRefs, reactive, ref, watch } from 'vue';
|
||||
import { redisApi } from './api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import TagTreeSelect from '../component/TagTreeSelect.vue';
|
||||
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
|
||||
import ProcdefSelectFormItem from '@/views/flow/components/ProcdefSelectFormItem.vue';
|
||||
import { ResourceCodePattern } from '@/common/pattern';
|
||||
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||
|
||||
@@ -199,7 +189,6 @@ const state = reactive({
|
||||
db: '',
|
||||
remark: '',
|
||||
sshTunnelMachineId: -1,
|
||||
flowProcdefKey: '',
|
||||
},
|
||||
submitForm: {} as any,
|
||||
dbList: [0],
|
||||
@@ -211,23 +200,25 @@ const { dialogVisible, tabActiveName, form, submitForm, dbList } = toRefs(state)
|
||||
const { isFetching: testConnBtnLoading, execute: testConnExec } = redisApi.testConn.useApi(submitForm);
|
||||
const { isFetching: saveBtnLoading, execute: saveRedisExec } = redisApi.saveRedis.useApi(submitForm);
|
||||
|
||||
watchEffect(() => {
|
||||
state.dialogVisible = props.visible;
|
||||
if (!state.dialogVisible) {
|
||||
return;
|
||||
watch(
|
||||
() => props.visible,
|
||||
() => {
|
||||
state.dialogVisible = props.visible;
|
||||
if (!state.dialogVisible) {
|
||||
return;
|
||||
}
|
||||
state.tabActiveName = 'basic';
|
||||
const redis: any = props.redis;
|
||||
if (redis) {
|
||||
state.form = { ...redis };
|
||||
state.form.tagCodePaths = redis.tags.map((t: any) => t.codePath);
|
||||
convertDb(state.form.db);
|
||||
} else {
|
||||
state.form = { db: '0', tagCodePaths: [] } as any;
|
||||
state.dbList = [0];
|
||||
}
|
||||
}
|
||||
state.tabActiveName = 'basic';
|
||||
const redis: any = props.redis;
|
||||
if (redis) {
|
||||
state.form = { ...redis };
|
||||
state.form.tagCodePaths = redis.tags.map((t: any) => t.codePath);
|
||||
convertDb(state.form.db);
|
||||
} else {
|
||||
state.form = { db: '0', tagCodePaths: [] } as any;
|
||||
|
||||
state.dbList = [0];
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
const convertDb = (db: string) => {
|
||||
state.dbList = db.split(',').map((x) => Number.parseInt(x));
|
||||
|
||||
@@ -128,13 +128,12 @@
|
||||
|
||||
<el-descriptions-item :span="3" label="库">{{ detailDialog.data.db }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="3" label="备注">{{ detailDialog.data.remark }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="3" label="工单流程key">{{ detailDialog.data?.flowProcdefKey }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="3" label="SSH隧道">{{ detailDialog.data.sshTunnelMachineId > 0 ? '是' : '否' }} </el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="2" label="创建时间">{{ dateFormat(detailDialog.data.createTime) }} </el-descriptions-item>
|
||||
<el-descriptions-item :span="2" label="创建时间">{{ formatDate(detailDialog.data.createTime) }} </el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="创建者">{{ detailDialog.data.creator }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="2" label="更新时间">{{ dateFormat(detailDialog.data.updateTime) }} </el-descriptions-item>
|
||||
<el-descriptions-item :span="2" label="更新时间">{{ formatDate(detailDialog.data.updateTime) }} </el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="修改者">{{ detailDialog.data.modifier }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
@@ -154,7 +153,7 @@ import { redisApi } from './api';
|
||||
import { onMounted, reactive, ref, Ref, toRefs } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import RedisEdit from './RedisEdit.vue';
|
||||
import { dateFormat } from '@/common/utils/date';
|
||||
import { formatDate } from '@/common/utils/format';
|
||||
import ResourceTags from '../component/ResourceTags.vue';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
@@ -180,7 +179,6 @@ const columns = ref([
|
||||
TableColumn.new('name', '名称'),
|
||||
TableColumn.new('host', 'host:port'),
|
||||
TableColumn.new('mode', 'mode'),
|
||||
TableColumn.new('flowProcdefKey', '关联流程'),
|
||||
TableColumn.new('remark', '备注'),
|
||||
TableColumn.new('code', '编号'),
|
||||
TableColumn.new('action', '操作').isSlot().setMinWidth(200).fixedRight().alignCenter(),
|
||||
|
||||
@@ -5,7 +5,7 @@ export type CmdExecProps = {
|
||||
id: number;
|
||||
db: number | string;
|
||||
cmd: any[];
|
||||
flowProcdefKey?: string;
|
||||
flowProcdef?: any;
|
||||
visible?: boolean;
|
||||
runSuccessFn?: Function;
|
||||
cancelFn?: Function;
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
<el-input type="textarea" disabled v-model="state.cmdStr" class="mt5" rows="5" />
|
||||
<el-input @keyup.enter="runCmd" ref="remarkInputRef" v-model="remark" placeholder="请输入执行备注" class="mt5" />
|
||||
|
||||
<div v-if="props.flowProcdefKey">
|
||||
<div v-if="props.flowProcdef">
|
||||
<el-divider content-position="left">审批节点</el-divider>
|
||||
<procdef-tasks :procdef-key="props.flowProcdefKey" />
|
||||
<procdef-tasks :procdef="props.flowProcdef" />
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
@@ -32,7 +32,7 @@ const props = withDefaults(defineProps<CmdExecProps>(), {});
|
||||
const remarkInputRef = ref<InputInstance>();
|
||||
const state = reactive({
|
||||
dialogVisible: false,
|
||||
flowProcdefKey: '' as any,
|
||||
flowProcdef: null as any,
|
||||
cmdStr: '',
|
||||
remark: '',
|
||||
btnLoading: false,
|
||||
|
||||
@@ -13,9 +13,9 @@ export class RedisInst {
|
||||
db: number;
|
||||
|
||||
/**
|
||||
* 流程定义key,若存在则需要审批执行
|
||||
* 流程定义,若存在则需要审批执行
|
||||
*/
|
||||
flowProcdefKey: string;
|
||||
flowProcdef: any;
|
||||
|
||||
/**
|
||||
* 执行命令
|
||||
@@ -24,11 +24,11 @@ export class RedisInst {
|
||||
*/
|
||||
async runCmd(cmd: any[]) {
|
||||
// 工单流程定义存在,并且为写入命令时,弹窗输入工单相关信息并提交
|
||||
if (this.flowProcdefKey && writeCmd[cmd[0].toUpperCase()]) {
|
||||
if (this.flowProcdef && writeCmd[cmd[0].toUpperCase()]) {
|
||||
showCmdExecBox({
|
||||
id: this.id,
|
||||
db: this.db,
|
||||
flowProcdefKey: this.flowProcdefKey,
|
||||
flowProcdef: this.flowProcdef,
|
||||
cmd,
|
||||
});
|
||||
// 报错,阻止后续继续执行
|
||||
|
||||
@@ -11,6 +11,14 @@
|
||||
<el-button v-auth="'authcert:save'" type="primary" icon="plus" @click="edit(false)">添加</el-button>
|
||||
</template>
|
||||
|
||||
<template #resourceCode="{ data }">
|
||||
<SvgIcon
|
||||
:name="EnumValue.getEnumByValue(TagResourceTypeEnum, data.resourceType)?.extra.icon"
|
||||
:color="EnumValue.getEnumByValue(TagResourceTypeEnum, data.resourceType)?.extra.iconColor"
|
||||
/>
|
||||
{{ data.resourceCode }}
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
<el-button v-auth="'authcert:save'" @click="edit(data)" type="primary" link>编辑</el-button>
|
||||
|
||||
@@ -41,6 +49,7 @@ import { SearchItem } from '@/components/SearchForm';
|
||||
import { AuthCertCiphertextTypeEnum, AuthCertTypeEnum } from './enums';
|
||||
import { ResourceTypeEnum, TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import ResourceAuthCertEdit from '../component/ResourceAuthCertEdit.vue';
|
||||
import EnumValue from '@/common/Enum';
|
||||
|
||||
const pageTableRef: Ref<any> = ref(null);
|
||||
const state = reactive({
|
||||
@@ -50,6 +59,7 @@ const state = reactive({
|
||||
name: null,
|
||||
},
|
||||
searchItems: [
|
||||
SearchItem.input('resourceCode', '资源编号'),
|
||||
SearchItem.input('name', '凭证名称'),
|
||||
SearchItem.select('resourceType', '资源类型').withEnum(ResourceTypeEnum),
|
||||
SearchItem.select('type', '凭证类型').withEnum(AuthCertTypeEnum),
|
||||
@@ -60,8 +70,7 @@ const state = reactive({
|
||||
TableColumn.new('type', '凭证类型').typeTag(AuthCertTypeEnum),
|
||||
TableColumn.new('username', '用户名'),
|
||||
TableColumn.new('ciphertextType', '密文类型').typeTag(AuthCertCiphertextTypeEnum),
|
||||
TableColumn.new('resourceType', '资源类型').typeTag(TagResourceTypeEnum),
|
||||
TableColumn.new('resourceCode', '资源编号'),
|
||||
TableColumn.new('resourceCode', '资源编号').isSlot().setAddWidth(30),
|
||||
TableColumn.new('remark', '备注'),
|
||||
TableColumn.new('creator', '创建人'),
|
||||
TableColumn.new('createTime', '创建时间').isTime(),
|
||||
|
||||
@@ -82,9 +82,9 @@
|
||||
<el-descriptions-item label="备注">{{ currentTag.remark }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="创建者">{{ currentTag.creator }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ dateFormat(currentTag.createTime) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ formatDate(currentTag.createTime) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="修改者">{{ currentTag.modifier }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">{{ dateFormat(currentTag.updateTime) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">{{ formatDate(currentTag.updateTime) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-tab-pane>
|
||||
|
||||
@@ -145,21 +145,23 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, ref, watch, reactive, onMounted, Ref } from 'vue';
|
||||
import { toRefs, ref, watch, reactive, onMounted, Ref, defineAsyncComponent } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { tagApi } from './api';
|
||||
import { dateFormat } from '@/common/utils/date';
|
||||
import { formatDate } from '@/common/utils/format';
|
||||
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu/index';
|
||||
import { useUserInfo } from '@/store/userInfo';
|
||||
import { Splitpanes, Pane } from 'splitpanes';
|
||||
import MachineList from '../machine/MachineList.vue';
|
||||
import RedisList from '../redis/RedisList.vue';
|
||||
import MongoList from '../mongo/MongoList.vue';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import EnumTag from '@/components/enumtag/EnumTag.vue';
|
||||
import EnumValue from '@/common/Enum';
|
||||
import InstanceList from '../db/InstanceList.vue';
|
||||
import TagCodePath from '../component/TagCodePath.vue';
|
||||
import { isPrefixSubsequence } from '@/common/utils/string';
|
||||
|
||||
const MachineList = defineAsyncComponent(() => import('../machine/MachineList.vue'));
|
||||
const InstanceList = defineAsyncComponent(() => import('../db/InstanceList.vue'));
|
||||
const RedisList = defineAsyncComponent(() => import('../redis/RedisList.vue'));
|
||||
const MongoList = defineAsyncComponent(() => import('../mongo/MongoList.vue'));
|
||||
|
||||
interface Tree {
|
||||
id: number;
|
||||
@@ -195,6 +197,9 @@ const contextmenuAdd = new ContextmenuItem('addTag', '添加子标签')
|
||||
const contextmenuEdit = new ContextmenuItem('edit', '编辑')
|
||||
.withIcon('edit')
|
||||
.withPermission('tag:save')
|
||||
.withHideFunc((data: any) => {
|
||||
return data.type != TagResourceTypeEnum.Tag.value;
|
||||
})
|
||||
.withOnClick((data: any) => showEditTagDialog(data));
|
||||
|
||||
const contextmenuDel = new ContextmenuItem('delete', '删除')
|
||||
@@ -367,8 +372,7 @@ const setNowTabData = () => {
|
||||
};
|
||||
|
||||
const filterNode = (value: string, data: Tree) => {
|
||||
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 search = async () => {
|
||||
@@ -376,6 +380,11 @@ const search = async () => {
|
||||
state.data = res;
|
||||
};
|
||||
|
||||
const getDetail = async (id: number) => {
|
||||
const tags = await tagApi.listByQuery.request({ id });
|
||||
return tags?.[0];
|
||||
};
|
||||
|
||||
// 树节点右击事件
|
||||
const nodeContextmenu = (event: any, data: any) => {
|
||||
const { clientX, clientY } = event;
|
||||
@@ -384,8 +393,8 @@ const nodeContextmenu = (event: any, data: any) => {
|
||||
contextmenuRef.value.openContextmenu(data);
|
||||
};
|
||||
|
||||
const treeNodeClick = (data: any) => {
|
||||
state.currentTag = data;
|
||||
const treeNodeClick = async (data: any) => {
|
||||
state.currentTag = await getDetail(data.id);
|
||||
// 关闭可能存在的右击菜单
|
||||
contextmenuRef.value.closeContextmenu();
|
||||
};
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
<TagCodePath :path="data.tags?.map((tag: any) => tag.codePath)" />
|
||||
</template>
|
||||
|
||||
<template #validityDate="{ data }"> {{ data.validityStartDate }} ~ {{ data.validityEndDate }} </template>
|
||||
|
||||
<template #action="{ data }">
|
||||
<el-button @click.prevent="showMembers(data)" link type="primary">成员</el-button>
|
||||
|
||||
@@ -36,16 +38,30 @@
|
||||
<DrawerHeader :header="addTeamDialog.form.id ? '编辑团队' : '添加团队'" :back="cancelSaveTeam" />
|
||||
</template>
|
||||
|
||||
<el-form ref="teamForm" :model="addTeamDialog.form" label-width="auto">
|
||||
<el-form ref="teamForm" :model="addTeamDialog.form" :rules="teamFormRules" label-width="auto">
|
||||
<el-form-item prop="name" label="团队名" required>
|
||||
<el-input :disabled="addTeamDialog.form.id" v-model="addTeamDialog.form.name" auto-complete="off"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="validityDate" label="生效时间" required>
|
||||
<el-date-picker
|
||||
v-model="addTeamDialog.form.validityDate"
|
||||
type="datetimerange"
|
||||
start-placeholder="生效开始时间"
|
||||
end-placeholder="生效结束时间"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
date-format="YYYY-MM-DD"
|
||||
time-format="HH:mm:ss"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="addTeamDialog.form.remark" auto-complete="off"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="tag" label="标签">
|
||||
<TagTreeCheck v-model="state.addTeamDialog.form.codePaths" :tag-type="0" />
|
||||
<TagTreeCheck height="calc(100vh - 390px)" v-model="state.addTeamDialog.form.codePaths" :tag-type="0" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
@@ -101,16 +117,35 @@ import AccountSelectFormItem from '@/views/system/account/components/AccountSele
|
||||
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||
import TagTreeCheck from '../component/TagTreeCheck.vue';
|
||||
import TagCodePath from '../component/TagCodePath.vue';
|
||||
import { formatDate } from '@/common/utils/format';
|
||||
|
||||
const teamForm: any = ref(null);
|
||||
const pageTableRef: Ref<any> = ref(null);
|
||||
const showMemPageTableRef: Ref<any> = ref(null);
|
||||
|
||||
const teamFormRules = {
|
||||
name: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入团队名',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
validityDate: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择生效时间',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const searchItems = [SearchItem.input('name', '团队名称')];
|
||||
const columns = [
|
||||
TableColumn.new('name', '团队名称'),
|
||||
TableColumn.new('remark', '备注'),
|
||||
TableColumn.new('tags', '分配标签').isSlot().setAddWidth(40),
|
||||
TableColumn.new('validityDate', '有效期').isSlot('validityDate').setMinWidth(310),
|
||||
TableColumn.new('remark', '备注'),
|
||||
TableColumn.new('creator', '创建者'),
|
||||
TableColumn.new('createTime', '创建时间').isTime(),
|
||||
TableColumn.new('modifier', '修改者'),
|
||||
@@ -122,7 +157,7 @@ const state = reactive({
|
||||
currentEditPermissions: false,
|
||||
addTeamDialog: {
|
||||
visible: false,
|
||||
form: { id: 0, name: '', remark: '', codePaths: [] },
|
||||
form: { id: 0, name: '', validityDate: ['', ''], validityStartDate: '', validityEndDate: '', remark: '', codePaths: [] },
|
||||
},
|
||||
query: {
|
||||
pageNum: 1,
|
||||
@@ -172,24 +207,33 @@ const showSaveTeamDialog = async (data: any) => {
|
||||
if (data) {
|
||||
state.addTeamDialog.form.id = data.id;
|
||||
state.addTeamDialog.form.name = data.name;
|
||||
state.addTeamDialog.form.validityDate = [data.validityStartDate, data.validityEndDate];
|
||||
state.addTeamDialog.form.remark = data.remark;
|
||||
state.addTeamDialog.form.codePaths = data.tags?.map((tag: any) => tag.codePath);
|
||||
// state.addTeamDialog.form.tags = await tagApi.getRelateTagIds.request({ relateType: TagTreeRelateTypeEnum.Team.value, relateId: data.id });
|
||||
} else {
|
||||
let end = new Date();
|
||||
end.setFullYear(end.getFullYear() + 10);
|
||||
state.addTeamDialog.form.validityDate = [formatDate(new Date()), formatDate(end)];
|
||||
}
|
||||
|
||||
state.addTeamDialog.visible = true;
|
||||
};
|
||||
|
||||
const saveTeam = async () => {
|
||||
teamForm.value.validate(async (valid: any) => {
|
||||
if (valid) {
|
||||
const form = state.addTeamDialog.form;
|
||||
await tagApi.saveTeam.request(form);
|
||||
ElMessage.success('保存成功');
|
||||
search();
|
||||
cancelSaveTeam();
|
||||
}
|
||||
});
|
||||
try {
|
||||
await teamForm.value.validate();
|
||||
} catch (e: any) {
|
||||
ElMessage.error('请正确填写信息');
|
||||
return false;
|
||||
}
|
||||
|
||||
const form = state.addTeamDialog.form;
|
||||
form.validityStartDate = form.validityDate[0];
|
||||
form.validityEndDate = form.validityDate[1];
|
||||
await tagApi.saveTeam.request(form);
|
||||
ElMessage.success('保存成功');
|
||||
search();
|
||||
cancelSaveTeam();
|
||||
};
|
||||
|
||||
const cancelSaveTeam = () => {
|
||||
|
||||
@@ -114,19 +114,19 @@ watchEffect(() => {
|
||||
});
|
||||
|
||||
const btnOk = async () => {
|
||||
accountForm.value.validate(async (valid: boolean) => {
|
||||
if (!valid) {
|
||||
ElMessage.error('表单填写有误');
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
await accountForm.value.validate();
|
||||
} catch (e: any) {
|
||||
ElMessage.error('请正确填写信息');
|
||||
return false;
|
||||
}
|
||||
|
||||
await saveAccountExec();
|
||||
ElMessage.success('操作成功');
|
||||
emit('val-change', state.form);
|
||||
//重置表单域
|
||||
accountForm.value.resetFields();
|
||||
state.form = {} as any;
|
||||
});
|
||||
await saveAccountExec();
|
||||
ElMessage.success('操作成功');
|
||||
emit('val-change', state.form);
|
||||
//重置表单域
|
||||
accountForm.value.resetFields();
|
||||
state.form = {} as any;
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
<el-table-column property="creator" label="分配账号" width="125"></el-table-column>
|
||||
<el-table-column property="createTime" label="分配时间">
|
||||
<template #default="scope">
|
||||
{{ dateFormat(scope.row.createTime) }}
|
||||
{{ formatDate(scope.row.createTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -60,7 +60,7 @@ import AccountEdit from './AccountEdit.vue';
|
||||
import { AccountStatusEnum } from '../enums';
|
||||
import { accountApi } from '../api';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
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';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-dialog :title="title" v-model="dvisible" :show-close="false" :before-close="cancel" width="900px" :destroy-on-close="true">
|
||||
<el-form ref="configForm" :model="form" label-width="auto">
|
||||
<el-form ref="configForm" :model="form" :rules="rules" label-width="auto">
|
||||
<el-form-item prop="name" label="配置项" required>
|
||||
<el-input v-model="form.name"></el-input>
|
||||
</el-form-item>
|
||||
@@ -44,6 +44,24 @@
|
||||
import { ref, toRefs, reactive, watch, watchEffect } from 'vue';
|
||||
import { configApi, accountApi } from '../api';
|
||||
import { DynamicFormEdit } from '@/components/dynamic-form';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
const rules = {
|
||||
name: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入配置项',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
key: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入配置key',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
@@ -82,31 +100,34 @@ const { dvisible, params, form } = toRefs(state);
|
||||
|
||||
const { isFetching: saveBtnLoading, execute: saveConfigExec } = configApi.save.useApi(form);
|
||||
|
||||
watchEffect(() => {
|
||||
state.dvisible = props.visible;
|
||||
if (!state.dvisible) {
|
||||
return;
|
||||
}
|
||||
watch(
|
||||
() => props.visible,
|
||||
() => {
|
||||
state.dvisible = props.visible;
|
||||
if (!state.dvisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (props.data) {
|
||||
state.form = { ...(props.data as any) };
|
||||
if (state.form.params) {
|
||||
state.params = JSON.parse(state.form.params);
|
||||
if (props.data) {
|
||||
state.form = { ...(props.data as any) };
|
||||
if (state.form.params) {
|
||||
state.params = JSON.parse(state.form.params);
|
||||
} else {
|
||||
state.params = [];
|
||||
}
|
||||
} else {
|
||||
state.form = { permission: 'all' } as any;
|
||||
state.params = [];
|
||||
}
|
||||
} else {
|
||||
state.form = { permission: 'all' } as any;
|
||||
state.params = [];
|
||||
}
|
||||
|
||||
if (state.form.permission != 'all') {
|
||||
const accounts = state.form.permission.split(',');
|
||||
state.permissionAccount = accounts.slice(0, accounts.length - 1);
|
||||
} else {
|
||||
state.permissionAccount = [];
|
||||
if (state.form.permission != 'all') {
|
||||
const accounts = state.form.permission.split(',');
|
||||
state.permissionAccount = accounts.slice(0, accounts.length - 1);
|
||||
} else {
|
||||
state.permissionAccount = [];
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
const cancel = () => {
|
||||
// 更新父组件visible prop对应的值为false
|
||||
@@ -125,22 +146,25 @@ const getAccount = (username: any) => {
|
||||
};
|
||||
|
||||
const btnOk = async () => {
|
||||
configForm.value.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
if (state.params) {
|
||||
state.form.params = JSON.stringify(state.params);
|
||||
}
|
||||
if (state.permissionAccount.length > 0) {
|
||||
state.form.permission = state.permissionAccount.join(',') + ',';
|
||||
} else {
|
||||
state.form.permission = 'all';
|
||||
}
|
||||
try {
|
||||
await configForm.value.validate();
|
||||
} catch (e: any) {
|
||||
ElMessage.error('请正确填写信息');
|
||||
return false;
|
||||
}
|
||||
|
||||
await saveConfigExec();
|
||||
emit('val-change', state.form);
|
||||
cancel();
|
||||
}
|
||||
});
|
||||
if (state.params) {
|
||||
state.form.params = JSON.stringify(state.params);
|
||||
}
|
||||
if (state.permissionAccount.length > 0) {
|
||||
state.form.permission = state.permissionAccount.join(',') + ',';
|
||||
} else {
|
||||
state.form.permission = 'all';
|
||||
}
|
||||
|
||||
await saveConfigExec();
|
||||
emit('val-change', state.form);
|
||||
cancel();
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
|
||||
@@ -254,7 +254,14 @@ const changeLinkType = () => {
|
||||
state.form.meta.component = '';
|
||||
};
|
||||
|
||||
const btnOk = () => {
|
||||
const btnOk = async () => {
|
||||
try {
|
||||
await menuForm.value.validate();
|
||||
} catch (e: any) {
|
||||
ElMessage.error('请正确填写信息');
|
||||
return false;
|
||||
}
|
||||
|
||||
const submitForm = { ...state.form };
|
||||
if (submitForm.type == 1) {
|
||||
// 如果是菜单,则解析meta,如果值为false或者''则去除该值
|
||||
@@ -263,16 +270,12 @@ const btnOk = () => {
|
||||
submitForm.meta = null as any;
|
||||
}
|
||||
|
||||
menuForm.value.validate(async (valid: any) => {
|
||||
if (valid) {
|
||||
state.submitForm = submitForm;
|
||||
await saveResouceExec();
|
||||
state.submitForm = submitForm;
|
||||
await saveResouceExec();
|
||||
|
||||
emit('val-change', submitForm);
|
||||
ElMessage.success('保存成功');
|
||||
cancel();
|
||||
}
|
||||
});
|
||||
emit('val-change', submitForm);
|
||||
ElMessage.success('保存成功');
|
||||
cancel();
|
||||
};
|
||||
|
||||
const parseMenuMeta = (meta: any) => {
|
||||
@@ -314,10 +317,4 @@ const cancel = () => {
|
||||
emit('cancel');
|
||||
};
|
||||
</script>
|
||||
<style lang="scss">
|
||||
// .m-dialog {
|
||||
// .el-cascader {
|
||||
// width: 100%;
|
||||
// }
|
||||
// }
|
||||
</style>
|
||||
<style lang="scss"></style>
|
||||
|
||||
@@ -89,9 +89,9 @@
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="创建者">{{ currentResource.creator }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ dateFormat(currentResource.createTime) }} </el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ formatDate(currentResource.createTime) }} </el-descriptions-item>
|
||||
<el-descriptions-item label="修改者">{{ currentResource.modifier }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">{{ dateFormat(currentResource.updateTime) }} </el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">{{ formatDate(currentResource.updateTime) }} </el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
@@ -119,10 +119,11 @@ import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import ResourceEdit from './ResourceEdit.vue';
|
||||
import { ResourceTypeEnum } from '../enums';
|
||||
import { resourceApi } from '../api';
|
||||
import { dateFormat } from '@/common/utils/date';
|
||||
import { formatDate } from '@/common/utils/format';
|
||||
import EnumTag from '@/components/enumtag/EnumTag.vue';
|
||||
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
|
||||
import { Splitpanes, Pane } from 'splitpanes';
|
||||
import { isPrefixSubsequence } from '@/common/utils/string';
|
||||
|
||||
const menuTypeValue = ResourceTypeEnum.Menu.value;
|
||||
const permissionTypeValue = ResourceTypeEnum.Permission.value;
|
||||
@@ -209,10 +210,7 @@ watch(filterResource, (val) => {
|
||||
});
|
||||
|
||||
const filterNode = (value: string, data: any) => {
|
||||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
return data.name.includes(value);
|
||||
return !value || isPrefixSubsequence(value, data.name);
|
||||
};
|
||||
|
||||
const search = async () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="role-dialog">
|
||||
<el-dialog :title="title" v-model="dvisible" :show-close="false" :before-close="cancel" width="500px" :destroy-on-close="true">
|
||||
<el-form ref="roleForm" :model="form" label-width="auto">
|
||||
<el-form ref="roleForm" :model="form" :rules="rules" label-width="auto">
|
||||
<el-form-item prop="name" label="角色名称" required>
|
||||
<el-input v-model="form.name" auto-complete="off"></el-input>
|
||||
</el-form-item>
|
||||
@@ -32,6 +32,30 @@ import { ref, toRefs, reactive, watchEffect } from 'vue';
|
||||
import { roleApi } from '../api';
|
||||
import { RoleStatusEnum } from '../enums';
|
||||
|
||||
const rules = {
|
||||
name: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入角色名称',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
code: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入角色编号',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
status: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择状态',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
@@ -80,13 +104,15 @@ const cancel = () => {
|
||||
};
|
||||
|
||||
const btnOk = async () => {
|
||||
roleForm.value.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
await saveRoleExec();
|
||||
emit('val-change', state.form);
|
||||
cancel();
|
||||
}
|
||||
});
|
||||
try {
|
||||
await roleForm.value.validate();
|
||||
} catch (e: any) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await saveRoleExec();
|
||||
emit('val-change', state.form);
|
||||
cancel();
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
{{ data.creator }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="分配时间">
|
||||
{{ dateFormat(data.createTime) }}
|
||||
{{ formatDate(data.createTime) }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
@@ -35,7 +35,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, reactive, watch } from 'vue';
|
||||
import { ResourceTypeEnum } from '../enums';
|
||||
import { dateFormat } from '@/common/utils/date';
|
||||
import { formatDate } from '@/common/utils/format';
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
|
||||
@@ -11,9 +11,11 @@ server:
|
||||
cert-file: ./default.pem
|
||||
jwt:
|
||||
# jwt key,不设置默认使用随机字符串
|
||||
key:
|
||||
# 过期时间单位分钟
|
||||
expire-time: 1440
|
||||
key: 333333000000
|
||||
# accessToken过期时间单位分钟
|
||||
expire-time: 720
|
||||
# refreshToken过期时间单位分钟
|
||||
refresh-token-expire-time: 4320
|
||||
# 资源密码aes加密key
|
||||
aes:
|
||||
key: 1111111111111111
|
||||
|
||||
@@ -7,13 +7,13 @@ require (
|
||||
gitee.com/liuzongyang/libpq v1.0.9
|
||||
github.com/buger/jsonparser v1.1.1
|
||||
github.com/emirpasic/gods v1.18.1
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.0
|
||||
github.com/go-ldap/ldap/v3 v3.4.8
|
||||
github.com/go-playground/locales v0.14.1
|
||||
github.com/go-playground/universal-translator v0.18.1
|
||||
github.com/go-playground/validator/v10 v10.14.0
|
||||
github.com/go-playground/validator/v10 v10.20.0
|
||||
github.com/go-sql-driver/mysql v1.8.1
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
github.com/google/uuid v1.6.0
|
||||
@@ -28,12 +28,12 @@ require (
|
||||
github.com/pquerna/otp v1.4.0
|
||||
github.com/redis/go-redis/v9 v9.5.1
|
||||
github.com/robfig/cron/v3 v3.0.1 // 定时任务
|
||||
github.com/sijms/go-ora/v2 v2.8.13
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/sijms/go-ora/v2 v2.8.19
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/veops/go-ansiterm v0.0.5
|
||||
go.mongodb.org/mongo-driver v1.15.0 // mongo
|
||||
golang.org/x/crypto v0.22.0 // ssh
|
||||
golang.org/x/oauth2 v0.19.0
|
||||
golang.org/x/crypto v0.24.0 // ssh
|
||||
golang.org/x/oauth2 v0.21.0
|
||||
golang.org/x/sync v0.7.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
@@ -46,13 +46,15 @@ require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
|
||||
github.com/bytedance/sonic v1.9.1 // indirect
|
||||
github.com/bytedance/sonic v1.11.6 // indirect
|
||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect
|
||||
@@ -67,36 +69,36 @@ require (
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.16.5 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/kr/fs v0.1.0 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.17 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/montanaflynn/stats v0.7.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rivo/uniseg v0.4.3 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||
github.com/xdg-go/scram v1.1.2 // indirect
|
||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230519143937-03e91628a987 // indirect
|
||||
golang.org/x/image v0.13.0 // indirect
|
||||
golang.org/x/net v0.22.0 // indirect
|
||||
golang.org/x/sys v0.19.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/net v0.25.0 // indirect
|
||||
golang.org/x/sys v0.21.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230131230820-1c016267d619 // indirect
|
||||
google.golang.org/grpc v1.52.3 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
modernc.org/libc v1.22.5 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.5.0 // indirect
|
||||
|
||||
@@ -2,7 +2,6 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"mayfly-go/internal/auth/api/form"
|
||||
"mayfly-go/internal/auth/config"
|
||||
@@ -71,11 +70,12 @@ func (a *AccountLogin) Login(rc *req.Ctx) {
|
||||
}
|
||||
|
||||
type OtpVerifyInfo struct {
|
||||
AccountId uint64
|
||||
Username string
|
||||
OptStatus int
|
||||
AccessToken string
|
||||
OtpSecret string
|
||||
AccountId uint64
|
||||
Username string
|
||||
OptStatus int
|
||||
AccessToken string
|
||||
RefreshToken string
|
||||
OtpSecret string
|
||||
}
|
||||
|
||||
// OTP双因素校验
|
||||
@@ -84,10 +84,9 @@ func (a *AccountLogin) OtpVerify(rc *req.Ctx) {
|
||||
req.BindJsonAndValid(rc, otpVerify)
|
||||
|
||||
tokenKey := fmt.Sprintf("otp:token:%s", otpVerify.OtpToken)
|
||||
otpInfoJson := cache.GetStr(tokenKey)
|
||||
biz.NotEmpty(otpInfoJson, "otpToken错误或失效, 请重新登陆获取")
|
||||
otpInfo := new(OtpVerifyInfo)
|
||||
json.Unmarshal([]byte(otpInfoJson), otpInfo)
|
||||
ok := cache.Get(tokenKey, otpInfo)
|
||||
biz.IsTrue(ok, "otpToken错误或失效, 请重新登陆获取")
|
||||
|
||||
failCountKey := fmt.Sprintf("account:otp:failcount:%d", otpInfo.AccountId)
|
||||
failCount := cache.GetInt(failCountKey)
|
||||
@@ -116,7 +115,19 @@ func (a *AccountLogin) OtpVerify(rc *req.Ctx) {
|
||||
go saveLogin(la, getIpAndRegion(rc))
|
||||
|
||||
cache.Del(tokenKey)
|
||||
rc.ResData = accessToken
|
||||
rc.ResData = collx.Kvs("token", accessToken, "refresh_token", otpInfo.RefreshToken)
|
||||
}
|
||||
|
||||
func (a *AccountLogin) RefreshToken(rc *req.Ctx) {
|
||||
refreshToken := rc.Query("refresh_token")
|
||||
biz.NotEmpty(refreshToken, "refresh_token不能为空")
|
||||
|
||||
accountId, username, err := req.ParseToken(refreshToken)
|
||||
biz.IsTrueBy(err == nil, errorx.PermissionErr)
|
||||
|
||||
token, refreshToken, err := req.CreateToken(accountId, username)
|
||||
biz.ErrIsNil(err)
|
||||
rc.ResData = collx.Kvs("token", token, "refresh_token", refreshToken)
|
||||
}
|
||||
|
||||
func (a *AccountLogin) Logout(rc *req.Ctx) {
|
||||
|
||||
@@ -41,18 +41,19 @@ func LastLoginCheck(account *sysentity.Account, accountLoginSecurity *config.Acc
|
||||
// 默认为不校验otp
|
||||
otpStatus := OtpStatusNone
|
||||
// 访问系统使用的token
|
||||
accessToken, err := req.CreateToken(account.Id, username)
|
||||
accessToken, refreshToken, err := req.CreateToken(account.Id, username)
|
||||
biz.ErrIsNilAppendErr(err, "token创建失败: %s")
|
||||
|
||||
// 若系统配置中设置开启otp双因素校验,则进行otp校验
|
||||
if accountLoginSecurity.UseOtp {
|
||||
otpInfo, otpurl, otpToken := useOtp(account, accountLoginSecurity.OtpIssuer, accessToken)
|
||||
otpInfo, otpurl, otpToken := useOtp(account, accountLoginSecurity.OtpIssuer, accessToken, refreshToken)
|
||||
otpStatus = otpInfo.OptStatus
|
||||
if otpurl != "" {
|
||||
res["otpUrl"] = otpurl
|
||||
}
|
||||
accessToken = otpToken
|
||||
} else {
|
||||
res["refresh_token"] = refreshToken
|
||||
// 不进行otp二次校验则直接返回accessToken
|
||||
// 保存登录消息
|
||||
go saveLogin(account, loginIp)
|
||||
@@ -64,7 +65,7 @@ func LastLoginCheck(account *sysentity.Account, accountLoginSecurity *config.Acc
|
||||
return res
|
||||
}
|
||||
|
||||
func useOtp(account *sysentity.Account, otpIssuer, accessToken string) (*OtpVerifyInfo, string, string) {
|
||||
func useOtp(account *sysentity.Account, otpIssuer, accessToken string, refreshToken string) (*OtpVerifyInfo, string, string) {
|
||||
biz.ErrIsNil(account.OtpSecretDecrypt())
|
||||
otpSecret := account.OtpSecret
|
||||
// 修改状态为已注册
|
||||
@@ -83,13 +84,14 @@ func useOtp(account *sysentity.Account, otpIssuer, accessToken string) (*OtpVeri
|
||||
otpUrl = key.URL()
|
||||
otpSecret = key.Secret()
|
||||
}
|
||||
// 缓存otpInfo, 只有双因素校验通过才可返回真正的accessToken
|
||||
// 缓存otpInfo, 只有双因素校验通过才可返回真正的token
|
||||
otpInfo := &OtpVerifyInfo{
|
||||
AccountId: account.Id,
|
||||
Username: account.Username,
|
||||
OptStatus: otpStatus,
|
||||
OtpSecret: otpSecret,
|
||||
AccessToken: accessToken,
|
||||
AccountId: account.Id,
|
||||
Username: account.Username,
|
||||
OptStatus: otpStatus,
|
||||
OtpSecret: otpSecret,
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
}
|
||||
cache.SetStr(fmt.Sprintf("otp:token:%s", token), jsonx.ToStr(otpInfo), time.Minute*time.Duration(3))
|
||||
return otpInfo, otpUrl, token
|
||||
|
||||
@@ -171,7 +171,7 @@ func (a *Oauth2Login) doLoginAction(rc *req.Ctx, userId string, oauth *config.Oa
|
||||
}
|
||||
|
||||
// 进行登录
|
||||
account, err := a.AccountApp.GetById(new(sysentity.Account), accountId, "Id", "Name", "Username", "Password", "Status", "LastLoginTime", "LastLoginIp", "OtpSecret")
|
||||
account, err := a.AccountApp.GetById(accountId, "Id", "Name", "Username", "Password", "Status", "LastLoginTime", "LastLoginIp", "OtpSecret")
|
||||
biz.ErrIsNilAppendErr(err, "获取用户信息失败: %s")
|
||||
|
||||
clientIp := getIpAndRegion(rc)
|
||||
|
||||
@@ -26,6 +26,8 @@ func Init(router *gin.RouterGroup) {
|
||||
// 用户账号密码登录
|
||||
req.NewPost("/accounts/login", accountLogin.Login).Log(req.NewLogSave("用户登录")).DontNeedToken(),
|
||||
|
||||
req.NewGet("/accounts/refreshToken", accountLogin.RefreshToken).DontNeedToken(),
|
||||
|
||||
// 用户退出登录
|
||||
req.NewPost("/accounts/logout", accountLogin.Logout),
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@ import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"mayfly-go/internal/common/consts"
|
||||
"mayfly-go/internal/db/api/form"
|
||||
"mayfly-go/internal/db/api/vo"
|
||||
"mayfly-go/internal/db/application"
|
||||
"mayfly-go/internal/db/application/dto"
|
||||
"mayfly-go/internal/db/config"
|
||||
"mayfly-go/internal/db/dbm/dbi"
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
@@ -57,10 +57,20 @@ func (d *Db) Dbs(rc *req.Ctx) {
|
||||
res, err := d.DbApp.GetPageList(queryCond, page, &dbvos)
|
||||
biz.ErrIsNil(err)
|
||||
|
||||
// 填充标签信息
|
||||
d.TagApp.FillTagInfo(tagentity.TagTypeDbName, collx.ArrayMap(dbvos, func(dbvo *vo.DbListVO) tagentity.ITagResource {
|
||||
return dbvo
|
||||
})...)
|
||||
instances, _ := d.InstanceApp.GetByIds(collx.ArrayMap(dbvos, func(i *vo.DbListVO) uint64 {
|
||||
return i.InstanceId
|
||||
}))
|
||||
instancesMap := collx.ArrayToMap(instances, func(i *entity.DbInstance) uint64 {
|
||||
return i.Id
|
||||
})
|
||||
for _, dbvo := range dbvos {
|
||||
di := instancesMap[dbvo.InstanceId]
|
||||
if di != nil {
|
||||
dbvo.InstanceType = di.Type
|
||||
dbvo.Host = di.Host
|
||||
dbvo.Port = di.Port
|
||||
}
|
||||
}
|
||||
|
||||
rc.ResData = res
|
||||
}
|
||||
@@ -84,8 +94,6 @@ func (d *Db) DeleteDb(rc *req.Ctx) {
|
||||
dbId := cast.ToUint64(v)
|
||||
biz.NotBlank(dbId, "存在错误dbId")
|
||||
biz.ErrIsNil(d.DbApp.Delete(ctx, dbId))
|
||||
// 删除该库的sql执行记录
|
||||
d.DbSqlExecApp.DeleteBy(ctx, &entity.DbSqlExec{DbId: dbId})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,9 +105,9 @@ func (d *Db) ExecSql(rc *req.Ctx) {
|
||||
dbId := getDbId(rc)
|
||||
dbConn, err := d.DbApp.GetDbConn(dbId, form.Db)
|
||||
biz.ErrIsNil(err)
|
||||
biz.ErrIsNilAppendErr(d.TagApp.CanAccess(rc.GetLoginAccount().Id, dbConn.Info.TagPath...), "%s")
|
||||
biz.ErrIsNilAppendErr(d.TagApp.CanAccess(rc.GetLoginAccount().Id, dbConn.Info.CodePath...), "%s")
|
||||
|
||||
global.EventBus.Publish(rc.MetaCtx, event.EventTopicResourceOp, dbConn.Info.TagPath[0])
|
||||
global.EventBus.Publish(rc.MetaCtx, event.EventTopicResourceOp, dbConn.Info.CodePath[0])
|
||||
|
||||
sqlBytes, err := base64.StdEncoding.DecodeString(form.Sql)
|
||||
biz.ErrIsNilAppendErr(err, "sql解码失败: %s")
|
||||
@@ -174,10 +182,8 @@ func (d *Db) ExecSqlFile(rc *req.Ctx) {
|
||||
clientId := rc.Query("clientId")
|
||||
|
||||
dbConn, err := d.DbApp.GetDbConn(dbId, dbName)
|
||||
// 开启流程审批时,执行文件暂时还未处理
|
||||
biz.IsTrue(dbConn.Info.FlowProcdefKey == "", "该库已开启流程审批,暂不支持该操作")
|
||||
biz.ErrIsNil(err)
|
||||
biz.ErrIsNilAppendErr(d.TagApp.CanAccess(rc.GetLoginAccount().Id, dbConn.Info.TagPath...), "%s")
|
||||
biz.ErrIsNilAppendErr(d.TagApp.CanAccess(rc.GetLoginAccount().Id, dbConn.Info.CodePath...), "%s")
|
||||
rc.ReqParam = fmt.Sprintf("filename: %s -> %s", filename, dbConn.Info.GetLogDesc())
|
||||
|
||||
defer func() {
|
||||
@@ -245,7 +251,7 @@ func (d *Db) ExecSqlFile(rc *req.Ctx) {
|
||||
}
|
||||
dbConn, err = d.DbApp.GetDbConn(dbId, stmtUse.DBName.String())
|
||||
biz.ErrIsNil(err)
|
||||
biz.ErrIsNilAppendErr(d.TagApp.CanAccess(laId, dbConn.Info.TagPath...), "%s")
|
||||
biz.ErrIsNilAppendErr(d.TagApp.CanAccess(laId, dbConn.Info.CodePath...), "%s")
|
||||
execReq.DbConn = dbConn
|
||||
}
|
||||
// 需要记录执行记录
|
||||
@@ -282,12 +288,12 @@ func (d *Db) DumpSql(rc *req.Ctx) {
|
||||
needData := dumpType == "2" || dumpType == "3"
|
||||
|
||||
la := rc.GetLoginAccount()
|
||||
db, err := d.DbApp.GetById(new(entity.Db), dbId)
|
||||
biz.ErrIsNil(err, "该数据库不存在")
|
||||
biz.ErrIsNilAppendErr(d.TagApp.CanAccess(la.Id, d.TagApp.ListTagPathByTypeAndCode(consts.ResourceTypeDb, db.Code)...), "%s")
|
||||
dbConn, err := d.DbApp.GetDbConn(dbId, dbName)
|
||||
biz.ErrIsNil(err)
|
||||
biz.ErrIsNilAppendErr(d.TagApp.CanAccess(la.Id, dbConn.Info.CodePath...), "%s")
|
||||
|
||||
now := time.Now()
|
||||
filename := fmt.Sprintf("%s-%s.%s.sql%s", db.Name, dbName, now.Format("20060102150405"), extName)
|
||||
filename := fmt.Sprintf("%s-%s.%s.sql%s", dbConn.Info.Name, dbName, now.Format("20060102150405"), extName)
|
||||
rc.Header("Content-Type", "application/octet-stream")
|
||||
rc.Header("Content-Disposition", "attachment; filename="+filename)
|
||||
if extName != ".gz" {
|
||||
@@ -308,7 +314,7 @@ func (d *Db) DumpSql(rc *req.Ctx) {
|
||||
}
|
||||
}()
|
||||
|
||||
biz.ErrIsNil(d.DbApp.DumpDb(rc.MetaCtx, &application.DumpDbReq{
|
||||
biz.ErrIsNil(d.DbApp.DumpDb(rc.MetaCtx, &dto.DumpDb{
|
||||
DbId: dbId,
|
||||
DbName: dbName,
|
||||
Tables: tables,
|
||||
@@ -317,7 +323,7 @@ func (d *Db) DumpSql(rc *req.Ctx) {
|
||||
Writer: rc.GetWriter(),
|
||||
}))
|
||||
|
||||
rc.ReqParam = collx.Kvs("db", db, "database", dbName, "tables", tablesStr, "dumpType", dumpType)
|
||||
rc.ReqParam = collx.Kvs("db", dbConn.Info, "database", dbName, "tables", tablesStr, "dumpType", dumpType)
|
||||
}
|
||||
|
||||
func (d *Db) TableInfos(rc *req.Ctx) {
|
||||
|
||||
@@ -27,7 +27,7 @@ type DbBackup struct {
|
||||
func (d *DbBackup) GetPageList(rc *req.Ctx) {
|
||||
dbId := uint64(rc.PathParamInt("dbId"))
|
||||
biz.IsTrue(dbId > 0, "无效的 dbId: %v", dbId)
|
||||
db, err := d.dbApp.GetById(new(entity.Db), dbId, "db_instance_id", "database")
|
||||
db, err := d.dbApp.GetById(dbId)
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
|
||||
|
||||
queryCond, page := req.BindQueryAndPage[*entity.DbBackupQuery](rc, new(entity.DbBackupQuery))
|
||||
@@ -49,7 +49,7 @@ func (d *DbBackup) Create(rc *req.Ctx) {
|
||||
|
||||
dbId := uint64(rc.PathParamInt("dbId"))
|
||||
biz.IsTrue(dbId > 0, "无效的 dbId: %v", dbId)
|
||||
db, err := d.dbApp.GetById(new(entity.Db), dbId, "instanceId")
|
||||
db, err := d.dbApp.GetById(dbId)
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
|
||||
jobs := make([]*entity.DbBackup, 0, len(dbNames))
|
||||
for _, dbName := range dbNames {
|
||||
@@ -134,7 +134,7 @@ func (d *DbBackup) Start(rc *req.Ctx) {
|
||||
// @router /api/dbs/:dbId/db-names-without-backup [GET]
|
||||
func (d *DbBackup) GetDbNamesWithoutBackup(rc *req.Ctx) {
|
||||
dbId := uint64(rc.PathParamInt("dbId"))
|
||||
db, err := d.dbApp.GetById(new(entity.Db), dbId, "instance_id", "database")
|
||||
db, err := d.dbApp.GetById(dbId, "instance_id", "database")
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
|
||||
dbNames := strings.Fields(db.Database)
|
||||
dbNamesWithoutBackup, err := d.backupApp.GetDbNamesWithoutBackup(db.InstanceId, dbNames)
|
||||
@@ -147,7 +147,7 @@ func (d *DbBackup) GetDbNamesWithoutBackup(rc *req.Ctx) {
|
||||
func (d *DbBackup) GetHistoryPageList(rc *req.Ctx) {
|
||||
dbId := uint64(rc.PathParamInt("dbId"))
|
||||
biz.IsTrue(dbId > 0, "无效的 dbId: %v", dbId)
|
||||
db, err := d.dbApp.GetById(new(entity.Db), dbId, "db_instance_id", "database")
|
||||
db, err := d.dbApp.GetById(dbId, "instance_id", "database")
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
|
||||
|
||||
backupHistoryCond, page := req.BindQueryAndPage[*entity.DbBackupHistoryQuery](rc, new(entity.DbBackupHistoryQuery))
|
||||
|
||||
@@ -64,7 +64,7 @@ func (d *DataSyncTask) ChangeStatus(rc *req.Ctx) {
|
||||
_ = d.DataSyncTaskApp.UpdateById(rc.MetaCtx, task)
|
||||
|
||||
if task.Status == entity.DataSyncTaskStatusEnable {
|
||||
task, err := d.DataSyncTaskApp.GetById(new(entity.DataSyncTask), task.Id)
|
||||
task, err := d.DataSyncTaskApp.GetById(task.Id)
|
||||
biz.ErrIsNil(err, "该任务不存在")
|
||||
d.DataSyncTaskApp.AddCronJob(rc.MetaCtx, task)
|
||||
} else {
|
||||
@@ -92,7 +92,7 @@ func (d *DataSyncTask) Stop(rc *req.Ctx) {
|
||||
|
||||
func (d *DataSyncTask) GetTask(rc *req.Ctx) {
|
||||
taskId := d.getTaskId(rc)
|
||||
dbEntity, _ := d.DataSyncTaskApp.GetById(new(entity.DataSyncTask), taskId)
|
||||
dbEntity, _ := d.DataSyncTaskApp.GetById(taskId)
|
||||
rc.ResData = dbEntity
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"mayfly-go/internal/db/api/form"
|
||||
"mayfly-go/internal/db/api/vo"
|
||||
"mayfly-go/internal/db/application"
|
||||
"mayfly-go/internal/db/application/dto"
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
|
||||
tagapp "mayfly-go/internal/tag/application"
|
||||
@@ -70,7 +71,7 @@ func (d *Instance) SaveInstance(rc *req.Ctx) {
|
||||
instance := req.BindJsonAndCopyTo[*entity.DbInstance](rc, form, new(entity.DbInstance))
|
||||
|
||||
rc.ReqParam = form
|
||||
id, err := d.InstanceApp.SaveDbInstance(rc.MetaCtx, &application.SaveDbInstanceParam{
|
||||
id, err := d.InstanceApp.SaveDbInstance(rc.MetaCtx, &dto.SaveDbInstance{
|
||||
DbInstance: instance,
|
||||
AuthCerts: form.AuthCerts,
|
||||
TagCodePaths: form.TagCodePaths,
|
||||
@@ -83,7 +84,7 @@ func (d *Instance) SaveInstance(rc *req.Ctx) {
|
||||
// @router /api/instances/:instance [GET]
|
||||
func (d *Instance) GetInstance(rc *req.Ctx) {
|
||||
dbId := getInstanceId(rc)
|
||||
dbEntity, err := d.InstanceApp.GetById(new(entity.DbInstance), dbId)
|
||||
dbEntity, err := d.InstanceApp.GetById(dbId)
|
||||
biz.ErrIsNil(err, "获取数据库实例错误")
|
||||
rc.ResData = dbEntity
|
||||
}
|
||||
@@ -113,6 +114,12 @@ func (d *Instance) GetDatabaseNames(rc *req.Ctx) {
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
func (d *Instance) GetDatabaseNamesByAc(rc *req.Ctx) {
|
||||
res, err := d.InstanceApp.GetDatabasesByAc(rc.PathParam("ac"))
|
||||
biz.ErrIsNil(err)
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
// 获取数据库实例server信息
|
||||
func (d *Instance) GetDbServer(rc *req.Ctx) {
|
||||
instanceId := getInstanceId(rc)
|
||||
|
||||
@@ -22,7 +22,7 @@ type DbRestore struct {
|
||||
func (d *DbRestore) GetPageList(rc *req.Ctx) {
|
||||
dbId := uint64(rc.PathParamInt("dbId"))
|
||||
biz.IsTrue(dbId > 0, "无效的 dbId: %v", dbId)
|
||||
db, err := d.dbApp.GetById(new(entity.Db), dbId, "db_instance_id", "database")
|
||||
db, err := d.dbApp.GetById(dbId, "db_instance_id", "database")
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
|
||||
|
||||
var restores []vo.DbRestore
|
||||
@@ -43,7 +43,7 @@ func (d *DbRestore) Create(rc *req.Ctx) {
|
||||
|
||||
dbId := uint64(rc.PathParamInt("dbId"))
|
||||
biz.IsTrue(dbId > 0, "无效的 dbId: %v", dbId)
|
||||
db, err := d.dbApp.GetById(new(entity.Db), dbId, "instanceId")
|
||||
db, err := d.dbApp.GetById(dbId, "instanceId")
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
|
||||
|
||||
job := &entity.DbRestore{
|
||||
@@ -123,7 +123,7 @@ func (d *DbRestore) Disable(rc *req.Ctx) {
|
||||
// @router /api/dbs/:dbId/db-names-without-backup [GET]
|
||||
func (d *DbRestore) GetDbNamesWithoutRestore(rc *req.Ctx) {
|
||||
dbId := uint64(rc.PathParamInt("dbId"))
|
||||
db, err := d.dbApp.GetById(new(entity.Db), dbId, "instance_id", "database")
|
||||
db, err := d.dbApp.GetById(dbId, "instance_id", "database")
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
|
||||
dbNames := strings.Fields(db.Database)
|
||||
dbNamesWithoutRestore, err := d.restoreApp.GetDbNamesWithoutRestore(db.InstanceId, dbNames)
|
||||
|
||||
@@ -43,8 +43,7 @@ func (d *DbSql) GetSqlNames(rc *req.Ctx) {
|
||||
// 获取用于是否有该dbsql的保存记录,有则更改,否则新增
|
||||
dbSql := &entity.DbSql{Type: 1, DbId: dbId, Db: dbName}
|
||||
dbSql.CreatorId = rc.GetLoginAccount().Id
|
||||
var sqls []entity.DbSql
|
||||
d.DbSqlApp.ListByCond(model.NewModelCond(dbSql).Columns("id", "name"), &sqls)
|
||||
sqls, _ := d.DbSqlApp.ListByCond(model.NewModelCond(dbSql).Columns("id", "name"))
|
||||
|
||||
rc.ResData = sqls
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
package form
|
||||
|
||||
import "mayfly-go/internal/db/domain/entity"
|
||||
|
||||
type DbForm struct {
|
||||
Id uint64 `json:"id"`
|
||||
Code string `binding:"required" json:"code"`
|
||||
Name string `binding:"required" json:"name"`
|
||||
Database string `json:"database"`
|
||||
Remark string `json:"remark"`
|
||||
InstanceId uint64 `binding:"required" json:"instanceId"`
|
||||
AuthCertName string `json:"authCertName"`
|
||||
FlowProcdefKey string `json:"flowProcdefKey"`
|
||||
Id uint64 `json:"id"`
|
||||
Code string `binding:"required" json:"code"`
|
||||
Name string `binding:"required" json:"name"`
|
||||
GetDatabaseMode entity.DbGetDatabaseMode `json:"getDatabaseMode"` // 获取数据库方式
|
||||
Database string `json:"database"`
|
||||
Remark string `json:"remark"`
|
||||
InstanceId uint64 `binding:"required" json:"instanceId"`
|
||||
AuthCertName string `json:"authCertName"`
|
||||
}
|
||||
|
||||
type DbSqlSaveForm struct {
|
||||
|
||||
@@ -1,27 +1,23 @@
|
||||
package vo
|
||||
|
||||
import (
|
||||
tagentity "mayfly-go/internal/tag/domain/entity"
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DbListVO struct {
|
||||
tagentity.ResourceTags
|
||||
Id *int64 `json:"id"`
|
||||
Code string `json:"code"`
|
||||
Name *string `json:"name"`
|
||||
GetDatabaseMode entity.DbGetDatabaseMode `json:"getDatabaseMode"` // 获取数据库方式
|
||||
Database *string `json:"database"`
|
||||
Remark *string `json:"remark"`
|
||||
InstanceId uint64 `json:"instanceId"`
|
||||
AuthCertName string `json:"authCertName"`
|
||||
|
||||
Id *int64 `json:"id"`
|
||||
Code string `json:"code"`
|
||||
Name *string `json:"name"`
|
||||
Database *string `json:"database"`
|
||||
Remark *string `json:"remark"`
|
||||
|
||||
InstanceId *int64 `json:"instanceId"`
|
||||
AuthCertName string `json:"authCertName"`
|
||||
InstanceName *string `json:"instanceName"`
|
||||
InstanceType *string `json:"type"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
|
||||
FlowProcdefKey string `json:"flowProcdefKey"`
|
||||
InstanceType string `json:"type" gorm:"-"`
|
||||
Host string `json:"host" gorm:"-"`
|
||||
Port int `json:"port" gorm:"-"`
|
||||
|
||||
CreateTime *time.Time `json:"createTime"`
|
||||
Creator *string `json:"creator"`
|
||||
@@ -30,7 +26,3 @@ type DbListVO struct {
|
||||
Modifier *string `json:"modifier"`
|
||||
ModifierId *int64 `json:"modifierId"`
|
||||
}
|
||||
|
||||
func (d DbListVO) GetCode() string {
|
||||
return d.Code
|
||||
}
|
||||
|
||||
@@ -3,11 +3,13 @@ package application
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"mayfly-go/internal/db/application/dto"
|
||||
"mayfly-go/internal/db/dbm"
|
||||
"mayfly-go/internal/db/dbm/dbi"
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
"mayfly-go/internal/db/domain/repository"
|
||||
tagapp "mayfly-go/internal/tag/application"
|
||||
tagdto "mayfly-go/internal/tag/application/dto"
|
||||
tagentity "mayfly-go/internal/tag/domain/entity"
|
||||
"mayfly-go/pkg/base"
|
||||
"mayfly-go/pkg/biz"
|
||||
@@ -40,7 +42,7 @@ type Db interface {
|
||||
GetDbConnByInstanceId(instanceId uint64) (*dbi.DbConn, error)
|
||||
|
||||
// DumpDb dumpDb
|
||||
DumpDb(ctx context.Context, reqParam *DumpDbReq) error
|
||||
DumpDb(ctx context.Context, reqParam *dto.DumpDb) error
|
||||
}
|
||||
|
||||
type dbAppImpl struct {
|
||||
@@ -48,10 +50,13 @@ type dbAppImpl struct {
|
||||
|
||||
dbSqlRepo repository.DbSql `inject:"DbSqlRepo"`
|
||||
dbInstanceApp Instance `inject:"DbInstanceApp"`
|
||||
dbSqlExecApp DbSqlExec `inject:"DbSqlExecApp"`
|
||||
tagApp tagapp.TagTree `inject:"TagTreeApp"`
|
||||
resourceAuthCertApp tagapp.ResourceAuthCert `inject:"ResourceAuthCertApp"`
|
||||
}
|
||||
|
||||
var _ (Db) = (*dbAppImpl)(nil)
|
||||
|
||||
// 注入DbRepo
|
||||
func (d *dbAppImpl) InjectDbRepo(repo repository.Db) {
|
||||
d.Repo = repo
|
||||
@@ -85,8 +90,8 @@ func (d *dbAppImpl) SaveDb(ctx context.Context, dbEntity *entity.Db) error {
|
||||
return d.Insert(ctx, dbEntity)
|
||||
}, func(ctx context.Context) error {
|
||||
// 将库关联至指定数据库授权凭证下
|
||||
return d.tagApp.RelateTagsByCodeAndType(ctx, &tagapp.RelateTagsByCodeAndTypeParam{
|
||||
Tags: []*tagapp.ResourceTag{{
|
||||
return d.tagApp.RelateTagsByCodeAndType(ctx, &tagdto.RelateTagsByCodeAndType{
|
||||
Tags: []*tagdto.ResourceTag{{
|
||||
Code: dbEntity.Code,
|
||||
Type: tagentity.TagTypeDbName,
|
||||
Name: dbEntity.Name,
|
||||
@@ -103,7 +108,7 @@ func (d *dbAppImpl) SaveDb(ctx context.Context, dbEntity *entity.Db) error {
|
||||
}
|
||||
|
||||
dbId := dbEntity.Id
|
||||
old, err := d.GetById(new(entity.Db), dbId)
|
||||
old, err := d.GetById(dbId)
|
||||
if err != nil {
|
||||
return errorx.NewBiz("该数据库不存在")
|
||||
}
|
||||
@@ -142,7 +147,7 @@ func (d *dbAppImpl) SaveDb(ctx context.Context, dbEntity *entity.Db) error {
|
||||
}
|
||||
|
||||
func (d *dbAppImpl) Delete(ctx context.Context, id uint64) error {
|
||||
db, err := d.GetById(new(entity.Db), id)
|
||||
db, err := d.GetById(id)
|
||||
if err != nil {
|
||||
return errorx.NewBiz("该数据库不存在")
|
||||
}
|
||||
@@ -160,7 +165,9 @@ func (d *dbAppImpl) Delete(ctx context.Context, id uint64) error {
|
||||
// 删除该库下用户保存的所有sql信息
|
||||
return d.dbSqlRepo.DeleteByCond(ctx, &entity.DbSql{DbId: id})
|
||||
}, func(ctx context.Context) error {
|
||||
return d.tagApp.DeleteTagByParam(ctx, &tagapp.DelResourceTagParam{
|
||||
return d.dbSqlExecApp.DeleteBy(ctx, &entity.DbSqlExec{DbId: id})
|
||||
}, func(ctx context.Context) error {
|
||||
return d.tagApp.DeleteTagByParam(ctx, &tagdto.DelResourceTag{
|
||||
ResourceCode: db.Code,
|
||||
ResourceType: tagentity.TagTypeDbName,
|
||||
})
|
||||
@@ -169,12 +176,12 @@ func (d *dbAppImpl) Delete(ctx context.Context, id uint64) error {
|
||||
|
||||
func (d *dbAppImpl) GetDbConn(dbId uint64, dbName string) (*dbi.DbConn, error) {
|
||||
return dbm.GetDbConn(dbId, dbName, func() (*dbi.DbInfo, error) {
|
||||
db, err := d.GetById(new(entity.Db), dbId)
|
||||
db, err := d.GetById(dbId)
|
||||
if err != nil {
|
||||
return nil, errorx.NewBiz("数据库信息不存在")
|
||||
}
|
||||
|
||||
instance, err := d.dbInstanceApp.GetById(new(entity.DbInstance), db.InstanceId)
|
||||
instance, err := d.dbInstanceApp.GetById(db.InstanceId)
|
||||
if err != nil {
|
||||
return nil, errorx.NewBiz("数据库实例不存在")
|
||||
}
|
||||
@@ -183,15 +190,11 @@ func (d *dbAppImpl) GetDbConn(dbId uint64, dbName string) (*dbi.DbConn, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
di.TagPath = d.tagApp.ListTagPathByTypeAndCode(int8(tagentity.TagTypeDbName), db.Code)
|
||||
di.CodePath = d.tagApp.ListTagPathByTypeAndCode(int8(tagentity.TagTypeDbName), db.Code)
|
||||
di.Id = db.Id
|
||||
|
||||
if db.FlowProcdefKey != nil {
|
||||
di.FlowProcdefKey = *db.FlowProcdefKey
|
||||
}
|
||||
|
||||
checkDb := di.GetDatabase()
|
||||
if !strings.Contains(" "+db.Database+" ", " "+checkDb+" ") {
|
||||
if db.GetDatabaseMode == entity.DbGetDatabaseModeAssign && !strings.Contains(" "+db.Database+" ", " "+checkDb+" ") {
|
||||
return nil, errorx.NewBiz("未配置数据库【%s】的操作权限", dbName)
|
||||
}
|
||||
|
||||
@@ -205,9 +208,8 @@ func (d *dbAppImpl) GetDbConnByInstanceId(instanceId uint64) (*dbi.DbConn, error
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
var dbs []*entity.Db
|
||||
|
||||
if err := d.ListByCond(model.NewModelCond(&entity.Db{InstanceId: instanceId}).Columns("id", "database"), &dbs); err != nil {
|
||||
dbs, err := d.ListByCond(&entity.Db{InstanceId: instanceId}, "id", "database")
|
||||
if err != nil {
|
||||
return nil, errorx.NewBiz("获取数据库列表失败")
|
||||
}
|
||||
if len(dbs) == 0 {
|
||||
@@ -219,7 +221,7 @@ func (d *dbAppImpl) GetDbConnByInstanceId(instanceId uint64) (*dbi.DbConn, error
|
||||
return d.GetDbConn(firstDb.Id, strings.Split(firstDb.Database, " ")[0])
|
||||
}
|
||||
|
||||
func (d *dbAppImpl) DumpDb(ctx context.Context, reqParam *DumpDbReq) error {
|
||||
func (d *dbAppImpl) DumpDb(ctx context.Context, reqParam *dto.DumpDb) error {
|
||||
writer := newGzipWriter(reqParam.Writer)
|
||||
defer writer.Close()
|
||||
dbId := reqParam.DbId
|
||||
@@ -295,7 +297,7 @@ func (d *dbAppImpl) DumpDb(ctx context.Context, reqParam *DumpDbReq) error {
|
||||
|
||||
// 生成insert sql,数据在索引前,加速insert
|
||||
if reqParam.DumpData {
|
||||
writer.WriteString(fmt.Sprintf("\n-- ----------------------------\n-- 表记录: %s \n-- ----------------------------\n", tableName))
|
||||
writer.WriteString(fmt.Sprintf("\n-- ----------------------------\n-- 表数据: %s \n-- ----------------------------\n", tableName))
|
||||
|
||||
dumpHelper.BeforeInsert(writer, quoteTableName)
|
||||
// 获取列信息
|
||||
|
||||
@@ -63,21 +63,18 @@ func (app *DbBackupApp) Init() error {
|
||||
}
|
||||
|
||||
func (app *DbBackupApp) prune(ctx context.Context) error {
|
||||
var jobs []*entity.DbBackup
|
||||
if err := app.backupRepo.ListByCond(map[string]any{}, &jobs); err != nil {
|
||||
jobs, err := app.backupRepo.SelectByCond(map[string]any{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, job := range jobs {
|
||||
if ctx.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
var histories []*entity.DbBackupHistory
|
||||
historyCond := map[string]any{
|
||||
"db_backup_id": job.Id,
|
||||
}
|
||||
if err := app.backupHistoryRepo.SelectByCond(historyCond, &histories); err != nil {
|
||||
return err
|
||||
}
|
||||
histories, _ := app.backupHistoryRepo.SelectByCond(historyCond)
|
||||
expiringTime := time.Now().Add(-math.MaxInt64)
|
||||
if job.MaxSaveDays > 0 {
|
||||
expiringTime = time.Now().Add(-time.Hour * 24 * time.Duration(job.MaxSaveDays+1))
|
||||
@@ -160,8 +157,8 @@ func (app *DbBackupApp) Enable(ctx context.Context, jobId uint64) error {
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
repo := app.backupRepo
|
||||
job := &entity.DbBackup{}
|
||||
if err := repo.GetById(job, jobId); err != nil {
|
||||
job, err := repo.GetById(jobId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if job.IsEnabled() {
|
||||
@@ -183,8 +180,8 @@ func (app *DbBackupApp) Disable(ctx context.Context, jobId uint64) error {
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
repo := app.backupRepo
|
||||
job := &entity.DbBackup{}
|
||||
if err := repo.GetById(job, jobId); err != nil {
|
||||
job, err := repo.GetById(jobId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !job.IsEnabled() {
|
||||
@@ -202,8 +199,8 @@ func (app *DbBackupApp) StartNow(ctx context.Context, jobId uint64) error {
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
job := &entity.DbBackup{}
|
||||
if err := app.backupRepo.GetById(job, jobId); err != nil {
|
||||
job, err := app.backupRepo.GetById(jobId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !job.IsEnabled() {
|
||||
@@ -267,8 +264,8 @@ func (app *DbBackupApp) DeleteHistory(ctx context.Context, historyId uint64) (re
|
||||
if !ok {
|
||||
return errRestoringBackupHistory
|
||||
}
|
||||
job := &entity.DbBackupHistory{}
|
||||
if err := app.backupHistoryRepo.GetById(job, historyId); err != nil {
|
||||
job, err := app.backupHistoryRepo.GetById(historyId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conn, err := app.dbApp.GetDbConnByInstanceId(job.DbInstanceId)
|
||||
|
||||
@@ -75,8 +75,8 @@ func (app *DbBinlogApp) fetchBinlog(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (app *DbBinlogApp) pruneBinlog(ctx context.Context) error {
|
||||
var jobs []*entity.DbBinlog
|
||||
if err := app.binlogRepo.SelectByCond(map[string]any{}, &jobs); err != nil {
|
||||
jobs, err := app.binlogRepo.SelectByCond(map[string]any{})
|
||||
if err != nil {
|
||||
logx.Error("DbBinlogApp: 获取 BINLOG 同步任务失败: ", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ func (app *dataSyncAppImpl) Save(ctx context.Context, taskEntity *entity.DataSyn
|
||||
return err
|
||||
}
|
||||
|
||||
task, err := app.GetById(new(entity.DataSyncTask), taskEntity.Id)
|
||||
task, err := app.GetById(taskEntity.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -112,7 +112,7 @@ func (app *dataSyncAppImpl) AddCronJob(ctx context.Context, taskEntity *entity.D
|
||||
}
|
||||
|
||||
func (app *dataSyncAppImpl) RemoveCronJobById(taskId uint64) {
|
||||
task, err := app.GetById(new(entity.DataSyncTask), taskId)
|
||||
task, err := app.GetById(taskId)
|
||||
if err == nil {
|
||||
scheduler.RemoveByKey(task.TaskKey)
|
||||
}
|
||||
@@ -127,7 +127,7 @@ func (app *dataSyncAppImpl) changeRunningState(id uint64, state int8) {
|
||||
|
||||
func (app *dataSyncAppImpl) RunCronJob(ctx context.Context, id uint64) error {
|
||||
// 查询最新的任务信息
|
||||
task, err := app.GetById(new(entity.DataSyncTask), id)
|
||||
task, err := app.GetById(id)
|
||||
if err != nil {
|
||||
return errorx.NewBiz("任务不存在")
|
||||
}
|
||||
@@ -369,7 +369,7 @@ func (app *dataSyncAppImpl) srcData2TargetDb(srcRes []map[string]any, fieldMap [
|
||||
}
|
||||
|
||||
// 运行过程中,判断状态是否为已关闭,是则结束运行,否则继续运行
|
||||
taskParam, _ := app.GetById(new(entity.DataSyncTask), task.Id)
|
||||
taskParam, _ := app.GetById(task.Id)
|
||||
if taskParam.RunningState == entity.DataSyncTaskRunStateStop {
|
||||
return errorx.NewBiz("该任务已被手动终止")
|
||||
}
|
||||
@@ -410,7 +410,7 @@ func (app *dataSyncAppImpl) InitCronJob() {
|
||||
}()
|
||||
|
||||
// 修改执行中状态为待执行
|
||||
_ = app.UpdateByCond(context.TODO(), &entity.DataSyncTask{RunningState: entity.DataSyncTaskRunStateRunning}, &entity.DataSyncTask{RunningState: entity.DataSyncTaskRunStateReady})
|
||||
_ = app.UpdateByCond(context.TODO(), &entity.DataSyncTask{RunningState: entity.DataSyncTaskRunStateReady}, &entity.DataSyncTask{RunningState: entity.DataSyncTaskRunStateRunning})
|
||||
|
||||
// 把所有正常任务添加到定时任务中
|
||||
pageParam := &model.PageParam{
|
||||
|
||||
@@ -2,31 +2,23 @@ package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"mayfly-go/internal/common/consts"
|
||||
"mayfly-go/internal/db/application/dto"
|
||||
"mayfly-go/internal/db/dbm"
|
||||
"mayfly-go/internal/db/dbm/dbi"
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
"mayfly-go/internal/db/domain/repository"
|
||||
tagapp "mayfly-go/internal/tag/application"
|
||||
tagdto "mayfly-go/internal/tag/application/dto"
|
||||
tagentity "mayfly-go/internal/tag/domain/entity"
|
||||
"mayfly-go/pkg/base"
|
||||
"mayfly-go/pkg/biz"
|
||||
"mayfly-go/pkg/errorx"
|
||||
"mayfly-go/pkg/logx"
|
||||
"mayfly-go/pkg/model"
|
||||
"mayfly-go/pkg/utils/collx"
|
||||
"mayfly-go/pkg/utils/structx"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type SaveDbInstanceParam struct {
|
||||
DbInstance *entity.DbInstance
|
||||
AuthCerts []*tagentity.ResourceAuthCert
|
||||
TagCodePaths []string
|
||||
}
|
||||
|
||||
type Instance interface {
|
||||
base.App[*entity.DbInstance]
|
||||
|
||||
@@ -35,7 +27,7 @@ type Instance interface {
|
||||
|
||||
TestConn(instanceEntity *entity.DbInstance, authCert *tagentity.ResourceAuthCert) error
|
||||
|
||||
SaveDbInstance(ctx context.Context, instance *SaveDbInstanceParam) (uint64, error)
|
||||
SaveDbInstance(ctx context.Context, instance *dto.SaveDbInstance) (uint64, error)
|
||||
|
||||
// Delete 删除数据库信息
|
||||
Delete(ctx context.Context, id uint64) error
|
||||
@@ -43,6 +35,9 @@ type Instance interface {
|
||||
// GetDatabases 获取数据库实例的所有数据库列表
|
||||
GetDatabases(entity *entity.DbInstance, authCert *tagentity.ResourceAuthCert) ([]string, error)
|
||||
|
||||
// GetDatabasesByAc 根据授权凭证名获取所有数据库名称列表
|
||||
GetDatabasesByAc(acName string) ([]string, error)
|
||||
|
||||
// ToDbInfo 根据实例与授权凭证返回对应的DbInfo
|
||||
ToDbInfo(instance *entity.DbInstance, authCertName string, database string) (*dbi.DbInfo, error)
|
||||
}
|
||||
@@ -57,6 +52,8 @@ type instanceAppImpl struct {
|
||||
restoreApp *DbRestoreApp `inject:"DbRestoreApp"`
|
||||
}
|
||||
|
||||
var _ (Instance) = (*instanceAppImpl)(nil)
|
||||
|
||||
// 注入DbInstanceRepo
|
||||
func (app *instanceAppImpl) InjectDbInstanceRepo(repo repository.Instance) {
|
||||
app.Repo = repo
|
||||
@@ -91,7 +88,7 @@ func (app *instanceAppImpl) TestConn(instanceEntity *entity.DbInstance, authCert
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *instanceAppImpl) SaveDbInstance(ctx context.Context, instance *SaveDbInstanceParam) (uint64, error) {
|
||||
func (app *instanceAppImpl) SaveDbInstance(ctx context.Context, instance *dto.SaveDbInstance) (uint64, error) {
|
||||
instanceEntity := instance.DbInstance
|
||||
// 默认tcp连接
|
||||
instanceEntity.Network = instanceEntity.GetNetwork()
|
||||
@@ -122,13 +119,13 @@ func (app *instanceAppImpl) SaveDbInstance(ctx context.Context, instance *SaveDb
|
||||
return instanceEntity.Id, app.Tx(ctx, func(ctx context.Context) error {
|
||||
return app.Insert(ctx, instanceEntity)
|
||||
}, func(ctx context.Context) error {
|
||||
return app.resourceAuthCertApp.RelateAuthCert(ctx, &tagapp.RelateAuthCertParam{
|
||||
return app.resourceAuthCertApp.RelateAuthCert(ctx, &tagdto.RelateAuthCert{
|
||||
ResourceCode: instanceEntity.Code,
|
||||
ResourceType: tagentity.TagType(resourceType),
|
||||
AuthCerts: authCerts,
|
||||
})
|
||||
}, func(ctx context.Context) error {
|
||||
return app.tagApp.SaveResourceTag(ctx, &tagapp.SaveResourceTagParam{
|
||||
return app.tagApp.SaveResourceTag(ctx, &tagdto.SaveResourceTag{
|
||||
ResourceTag: app.genDbInstanceResourceTag(instanceEntity, authCerts),
|
||||
ParentTagCodePaths: tagCodePaths,
|
||||
})
|
||||
@@ -142,7 +139,7 @@ func (app *instanceAppImpl) SaveDbInstance(ctx context.Context, instance *SaveDb
|
||||
}
|
||||
} else {
|
||||
// 根据host等未查到旧数据,则需要根据id重新获取,因为后续需要使用到code
|
||||
oldInstance, err = app.GetById(new(entity.DbInstance), instanceEntity.Id)
|
||||
oldInstance, err = app.GetById(instanceEntity.Id)
|
||||
if err != nil {
|
||||
return 0, errorx.NewBiz("该数据库实例不存在")
|
||||
}
|
||||
@@ -151,7 +148,7 @@ func (app *instanceAppImpl) SaveDbInstance(ctx context.Context, instance *SaveDb
|
||||
return oldInstance.Id, app.Tx(ctx, func(ctx context.Context) error {
|
||||
return app.UpdateById(ctx, instanceEntity)
|
||||
}, func(ctx context.Context) error {
|
||||
return app.resourceAuthCertApp.RelateAuthCert(ctx, &tagapp.RelateAuthCertParam{
|
||||
return app.resourceAuthCertApp.RelateAuthCert(ctx, &tagdto.RelateAuthCert{
|
||||
ResourceCode: oldInstance.Code,
|
||||
ResourceType: tagentity.TagType(resourceType),
|
||||
AuthCerts: authCerts,
|
||||
@@ -162,7 +159,7 @@ func (app *instanceAppImpl) SaveDbInstance(ctx context.Context, instance *SaveDb
|
||||
return err
|
||||
}
|
||||
}
|
||||
return app.tagApp.SaveResourceTag(ctx, &tagapp.SaveResourceTagParam{
|
||||
return app.tagApp.SaveResourceTag(ctx, &tagdto.SaveResourceTag{
|
||||
ResourceTag: app.genDbInstanceResourceTag(instanceEntity, authCerts),
|
||||
ParentTagCodePaths: tagCodePaths,
|
||||
})
|
||||
@@ -170,7 +167,7 @@ func (app *instanceAppImpl) SaveDbInstance(ctx context.Context, instance *SaveDb
|
||||
}
|
||||
|
||||
func (app *instanceAppImpl) Delete(ctx context.Context, instanceId uint64) error {
|
||||
instance, err := app.GetById(new(entity.DbInstance), instanceId, "name")
|
||||
instance, err := app.GetById(instanceId)
|
||||
if err != nil {
|
||||
return errorx.NewBiz("获取数据库实例错误,数据库实例ID为: %d", instance.Id)
|
||||
}
|
||||
@@ -179,54 +176,43 @@ func (app *instanceAppImpl) Delete(ctx context.Context, instanceId uint64) error
|
||||
DbInstanceId: instanceId,
|
||||
}
|
||||
err = app.restoreApp.restoreRepo.GetByCond(restore)
|
||||
switch {
|
||||
case err == nil:
|
||||
biz.ErrNotNil(err, "不能删除数据库实例【%s】,请先删除关联的数据库恢复任务。", instance.Name)
|
||||
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||
break
|
||||
default:
|
||||
biz.ErrIsNil(err, "删除数据库实例失败: %v", err)
|
||||
if err != nil {
|
||||
return errorx.NewBiz("不能删除数据库实例【%s】,请先删除关联的数据库恢复任务。", instance.Name)
|
||||
}
|
||||
|
||||
backup := &entity.DbBackup{
|
||||
DbInstanceId: instanceId,
|
||||
}
|
||||
err = app.backupApp.backupRepo.GetByCond(backup)
|
||||
switch {
|
||||
case err == nil:
|
||||
biz.ErrNotNil(err, "不能删除数据库实例【%s】,请先删除关联的数据库备份任务。", instance.Name)
|
||||
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||
break
|
||||
default:
|
||||
biz.ErrIsNil(err, "删除数据库实例失败: %v", err)
|
||||
if err != nil {
|
||||
return errorx.NewBiz("不能删除数据库实例【%s】,请先删除关联的数据库备份任务。", instance.Name)
|
||||
}
|
||||
|
||||
db := &entity.Db{
|
||||
dbs, _ := app.dbApp.ListByCond(&entity.Db{
|
||||
InstanceId: instanceId,
|
||||
}
|
||||
err = app.dbApp.GetByCond(db)
|
||||
switch {
|
||||
case err == nil:
|
||||
biz.ErrNotNil(err, "不能删除数据库实例【%s】,请先删除关联的数据库资源。", instance.Name)
|
||||
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||
break
|
||||
default:
|
||||
biz.ErrIsNil(err, "删除数据库实例失败: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
return app.Tx(ctx, func(ctx context.Context) error {
|
||||
return app.DeleteById(ctx, instanceId)
|
||||
}, func(ctx context.Context) error {
|
||||
// 删除该实例关联的授权凭证信息
|
||||
return app.resourceAuthCertApp.RelateAuthCert(ctx, &tagapp.RelateAuthCertParam{
|
||||
return app.resourceAuthCertApp.RelateAuthCert(ctx, &tagdto.RelateAuthCert{
|
||||
ResourceCode: instance.Code,
|
||||
ResourceType: tagentity.TagType(consts.ResourceTypeDb),
|
||||
})
|
||||
}, func(ctx context.Context) error {
|
||||
return app.tagApp.DeleteTagByParam(ctx, &tagapp.DelResourceTagParam{
|
||||
return app.tagApp.DeleteTagByParam(ctx, &tagdto.DelResourceTag{
|
||||
ResourceCode: instance.Code,
|
||||
ResourceType: tagentity.TagType(consts.ResourceTypeDb),
|
||||
})
|
||||
}, func(ctx context.Context) error {
|
||||
// 删除所有库配置
|
||||
for _, db := range dbs {
|
||||
if err := app.dbApp.Delete(ctx, db.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -244,16 +230,22 @@ func (app *instanceAppImpl) GetDatabases(ed *entity.DbInstance, authCert *tagent
|
||||
}
|
||||
}
|
||||
|
||||
ed.Network = ed.GetNetwork()
|
||||
dbi := app.toDbInfoByAc(ed, authCert, "")
|
||||
return app.getDatabases(ed, authCert)
|
||||
}
|
||||
|
||||
dbConn, err := dbm.Conn(dbi)
|
||||
func (app *instanceAppImpl) GetDatabasesByAc(acName string) ([]string, error) {
|
||||
ac, err := app.resourceAuthCertApp.GetAuthCert(acName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, errorx.NewBiz("该授权凭证不存在")
|
||||
}
|
||||
defer dbConn.Close()
|
||||
|
||||
return dbConn.GetMetaData().GetDbNames()
|
||||
instance := &entity.DbInstance{Code: ac.ResourceCode}
|
||||
err = app.GetByCond(instance)
|
||||
if err != nil {
|
||||
return nil, errorx.NewBiz("不存在该授权凭证对应的数据库实例信息")
|
||||
}
|
||||
|
||||
return app.getDatabases(instance, ac)
|
||||
}
|
||||
|
||||
func (app *instanceAppImpl) ToDbInfo(instance *entity.DbInstance, authCertName string, database string) (*dbi.DbInfo, error) {
|
||||
@@ -265,6 +257,19 @@ func (app *instanceAppImpl) ToDbInfo(instance *entity.DbInstance, authCertName s
|
||||
return app.toDbInfoByAc(instance, ac, database), nil
|
||||
}
|
||||
|
||||
func (app *instanceAppImpl) getDatabases(instance *entity.DbInstance, ac *tagentity.ResourceAuthCert) ([]string, error) {
|
||||
instance.Network = instance.GetNetwork()
|
||||
dbi := app.toDbInfoByAc(instance, ac, "")
|
||||
|
||||
dbConn, err := dbm.Conn(dbi)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer dbConn.Close()
|
||||
|
||||
return dbConn.GetMetaData().GetDbNames()
|
||||
}
|
||||
|
||||
func (app *instanceAppImpl) toDbInfoByAc(instance *entity.DbInstance, ac *tagentity.ResourceAuthCert, database string) *dbi.DbInfo {
|
||||
di := new(dbi.DbInfo)
|
||||
di.InstanceId = instance.Id
|
||||
@@ -276,25 +281,25 @@ func (app *instanceAppImpl) toDbInfoByAc(instance *entity.DbInstance, ac *tagent
|
||||
return di
|
||||
}
|
||||
|
||||
func (m *instanceAppImpl) genDbInstanceResourceTag(me *entity.DbInstance, authCerts []*tagentity.ResourceAuthCert) *tagapp.ResourceTag {
|
||||
authCertTags := collx.ArrayMap[*tagentity.ResourceAuthCert, *tagapp.ResourceTag](authCerts, func(val *tagentity.ResourceAuthCert) *tagapp.ResourceTag {
|
||||
return &tagapp.ResourceTag{
|
||||
func (m *instanceAppImpl) genDbInstanceResourceTag(me *entity.DbInstance, authCerts []*tagentity.ResourceAuthCert) *tagdto.ResourceTag {
|
||||
authCertTags := collx.ArrayMap[*tagentity.ResourceAuthCert, *tagdto.ResourceTag](authCerts, func(val *tagentity.ResourceAuthCert) *tagdto.ResourceTag {
|
||||
return &tagdto.ResourceTag{
|
||||
Code: val.Name,
|
||||
Name: val.Username,
|
||||
Type: tagentity.TagTypeDbAuthCert,
|
||||
}
|
||||
})
|
||||
|
||||
var dbs []*entity.Db
|
||||
if err := m.dbApp.ListByCond(&entity.Db{
|
||||
dbs, err := m.dbApp.ListByCond(&entity.Db{
|
||||
InstanceId: me.Id,
|
||||
}, &dbs); err != nil {
|
||||
})
|
||||
if err != nil {
|
||||
logx.Errorf("获取实例关联的数据库失败: %v", err)
|
||||
}
|
||||
|
||||
authCertName2DbTags := make(map[string][]*tagapp.ResourceTag)
|
||||
authCertName2DbTags := make(map[string][]*tagdto.ResourceTag)
|
||||
for _, db := range dbs {
|
||||
authCertName2DbTags[db.AuthCertName] = append(authCertName2DbTags[db.AuthCertName], &tagapp.ResourceTag{
|
||||
authCertName2DbTags[db.AuthCertName] = append(authCertName2DbTags[db.AuthCertName], &tagdto.ResourceTag{
|
||||
Code: db.Code,
|
||||
Name: db.Name,
|
||||
Type: tagentity.TagTypeDbName,
|
||||
@@ -306,7 +311,7 @@ func (m *instanceAppImpl) genDbInstanceResourceTag(me *entity.DbInstance, authCe
|
||||
ac.Children = authCertName2DbTags[ac.Code]
|
||||
}
|
||||
|
||||
return &tagapp.ResourceTag{
|
||||
return &tagdto.ResourceTag{
|
||||
Code: me.Code,
|
||||
Type: tagentity.TagTypeDb,
|
||||
Name: me.Name,
|
||||
|
||||
@@ -78,8 +78,8 @@ func (app *DbRestoreApp) Enable(ctx context.Context, jobId uint64) error {
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
repo := app.restoreRepo
|
||||
job := &entity.DbRestore{}
|
||||
if err := repo.GetById(job, jobId); err != nil {
|
||||
job, err := repo.GetById(jobId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if job.IsEnabled() {
|
||||
@@ -101,8 +101,8 @@ func (app *DbRestoreApp) Disable(ctx context.Context, jobId uint64) error {
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
repo := app.restoreRepo
|
||||
job := &entity.DbRestore{}
|
||||
if err := repo.GetById(job, jobId); err != nil {
|
||||
job, err := repo.GetById(jobId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !job.IsEnabled() {
|
||||
|
||||
@@ -4,8 +4,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"golang.org/x/sync/singleflight"
|
||||
"gorm.io/gorm"
|
||||
"mayfly-go/internal/db/dbm/dbi"
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
"mayfly-go/internal/db/domain/repository"
|
||||
@@ -14,6 +12,9 @@ import (
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sync/singleflight"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -173,8 +174,8 @@ func (s *dbScheduler) restore(ctx context.Context, dbProgram dbi.DbProgram, rest
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
backupHistory := &entity.DbBackupHistory{}
|
||||
if err := s.backupHistoryRepo.GetById(backupHistory, restore.DbBackupHistoryId); err != nil {
|
||||
backupHistory, err := s.backupHistoryRepo.GetById(restore.DbBackupHistoryId)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
err = errors.New("备份历史已删除")
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
"mayfly-go/internal/db/domain/repository"
|
||||
flowapp "mayfly-go/internal/flow/application"
|
||||
flowdto "mayfly-go/internal/flow/application/dto"
|
||||
flowentity "mayfly-go/internal/flow/domain/entity"
|
||||
"mayfly-go/pkg/contextx"
|
||||
"mayfly-go/pkg/errorx"
|
||||
@@ -56,7 +57,7 @@ type DbSqlExec interface {
|
||||
Exec(ctx context.Context, execSqlReq *DbSqlExecReq) (*DbSqlExecRes, error)
|
||||
|
||||
// 根据条件删除sql执行记录
|
||||
DeleteBy(ctx context.Context, condition *entity.DbSqlExec)
|
||||
DeleteBy(ctx context.Context, condition *entity.DbSqlExec) error
|
||||
|
||||
// 分页获取
|
||||
GetPageList(condition *entity.DbSqlExecQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
|
||||
@@ -67,6 +68,7 @@ type dbSqlExecAppImpl struct {
|
||||
dbSqlExecRepo repository.DbSqlExec `inject:"DbSqlExecRepo"`
|
||||
|
||||
flowProcinstApp flowapp.Procinst `inject:"ProcinstApp"`
|
||||
flowProcdefApp flowapp.Procdef `inject:"ProcdefApp"`
|
||||
}
|
||||
|
||||
func createSqlExecRecord(ctx context.Context, execSqlReq *DbSqlExecReq) *entity.DbSqlExec {
|
||||
@@ -196,8 +198,8 @@ func (d *dbSqlExecAppImpl) FlowBizHandle(ctx context.Context, bizHandleParam *fl
|
||||
return d.dbSqlExecRepo.UpdateById(ctx, dbSqlExec)
|
||||
}
|
||||
|
||||
func (d *dbSqlExecAppImpl) DeleteBy(ctx context.Context, condition *entity.DbSqlExec) {
|
||||
d.dbSqlExecRepo.DeleteByCond(ctx, condition)
|
||||
func (d *dbSqlExecAppImpl) DeleteBy(ctx context.Context, condition *entity.DbSqlExec) error {
|
||||
return d.dbSqlExecRepo.DeleteByCond(ctx, condition)
|
||||
}
|
||||
|
||||
func (d *dbSqlExecAppImpl) GetPageList(condition *entity.DbSqlExecQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
@@ -348,11 +350,11 @@ func (d *dbSqlExecAppImpl) doInsert(ctx context.Context, insert *sqlparser.Inser
|
||||
|
||||
func (d *dbSqlExecAppImpl) doExec(ctx context.Context, execSqlReq *DbSqlExecReq, dbSqlExecRecord *entity.DbSqlExec) (*DbSqlExecRes, error) {
|
||||
dbConn := execSqlReq.DbConn
|
||||
flowProcdefKey := dbConn.Info.FlowProcdefKey
|
||||
if flowProcdefKey != "" {
|
||||
|
||||
if flowProcdefId := d.flowProcdefApp.GetProcdefIdByCodePath(ctx, dbConn.Info.CodePath...); flowProcdefId != 0 {
|
||||
bizKey := stringx.Rand(24)
|
||||
// 如果该库关联了审批流程,则启动流程实例即可
|
||||
_, err := d.flowProcinstApp.StartProc(ctx, flowProcdefKey, &flowapp.StarProcParam{
|
||||
_, err := d.flowProcinstApp.StartProc(ctx, flowProcdefId, &flowdto.StarProc{
|
||||
BizType: DbSqlExecFlowBizType,
|
||||
BizKey: bizKey,
|
||||
Remark: dbSqlExecRecord.Remark,
|
||||
|
||||
@@ -2,6 +2,7 @@ package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"mayfly-go/internal/db/dbm/dbi"
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
@@ -87,16 +88,20 @@ func (app *dbTransferAppImpl) CreateLog(ctx context.Context, taskId uint64) (uin
|
||||
}
|
||||
|
||||
func (app *dbTransferAppImpl) Run(ctx context.Context, taskId uint64, logId uint64) {
|
||||
task, err := app.GetById(new(entity.DbTransferTask), taskId)
|
||||
defer app.logApp.Flush(logId, true)
|
||||
|
||||
task, err := app.GetById(taskId)
|
||||
if err != nil {
|
||||
logx.Errorf("创建DBMS-执行数据迁移日志失败:%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if app.IsRunning(taskId) {
|
||||
logx.Warnf("[%d]该任务正在运行中...", taskId)
|
||||
return
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
|
||||
defer app.logApp.Flush(logId, true)
|
||||
|
||||
// 修改状态与关联日志id
|
||||
task.LogId = logId
|
||||
task.RunningState = entity.DbTransferTaskRunStateRunning
|
||||
@@ -150,7 +155,7 @@ func (app *dbTransferAppImpl) Run(ctx context.Context, taskId uint64, logId uint
|
||||
}
|
||||
|
||||
func (app *dbTransferAppImpl) Stop(ctx context.Context, taskId uint64) error {
|
||||
task, err := app.GetById(new(entity.DbTransferTask), taskId)
|
||||
task, err := app.GetById(taskId)
|
||||
if err != nil {
|
||||
return errorx.NewBiz("任务不存在")
|
||||
}
|
||||
@@ -322,6 +327,8 @@ func (app *dbTransferAppImpl) transfer2Target(taskId uint64, targetConn *dbi.DbC
|
||||
columnNames = append(columnNames, targetMeta.QuoteIdentifier(col.ColumnName))
|
||||
}
|
||||
|
||||
dataHelper := targetMeta.GetDataHelper()
|
||||
|
||||
// 从目标库数据中取出源库字段对应的值
|
||||
values := make([][]any, 0)
|
||||
for _, record := range result {
|
||||
@@ -338,6 +345,14 @@ func (app *dbTransferAppImpl) transfer2Target(taskId uint64, targetConn *dbi.DbC
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if dataHelper.GetDataType(string(tc.DataType)) == dbi.DataTypeBlob {
|
||||
decodeBytes, err := hex.DecodeString(val.(string))
|
||||
if err == nil {
|
||||
val = decodeBytes
|
||||
}
|
||||
}
|
||||
|
||||
rawValue = append(rawValue, val)
|
||||
}
|
||||
values = append(values, rawValue)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user