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

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