mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-03 16:00:25 +08:00
feat: 数据迁移新增实时日志&数据库游标遍历查询问题修复
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
102
mayfly_go_web/src/components/terminal/TerminalLog.vue
Normal file
102
mayfly_go_web/src/components/terminal/TerminalLog.vue
Normal 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>
|
||||
@@ -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;
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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>
|
||||
@@ -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(),
|
||||
]);
|
||||
|
||||
// 该用户拥有的的操作列按钮权限
|
||||
|
||||
@@ -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'),
|
||||
};
|
||||
|
||||
@@ -44,6 +44,7 @@ export const configApi = {
|
||||
|
||||
export const logApi = {
|
||||
list: Api.newGet('/syslogs'),
|
||||
detail: Api.newGet('/syslogs/{id}'),
|
||||
};
|
||||
|
||||
export const authApi = {
|
||||
|
||||
@@ -18,4 +18,5 @@ export const RoleStatusEnum = {
|
||||
export const LogTypeEnum = {
|
||||
Success: EnumValue.of(1, '成功').tagTypeSuccess(),
|
||||
Error: EnumValue.of(2, '失败').tagTypeDanger(),
|
||||
Running: EnumValue.of(-1, '执行中'),
|
||||
};
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user