You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
365 lines
12 KiB
365 lines
12 KiB
<script setup lang="ts">
|
|
import { ref, reactive, onMounted } from 'vue';
|
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
|
import {
|
|
triggerSync,
|
|
triggerAllSync,
|
|
triggerForceSync,
|
|
getTaskList,
|
|
getLogList
|
|
} from '@/api/efcode/biz';
|
|
|
|
/**
|
|
* 同步配置项列表
|
|
* 对应后端 resources/businessdatasyn/*.json 中的每一个配置文件
|
|
* 后续新增同步表时,只需在此数组中追加一条即可
|
|
*/
|
|
const syncConfigs = ref([
|
|
{
|
|
configKey: 'enforcement_registration',
|
|
description: '执法登记记录表双向同步',
|
|
v1Table: 'enforcement_registration',
|
|
v2Table: 'enforcement_registration',
|
|
syncDirection: '双向',
|
|
enabled: true
|
|
},
|
|
{
|
|
configKey: 'enterprise_information',
|
|
description: '企业信息表双向同步',
|
|
v1Table: 'enterprise_information',
|
|
v2Table: 'enterprise_information',
|
|
syncDirection: '双向',
|
|
enabled: false
|
|
}
|
|
]);
|
|
|
|
const loading = ref(false);
|
|
|
|
// ============ 任务记录弹窗 ============
|
|
const taskDialogVisible = ref(false);
|
|
const taskLoading = ref(false);
|
|
const currentConfigKey = ref('');
|
|
const taskList = ref<any[]>([]);
|
|
const taskTotal = ref(0);
|
|
const taskQuery = reactive({
|
|
configKey: '',
|
|
pageNum: 1,
|
|
pageSize: 10
|
|
});
|
|
|
|
// ============ 日志弹窗 ============
|
|
const logDialogVisible = ref(false);
|
|
const logLoading = ref(false);
|
|
const currentTaskId = ref('');
|
|
const logList = ref<any[]>([]);
|
|
const logTotal = ref(0);
|
|
const logQuery = reactive({
|
|
taskId: '',
|
|
configKey: '',
|
|
pageNum: 1,
|
|
pageSize: 10
|
|
});
|
|
|
|
/** 触发单个表增量同步 */
|
|
const handleTrigger = async (row: any) => {
|
|
try {
|
|
await ElMessageBox.confirm(
|
|
`确认触发【${row.description}】增量同步?`,
|
|
'确认',
|
|
{ type: 'warning' }
|
|
);
|
|
loading.value = true;
|
|
const res: any = await triggerSync(row.configKey);
|
|
ElMessage.success(res.msg || '触发成功');
|
|
} catch (e: any) {
|
|
if (e !== 'cancel' && e?.action !== 'cancel') {
|
|
ElMessage.error(e?.msg || '触发失败');
|
|
}
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
/** 触发全部同步 */
|
|
const handleTriggerAll = async () => {
|
|
try {
|
|
await ElMessageBox.confirm('确认触发全部表的增量同步?', '确认', { type: 'warning' });
|
|
loading.value = true;
|
|
const res: any = await triggerAllSync();
|
|
ElMessage.success(res.msg || '全部触发成功');
|
|
} catch (e: any) {
|
|
if (e !== 'cancel' && e?.action !== 'cancel') {
|
|
ElMessage.error(e?.msg || '触发失败');
|
|
}
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
/** 强制全量同步 */
|
|
const handleForceSync = async () => {
|
|
try {
|
|
await ElMessageBox.confirm(
|
|
'强制全量同步将扫描全部数据,耗时较长,确认执行?',
|
|
'警告',
|
|
{ type: 'error', confirmButtonText: '确定执行', cancelButtonText: '取消' }
|
|
);
|
|
loading.value = true;
|
|
const res: any = await triggerForceSync();
|
|
ElMessage.success(res.msg || '强制全量触发成功');
|
|
} catch (e: any) {
|
|
if (e !== 'cancel' && e?.action !== 'cancel') {
|
|
ElMessage.error(e?.msg || '触发失败');
|
|
}
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
/** 查看任务记录 */
|
|
const handleViewTask = (row: any) => {
|
|
currentConfigKey.value = row.configKey;
|
|
taskQuery.configKey = row.configKey;
|
|
taskQuery.pageNum = 1;
|
|
taskDialogVisible.value = true;
|
|
loadTaskList();
|
|
};
|
|
|
|
/** 加载任务列表 */
|
|
const loadTaskList = async () => {
|
|
taskLoading.value = true;
|
|
try {
|
|
const res: any = await getTaskList(taskQuery);
|
|
const data = res.data;
|
|
taskList.value = data?.records || [];
|
|
taskTotal.value = data?.total || 0;
|
|
} catch (e) {
|
|
console.error('加载任务列表失败', e);
|
|
} finally {
|
|
taskLoading.value = false;
|
|
}
|
|
};
|
|
|
|
/** 查看某个任务的执行日志明细 */
|
|
const handleViewLog = (task: any) => {
|
|
currentTaskId.value = task.taskId;
|
|
logQuery.taskId = task.taskId;
|
|
logQuery.configKey = '';
|
|
logQuery.pageNum = 1;
|
|
logDialogVisible.value = true;
|
|
loadLogList();
|
|
};
|
|
|
|
/** 加载日志列表 */
|
|
const loadLogList = async () => {
|
|
logLoading.value = true;
|
|
try {
|
|
const res: any = await getLogList(logQuery);
|
|
const data = res.data;
|
|
logList.value = data?.records || [];
|
|
logTotal.value = data?.total || 0;
|
|
} catch (e) {
|
|
console.error('加载日志列表失败', e);
|
|
} finally {
|
|
logLoading.value = false;
|
|
}
|
|
};
|
|
|
|
/** 任务状态标签类型 */
|
|
const taskStatusType = (status: string) => {
|
|
switch (status) {
|
|
case 'SUCCESS': return 'success';
|
|
case 'RUNNING': return 'warning';
|
|
case 'PARTIAL_FAIL': return 'danger';
|
|
case 'FAIL': return 'danger';
|
|
case 'ABORTED': return 'info';
|
|
default: return 'info';
|
|
}
|
|
};
|
|
|
|
/** action 标签类型 */
|
|
const actionTagType = (action: string) => {
|
|
if (!action) return 'info';
|
|
if (action.startsWith('WRITE_')) return 'success';
|
|
if (action.startsWith('DELETE_')) return 'danger';
|
|
if (action.startsWith('CONFLICT_')) return 'warning';
|
|
return 'info';
|
|
};
|
|
|
|
/** 格式化时间 */
|
|
const formatTime = (val: string) => {
|
|
if (!val) return '-';
|
|
return val.replace('T', ' ').substring(0, 19);
|
|
};
|
|
|
|
onMounted(() => {});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="app-container">
|
|
<el-card shadow="never">
|
|
<template #header>
|
|
<div class="card-header">
|
|
<div class="header-title">
|
|
<span>业务数据同步管理</span>
|
|
<el-tag type="info" size="small" style="margin-left: 8px">
|
|
共 {{ syncConfigs.length }} 个配置
|
|
</el-tag>
|
|
</div>
|
|
<div class="header-actions">
|
|
<el-button type="primary" :loading="loading" @click="handleTriggerAll">
|
|
全部增量同步
|
|
</el-button>
|
|
<el-button type="danger" :loading="loading" @click="handleForceSync">
|
|
强制全量同步
|
|
</el-button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<el-alert type="info" :closable="false" style="margin-bottom: 16px">
|
|
<template #title>
|
|
<span>
|
|
单个同步配置说明:每条记录对应后端 resources/businessdatasyn/*.json 中的一个 JSON 配置文件,
|
|
实际的字段映射/主键/哈希列在 JSON 中定义。此页仅提供触发入口。
|
|
</span>
|
|
</template>
|
|
</el-alert>
|
|
|
|
<el-table :data="syncConfigs" border stripe style="width: 100%">
|
|
<el-table-column label="配置Key" prop="configKey" width="240" />
|
|
<el-table-column label="描述" prop="description" min-width="200" />
|
|
<el-table-column label="V1表名" prop="v1Table" width="200" />
|
|
<el-table-column label="V2表名" prop="v2Table" width="200" />
|
|
<el-table-column label="同步方向" prop="syncDirection" width="90" align="center" />
|
|
<el-table-column label="状态" width="80" align="center">
|
|
<template #default="{ row }">
|
|
<el-tag :type="row.enabled ? 'success' : 'info'" size="small">
|
|
{{ row.enabled ? '启用' : '禁用' }}
|
|
</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="操作" width="220" align="center" fixed="right">
|
|
<template #default="{ row }">
|
|
<el-button
|
|
type="primary"
|
|
link
|
|
size="small"
|
|
:disabled="!row.enabled"
|
|
@click="handleTrigger(row)"
|
|
>
|
|
增量同步
|
|
</el-button>
|
|
<el-button type="info" link size="small" @click="handleViewTask(row)">
|
|
任务记录
|
|
</el-button>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
</el-card>
|
|
|
|
<!-- 任务记录弹窗 -->
|
|
<el-dialog
|
|
v-model="taskDialogVisible"
|
|
:title="`同步任务记录 - ${currentConfigKey}`"
|
|
width="1000px"
|
|
append-to-body
|
|
>
|
|
<el-table v-loading="taskLoading" :data="taskList" border stripe size="small">
|
|
<el-table-column label="任务ID" prop="taskId" width="220" show-overflow-tooltip />
|
|
<el-table-column label="开始时间" width="160">
|
|
<template #default="{ row }">{{ formatTime(row.startTime) }}</template>
|
|
</el-table-column>
|
|
<el-table-column label="结束时间" width="160">
|
|
<template #default="{ row }">{{ formatTime(row.endTime) }}</template>
|
|
</el-table-column>
|
|
<el-table-column label="候选" prop="totalCandidates" width="70" align="center" />
|
|
<el-table-column label="成功" prop="successCount" width="70" align="center" />
|
|
<el-table-column label="冲突" prop="conflictCount" width="70" align="center" />
|
|
<el-table-column label="失败" prop="failCount" width="70" align="center" />
|
|
<el-table-column label="状态" width="110" align="center">
|
|
<template #default="{ row }">
|
|
<el-tag :type="taskStatusType(row.status)" size="small">{{ row.status }}</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="触发方式" prop="triggerType" width="90" align="center" />
|
|
<el-table-column label="操作" width="80" align="center" fixed="right">
|
|
<template #default="{ row }">
|
|
<el-button type="primary" link size="small" @click="handleViewLog(row)">
|
|
日志
|
|
</el-button>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
<el-pagination
|
|
v-model:current-page="taskQuery.pageNum"
|
|
v-model:page-size="taskQuery.pageSize"
|
|
:total="taskTotal"
|
|
:page-sizes="[10, 20, 50]"
|
|
layout="total, sizes, prev, pager, next"
|
|
class="mt-4"
|
|
@size-change="loadTaskList"
|
|
@current-change="loadTaskList"
|
|
/>
|
|
</el-dialog>
|
|
|
|
<!-- 日志明细弹窗 -->
|
|
<el-dialog
|
|
v-model="logDialogVisible"
|
|
:title="`同步日志明细 - ${currentTaskId}`"
|
|
width="1100px"
|
|
append-to-body
|
|
>
|
|
<el-table v-loading="logLoading" :data="logList" border stripe size="small">
|
|
<el-table-column label="主键值" prop="pkValue" width="200" show-overflow-tooltip />
|
|
<el-table-column label="动作" width="140" align="center">
|
|
<template #default="{ row }">
|
|
<el-tag :type="actionTagType(row.action)" size="small">{{ row.action }}</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="归属方" prop="ownerSide" width="80" align="center" />
|
|
<el-table-column label="成功" width="70" align="center">
|
|
<template #default="{ row }">
|
|
<el-tag :type="row.success ? 'success' : 'danger'" size="small">
|
|
{{ row.success ? '✓' : '✗' }}
|
|
</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="错误信息" prop="errorMsg" min-width="200" show-overflow-tooltip />
|
|
<el-table-column label="耗时(ms)" prop="costMs" width="90" align="center" />
|
|
<el-table-column label="时间" width="160">
|
|
<template #default="{ row }">{{ formatTime(row.createTime) }}</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
<el-pagination
|
|
v-model:current-page="logQuery.pageNum"
|
|
v-model:page-size="logQuery.pageSize"
|
|
:total="logTotal"
|
|
:page-sizes="[10, 20, 50]"
|
|
layout="total, sizes, prev, pager, next"
|
|
class="mt-4"
|
|
@size-change="loadLogList"
|
|
@current-change="loadLogList"
|
|
/>
|
|
</el-dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped lang="scss">
|
|
.app-container {
|
|
padding: 16px;
|
|
}
|
|
.card-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
.header-title {
|
|
display: flex;
|
|
align-items: center;
|
|
font-weight: bold;
|
|
}
|
|
}
|
|
.mt-4 {
|
|
margin-top: 16px;
|
|
}
|
|
</style>
|
|
|