feat: 数据迁移新增实时日志&数据库游标遍历查询问题修复

This commit is contained in:
meilin.huang
2024-03-28 22:20:39 +08:00
parent 5e4793433b
commit d1d372e1bf
31 changed files with 477 additions and 344 deletions

View File

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

View File

@@ -124,6 +124,8 @@ function initTerm() {
state.addon.fit = fitAddon;
term.loadAddon(fitAddon);
fitTerminal();
// 注册窗口大小监听器
useEventListener('resize', debounce(fitTerminal, 400));
// 注册搜索组件
const searchAddon = new SearchAddon();
@@ -148,10 +150,11 @@ function initTerm() {
}
function initSocket() {
if (props.socketUrl) {
socket = new WebSocket(`${props.socketUrl}&rows=${term?.rows}&cols=${term?.cols}`);
if (!props.socketUrl) {
return;
}
socket = new WebSocket(`${props.socketUrl}&rows=${term?.rows}&cols=${term?.cols}`);
// 监听socket连接
socket.onopen = () => {
// 注册心跳
@@ -162,8 +165,6 @@ function initSocket() {
term.onResize((event) => sendResize(event.cols, event.rows));
term.onData((event) => sendCmd(event));
// // 注册窗口大小监听器
useEventListener('resize', debounce(fitTerminal, 400));
focus();
// 如果有初始要执行的命令,则发送执行命令
@@ -187,10 +188,19 @@ function initSocket() {
// 监听socket消息
socket.onmessage = (msg: any) => {
// msg.data是真正后端返回的数据
term.write(msg.data);
write2Term(msg.data);
};
}
// 写入内容至终端
const write2Term = (data: any) => {
term.write(data);
};
const writeln2Term = (data: any) => {
term.writeln(data);
};
const getTerminalTheme = () => {
const terminalTheme = themeConfig.value.terminalTheme;
// 如果不是自定义主题,则返回内置主题
@@ -229,7 +239,7 @@ enum MsgType {
}
const send = (msg: any) => {
state.status == TerminalStatus.Connected && socket.send(msg);
state.status == TerminalStatus.Connected && socket?.send(msg);
};
const sendResize = (cols: number, rows: number) => {
@@ -266,7 +276,7 @@ const getStatus = (): TerminalStatus => {
return state.status;
};
defineExpose({ init, fitTerminal, focus, clear, close, getStatus, sendResize });
defineExpose({ init, fitTerminal, focus, clear, close, getStatus, sendResize, write2Term, writeln2Term });
</script>
<style lang="scss">
#terminal-body {
@@ -276,9 +286,9 @@ defineExpose({ init, fitTerminal, focus, clear, close, getStatus, sendResize });
width: 100%;
height: 100%;
.xterm .xterm-viewport {
overflow-y: hidden;
}
// .xterm .xterm-viewport {
// overflow-y: hidden;
// }
}
}
</style>

View File

@@ -0,0 +1,102 @@
<template>
<div>
<el-drawer v-model="visible" :before-close="cancel" size="50%">
<template #header>
<DrawerHeader :header="props.title" :back="cancel">
<template #extra>
<EnumTag :enums="LogTypeEnum" :value="log?.type" class="mr20" />
</template>
</DrawerHeader>
</template>
<TerminalBody ref="terminalRef" />
</el-drawer>
</div>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import TerminalBody from './TerminalBody.vue';
import { logApi } from '../../views/system/api';
import { LogTypeEnum } from '@/views/system/enums';
import { useIntervalFn } from '@vueuse/core';
import EnumTag from '@/components/enumtag/EnumTag.vue';
const props = defineProps({
title: {
type: String,
default: '日志',
},
});
const visible = defineModel<boolean>('visible', { default: false });
const logId = defineModel<number>('logId', { default: 0 });
const terminalRef: any = ref(null);
const nowLine = ref(0);
const log = ref({}) as any;
// 定时获取最新日志
const { pause, resume } = useIntervalFn(() => {
writeLog();
}, 2000);
watch(
() => logId.value,
(logId: number) => {
if (!logId) {
return;
}
terminalRef.value?.clear();
writeLog();
}
);
const cancel = () => {
visible.value = false;
logId.value = 0;
nowLine.value = 0;
pause();
};
const writeLog = async () => {
const log = await getLog();
if (!log) {
return;
}
writeLog2Term(log);
// 如果不是还在执行中的日志,则暂停轮询
if (log.type != LogTypeEnum.Running.value) {
pause();
return;
}
resume();
};
const writeLog2Term = (log: any) => {
if (!log) {
return;
}
const lines = log.resp.split('\n');
for (let line of lines.slice(nowLine.value)) {
nowLine.value += 1;
terminalRef.value.writeln2Term(line);
}
terminalRef.value.focus();
};
const getLog = async () => {
if (!logId.value) {
return;
}
const logRes = await logApi.detail.request({
id: logId.value,
});
log.value = logRes;
return logRes;
};
</script>
<style lang="scss"></style>

View File

@@ -1,7 +1,7 @@
<template>
<div class="tag-tree card pd5">
<el-scrollbar>
<el-input v-model="filterText" placeholder="输入关键字->搜索已展开节点信息" clearable size="small" class="mb5 w100" />
<div class="card pd5">
<el-input v-model="filterText" placeholder="输入关键字->搜索已展开节点信息" clearable size="small" class="mb5 w100" />
<el-scrollbar class="tag-tree">
<el-tree
ref="treeRef"
:highlight-current="true"
@@ -206,7 +206,7 @@ defineExpose({
<style lang="scss" scoped>
.tag-tree {
height: calc(100vh - 108px);
height: calc(100vh - 148px);
.el-tree {
display: inline-block;

View File

@@ -34,15 +34,17 @@
<template #action="{ data }">
<!-- 删除启停用编辑 -->
<el-button v-if="actionBtns[perms.save]" @click="edit(data)" type="primary" link>编辑</el-button>
<el-button v-if="actionBtns[perms.log]" type="primary" link @click="log(data)">日志</el-button>
<el-button :disabled="state.runBtnDisabled" v-if="actionBtns[perms.log]" type="primary" link @click="log(data)">日志</el-button>
<el-button v-if="data.runningState === 1" @click="stop(data.id)" type="danger" link>停止</el-button>
<el-button v-if="actionBtns[perms.run] && data.runningState !== 1" type="primary" link @click="reRun(data)">运行</el-button>
<el-button :disabled="state.runBtnDisabled" v-if="actionBtns[perms.run] && data.runningState !== 1" type="primary" link @click="reRun(data)"
>运行</el-button
>
</template>
</page-table>
<db-transfer-edit @val-change="search" :title="editDialog.title" v-model:visible="editDialog.visible" v-model:data="editDialog.data" />
<db-transfer-log v-model:visible="logsDialog.visible" v-model:taskId="logsDialog.taskId" :running="state.logsDialog.running" />
<TerminalLog v-model:log-id="logsDialog.logId" v-model:visible="logsDialog.visible" :title="logsDialog.title" />
</div>
</template>
@@ -55,9 +57,10 @@ import { TableColumn } from '@/components/pagetable';
import { hasPerms } from '@/components/auth/auth';
import { SearchItem } from '@/components/SearchForm';
import { getDbDialect } from '@/views/ops/db/dialect';
import { DbTransferRunningStateEnum } from './enums';
import TerminalLog from '@/components/terminal/TerminalLog.vue';
const DbTransferEdit = defineAsyncComponent(() => import('./DbTransferEdit.vue'));
const DbTransferLog = defineAsyncComponent(() => import('./DbTransferLog.vue'));
const perms = {
save: 'db:transfer:save',
@@ -72,8 +75,11 @@ const searchItems = [SearchItem.input('name', '名称')];
const columns = ref([
TableColumn.new('srcDb', '源库').setMinWidth(250).isSlot(),
TableColumn.new('targetDb', '目标库').setMinWidth(250).isSlot(),
TableColumn.new('modifier', '修改人').alignCenter(),
TableColumn.new('updateTime', '修改时间').alignCenter().isTime(),
TableColumn.new('runningState', '执行状态').typeTag(DbTransferRunningStateEnum),
TableColumn.new('creator', '创建人'),
TableColumn.new('createTime', '创建时间').isTime(),
TableColumn.new('modifier', '修改人'),
TableColumn.new('updateTime', '修改时间').isTime(),
]);
// 该用户拥有的的操作列按钮权限
@@ -104,11 +110,13 @@ const state = reactive({
title: '新增数据数据迁移任务',
},
logsDialog: {
taskId: 0,
logId: 0,
title: '数据库迁移日志',
visible: false,
data: null as any,
running: false,
},
runBtnDisabled: false,
});
const { selectionData, query, editDialog, logsDialog } = toRefs(state);
@@ -146,8 +154,9 @@ const stop = async (id: any) => {
};
const log = async (data: any) => {
state.logsDialog.taskId = data.id;
state.logsDialog.logId = data.logId;
state.logsDialog.visible = true;
state.logsDialog.title = '数据库迁移日志';
state.logsDialog.running = data.state === 1;
};
@@ -157,9 +166,18 @@ const reRun = async (data: any) => {
cancelButtonText: '取消',
type: 'warning',
});
await dbApi.runDbTransferTask.request({ taskId: data.id });
ElMessage.success('运行成功');
search();
try {
state.runBtnDisabled = true;
await dbApi.runDbTransferTask.request({ taskId: data.id });
ElMessage.success('运行成功');
} catch (e) {
state.runBtnDisabled = false;
}
// 延迟2秒执行后端异步执行
setTimeout(() => {
search();
state.runBtnDisabled = false;
}, 2000);
};
const del = async () => {

View File

@@ -1,117 +0,0 @@
<template>
<div class="sync-task-logs">
<el-dialog v-model="dialogVisible" :before-close="cancel" :destroy-on-close="false" width="1120px">
<template #header>
<span class="mr10">任务执行日志</span>
<el-switch v-model="realTime" @change="watchPolling" inline-prompt active-text="实时" inactive-text="非实时" />
<el-button @click="search" icon="Refresh" circle size="small" :loading="realTime" class="ml10"></el-button>
</template>
<page-table
ref="logTableRef"
:page-api="dbApi.dbTransferTaskLogs"
v-model:query-form="query"
:tool-button="false"
:columns="columns"
size="small"
/>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { reactive, Ref, ref, toRefs, watch } from 'vue';
import { dbApi } from '@/views/ops/db/api';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { DbDataSyncLogStatusEnum } from './enums';
const props = defineProps({
taskId: {
type: Number,
},
running: {
type: Boolean,
default: false,
},
});
const dialogVisible = defineModel<boolean>('visible', { default: false });
const columns = ref([
// 状态:1.成功 -1.失败
TableColumn.new('status', '状态').alignCenter().typeTag(DbDataSyncLogStatusEnum),
TableColumn.new('createTime', '时间').alignCenter().isTime(),
TableColumn.new('errText', '日志'),
TableColumn.new('dataSqlFull', 'SQL').alignCenter(),
TableColumn.new('resNum', '数据条数'),
]);
watch(dialogVisible, (newValue: any) => {
if (!newValue) {
state.polling = false;
watchPolling(false);
return;
}
state.query.taskId = props.taskId!;
search();
state.realTime = props.running;
watchPolling(props.running);
});
const startPolling = () => {
if (!state.polling) {
state.polling = true;
state.pollingIndex = setInterval(search, 1000);
}
};
const stopPolling = () => {
if (state.polling) {
state.polling = false;
clearInterval(state.pollingIndex);
}
};
const watchPolling = (polling: boolean) => {
if (polling) {
startPolling();
} else {
stopPolling();
}
};
const logTableRef: Ref<any> = ref(null);
const search = () => {
try {
logTableRef.value.search();
} catch (e) {
/* empty */
}
};
const emit = defineEmits(['update:visible', 'cancel', 'val-change']);
//定义事件
const cancel = () => {
dialogVisible.value = false;
emit('cancel');
watchPolling(false);
};
const state = reactive({
polling: false,
pollingIndex: 0 as any,
realTime: props.running,
/**
* 查询条件
*/
query: {
taskId: 0,
name: null,
pageNum: 1,
pageSize: 0,
},
});
const { query, realTime } = toRefs(state);
</script>

View File

@@ -71,11 +71,13 @@ const searchItems = [SearchItem.input('name', '名称')];
// 任务名、修改人、修改时间、最近一次任务执行状态、状态(停用启用)、操作
const columns = ref([
TableColumn.new('taskName', '任务名'),
TableColumn.new('runningState', '运行状态').alignCenter().typeTag(DbDataSyncRunningStateEnum),
TableColumn.new('recentState', '最近任务状态').alignCenter().typeTag(DbDataSyncRecentStateEnum),
TableColumn.new('status', '状态').alignCenter().isSlot(),
TableColumn.new('modifier', '修改人').alignCenter(),
TableColumn.new('updateTime', '修改时间').alignCenter().isTime(),
TableColumn.new('runningState', '运行状态').typeTag(DbDataSyncRunningStateEnum),
TableColumn.new('recentState', '最近任务状态').typeTag(DbDataSyncRecentStateEnum),
TableColumn.new('status', '状态').isSlot(),
TableColumn.new('creator', '创建人'),
TableColumn.new('createTime', '创建时间').isTime(),
TableColumn.new('modifier', '修改人'),
TableColumn.new('updateTime', '修改时间').isTime(),
]);
// 该用户拥有的的操作列按钮权限

View File

@@ -30,3 +30,10 @@ export const DbDataSyncRunningStateEnum = {
Wait: EnumValue.of(2, '待运行').setTagType('primary'),
Fail: EnumValue.of(3, '已停止').setTagType('danger'),
};
export const DbTransferRunningStateEnum = {
Success: EnumValue.of(2, '成功').setTagType('success'),
Wait: EnumValue.of(1, '执行中').setTagType('primary'),
Fail: EnumValue.of(-1, '失败').setTagType('danger'),
Stop: EnumValue.of(-2, '手动终止').setTagType('warning'),
};

View File

@@ -44,6 +44,7 @@ export const configApi = {
export const logApi = {
list: Api.newGet('/syslogs'),
detail: Api.newGet('/syslogs/{id}'),
};
export const authApi = {

View File

@@ -18,4 +18,5 @@ export const RoleStatusEnum = {
export const LogTypeEnum = {
Success: EnumValue.of(1, '成功').tagTypeSuccess(),
Error: EnumValue.of(2, '失败').tagTypeDanger(),
Running: EnumValue.of(-1, '执行中'),
};

View File

@@ -43,7 +43,7 @@ const columns = [
TableColumn.new('type', '结果').typeTag(LogTypeEnum),
TableColumn.new('description', '描述'),
TableColumn.new('reqParam', '操作信息').canBeautify(),
TableColumn.new('resp', '响应信息'),
TableColumn.new('resp', '响应信息').canBeautify(),
];
const state = reactive({