|
|
@ -9,7 +9,10 @@ import { |
|
|
triggerForceSync, |
|
|
triggerForceSync, |
|
|
getTaskList, |
|
|
getTaskList, |
|
|
getLogList, |
|
|
getLogList, |
|
|
getJobStatus |
|
|
getJobStatus, |
|
|
|
|
|
getRecordList, |
|
|
|
|
|
getRecordDetail, |
|
|
|
|
|
retryRecord |
|
|
} from '@/api/efcode/biz'; |
|
|
} from '@/api/efcode/biz'; |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
@ -118,6 +121,105 @@ const syncConfigs = ref([ |
|
|
|
|
|
|
|
|
const loading = ref(false); |
|
|
const loading = ref(false); |
|
|
|
|
|
|
|
|
|
|
|
// ============ Tab 切换 ============ |
|
|
|
|
|
const activeTab = ref('config'); |
|
|
|
|
|
|
|
|
|
|
|
/** Tab 切换:首次进入影子记录页时自动加载 */ |
|
|
|
|
|
const handleTabChange = (name: string | number) => { |
|
|
|
|
|
if (name === 'record' && recordList.value.length === 0) { |
|
|
|
|
|
loadRecordList(); |
|
|
|
|
|
} |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
// ============ 影子记录 Tab ============ |
|
|
|
|
|
const recordLoading = ref(false); |
|
|
|
|
|
const recordList = ref<any[]>([]); |
|
|
|
|
|
const recordTotal = ref(0); |
|
|
|
|
|
const recordQuery = reactive({ |
|
|
|
|
|
configKey: '', |
|
|
|
|
|
status: '', |
|
|
|
|
|
pageNum: 1, |
|
|
|
|
|
pageSize: 20 |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
/** 加载影子记录列表(后端按 create_time 倒序) */ |
|
|
|
|
|
const loadRecordList = async () => { |
|
|
|
|
|
recordLoading.value = true; |
|
|
|
|
|
try { |
|
|
|
|
|
const res: any = await getRecordList(recordQuery); |
|
|
|
|
|
const data = res.data; |
|
|
|
|
|
recordList.value = data?.records || []; |
|
|
|
|
|
recordTotal.value = data?.total || 0; |
|
|
|
|
|
} catch (e) { |
|
|
|
|
|
console.error('加载影子记录失败', e); |
|
|
|
|
|
} finally { |
|
|
|
|
|
recordLoading.value = false; |
|
|
|
|
|
} |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
/** 影子记录查询(重置到第一页) */ |
|
|
|
|
|
const handleRecordSearch = () => { |
|
|
|
|
|
recordQuery.pageNum = 1; |
|
|
|
|
|
loadRecordList(); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
/** 重试死信记录 */ |
|
|
|
|
|
const handleRetryRecord = async (row: any) => { |
|
|
|
|
|
try { |
|
|
|
|
|
await ElMessageBox.confirm( |
|
|
|
|
|
`确认重试该死信记录(主键: ${row.pkValue})?`, |
|
|
|
|
|
'确认', |
|
|
|
|
|
{ type: 'warning' } |
|
|
|
|
|
); |
|
|
|
|
|
const res: any = await retryRecord(row.id); |
|
|
|
|
|
ElMessage.success(res?.msg || '重试成功'); |
|
|
|
|
|
loadRecordList(); |
|
|
|
|
|
} catch (e: any) { |
|
|
|
|
|
if (e !== 'cancel' && e?.action !== 'cancel') { |
|
|
|
|
|
ElMessage.error(e?.msg || '重试失败'); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
/** 影子记录状态标签类型 */ |
|
|
|
|
|
const recordStatusType = (status: string) => { |
|
|
|
|
|
switch (status) { |
|
|
|
|
|
case 'NORMAL': return 'success'; |
|
|
|
|
|
case 'DEAD': return 'danger'; |
|
|
|
|
|
default: return 'info'; |
|
|
|
|
|
} |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
// ============ 同步记录详情弹窗 ============ |
|
|
|
|
|
const recordDetailVisible = ref(false); |
|
|
|
|
|
const recordDetailLoading = ref(false); |
|
|
|
|
|
const recordDetail = ref<any>(null); |
|
|
|
|
|
|
|
|
|
|
|
/** 格式化 JSON 字符串(美化缩进) */ |
|
|
|
|
|
const prettyJson = (jsonStr: string) => { |
|
|
|
|
|
if (!jsonStr) return '(空)'; |
|
|
|
|
|
try { |
|
|
|
|
|
return JSON.stringify(JSON.parse(jsonStr), null, 2); |
|
|
|
|
|
} catch (e) { |
|
|
|
|
|
return jsonStr; |
|
|
|
|
|
} |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
/** 查看同步记录详情(含快照JSON,按需加载) */ |
|
|
|
|
|
const handleViewRecordDetail = async (row: any) => { |
|
|
|
|
|
recordDetailVisible.value = true; |
|
|
|
|
|
recordDetailLoading.value = true; |
|
|
|
|
|
recordDetail.value = null; |
|
|
|
|
|
try { |
|
|
|
|
|
const res: any = await getRecordDetail(row.id); |
|
|
|
|
|
recordDetail.value = res?.data || null; |
|
|
|
|
|
} catch (e: any) { |
|
|
|
|
|
ElMessage.error(e?.msg || '加载详情失败'); |
|
|
|
|
|
} finally { |
|
|
|
|
|
recordDetailLoading.value = false; |
|
|
|
|
|
} |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
// ============ 异步任务轮询状态 ============ |
|
|
// ============ 异步任务轮询状态 ============ |
|
|
interface RunningJob { |
|
|
interface RunningJob { |
|
|
jobId: string; |
|
|
jobId: string; |
|
|
@ -452,6 +554,9 @@ onUnmounted(() => { |
|
|
</template> |
|
|
</template> |
|
|
</el-alert> |
|
|
</el-alert> |
|
|
|
|
|
|
|
|
|
|
|
<el-tabs v-model="activeTab" @tab-change="handleTabChange"> |
|
|
|
|
|
<!-- Tab1: 同步配置列表 --> |
|
|
|
|
|
<el-tab-pane label="同步配置" name="config"> |
|
|
<el-card shadow="never"> |
|
|
<el-card shadow="never"> |
|
|
<template #header> |
|
|
<template #header> |
|
|
<div class="card-header"> |
|
|
<div class="card-header"> |
|
|
@ -512,6 +617,112 @@ onUnmounted(() => { |
|
|
</el-table-column> |
|
|
</el-table-column> |
|
|
</el-table> |
|
|
</el-table> |
|
|
</el-card> |
|
|
</el-card> |
|
|
|
|
|
</el-tab-pane> |
|
|
|
|
|
|
|
|
|
|
|
<!-- Tab2: 同步记录列表(biz_sync_record,按创建时间倒序) --> |
|
|
|
|
|
<el-tab-pane label="同步记录" name="record"> |
|
|
|
|
|
<el-card shadow="never"> |
|
|
|
|
|
<template #header> |
|
|
|
|
|
<div class="card-header"> |
|
|
|
|
|
<div class="header-title"> |
|
|
|
|
|
<span>同步记录 (biz_sync_record)</span> |
|
|
|
|
|
<el-tag type="info" size="small" style="margin-left: 8px"> |
|
|
|
|
|
共 {{ recordTotal }} 条 |
|
|
|
|
|
</el-tag> |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
</template> |
|
|
|
|
|
|
|
|
|
|
|
<!-- 查询条件 --> |
|
|
|
|
|
<el-form inline style="margin-bottom: 8px"> |
|
|
|
|
|
<el-form-item label="配置Key"> |
|
|
|
|
|
<el-select |
|
|
|
|
|
v-model="recordQuery.configKey" |
|
|
|
|
|
placeholder="全部" |
|
|
|
|
|
clearable |
|
|
|
|
|
style="width: 280px" |
|
|
|
|
|
@change="handleRecordSearch" |
|
|
|
|
|
> |
|
|
|
|
|
<el-option |
|
|
|
|
|
v-for="cfg in syncConfigs" |
|
|
|
|
|
:key="cfg.configKey" |
|
|
|
|
|
:label="cfg.configKey" |
|
|
|
|
|
:value="cfg.configKey" |
|
|
|
|
|
/> |
|
|
|
|
|
</el-select> |
|
|
|
|
|
</el-form-item> |
|
|
|
|
|
<el-form-item label="状态"> |
|
|
|
|
|
<el-select |
|
|
|
|
|
v-model="recordQuery.status" |
|
|
|
|
|
placeholder="全部" |
|
|
|
|
|
clearable |
|
|
|
|
|
style="width: 140px" |
|
|
|
|
|
@change="handleRecordSearch" |
|
|
|
|
|
> |
|
|
|
|
|
<el-option label="NORMAL" value="NORMAL" /> |
|
|
|
|
|
<el-option label="DEAD" value="DEAD" /> |
|
|
|
|
|
</el-select> |
|
|
|
|
|
</el-form-item> |
|
|
|
|
|
<el-form-item> |
|
|
|
|
|
<el-button type="primary" :loading="recordLoading" @click="handleRecordSearch">查询</el-button> |
|
|
|
|
|
<el-button :loading="recordLoading" @click="loadRecordList">刷新</el-button> |
|
|
|
|
|
</el-form-item> |
|
|
|
|
|
</el-form> |
|
|
|
|
|
|
|
|
|
|
|
<el-table v-loading="recordLoading" :data="recordList" border stripe size="small" style="width: 100%"> |
|
|
|
|
|
<el-table-column label="配置Key" prop="configKey" width="230" show-overflow-tooltip /> |
|
|
|
|
|
<el-table-column label="主键值" prop="pkValue" width="220" show-overflow-tooltip /> |
|
|
|
|
|
<el-table-column label="归属方" prop="ownerSide" width="80" align="center" /> |
|
|
|
|
|
<el-table-column label="最近动作" width="160" align="center"> |
|
|
|
|
|
<template #default="{ row }"> |
|
|
|
|
|
<el-tag :type="actionTagType(row.lastAction)" size="small">{{ row.lastAction || '-' }}</el-tag> |
|
|
|
|
|
</template> |
|
|
|
|
|
</el-table-column> |
|
|
|
|
|
<el-table-column label="状态" width="90" align="center"> |
|
|
|
|
|
<template #default="{ row }"> |
|
|
|
|
|
<el-tag :type="recordStatusType(row.status)" size="small">{{ row.status }}</el-tag> |
|
|
|
|
|
</template> |
|
|
|
|
|
</el-table-column> |
|
|
|
|
|
<el-table-column label="重试次数" prop="retryCount" width="90" align="center" /> |
|
|
|
|
|
<el-table-column label="最近同步时间" width="160"> |
|
|
|
|
|
<template #default="{ row }">{{ formatTime(row.lastSyncTime) }}</template> |
|
|
|
|
|
</el-table-column> |
|
|
|
|
|
<el-table-column label="创建时间" width="160"> |
|
|
|
|
|
<template #default="{ row }">{{ formatTime(row.createTime) }}</template> |
|
|
|
|
|
</el-table-column> |
|
|
|
|
|
<el-table-column label="更新时间" width="160"> |
|
|
|
|
|
<template #default="{ row }">{{ formatTime(row.updateTime) }}</template> |
|
|
|
|
|
</el-table-column> |
|
|
|
|
|
<el-table-column label="操作" width="130" align="center" fixed="right"> |
|
|
|
|
|
<template #default="{ row }"> |
|
|
|
|
|
<el-button type="primary" link size="small" @click="handleViewRecordDetail(row)"> |
|
|
|
|
|
详情 |
|
|
|
|
|
</el-button> |
|
|
|
|
|
<el-button |
|
|
|
|
|
v-if="row.status === 'DEAD'" |
|
|
|
|
|
type="warning" |
|
|
|
|
|
link |
|
|
|
|
|
size="small" |
|
|
|
|
|
@click="handleRetryRecord(row)" |
|
|
|
|
|
> |
|
|
|
|
|
重试 |
|
|
|
|
|
</el-button> |
|
|
|
|
|
</template> |
|
|
|
|
|
</el-table-column> |
|
|
|
|
|
</el-table> |
|
|
|
|
|
<el-pagination |
|
|
|
|
|
v-model:current-page="recordQuery.pageNum" |
|
|
|
|
|
v-model:page-size="recordQuery.pageSize" |
|
|
|
|
|
:total="recordTotal" |
|
|
|
|
|
:page-sizes="[10, 20, 50, 100]" |
|
|
|
|
|
layout="total, sizes, prev, pager, next" |
|
|
|
|
|
class="mt-4" |
|
|
|
|
|
@size-change="loadRecordList" |
|
|
|
|
|
@current-change="loadRecordList" |
|
|
|
|
|
/> |
|
|
|
|
|
</el-card> |
|
|
|
|
|
</el-tab-pane> |
|
|
|
|
|
</el-tabs> |
|
|
|
|
|
|
|
|
<!-- 任务记录弹窗 --> |
|
|
<!-- 任务记录弹窗 --> |
|
|
<el-dialog |
|
|
<el-dialog |
|
|
@ -597,6 +808,56 @@ onUnmounted(() => { |
|
|
@current-change="loadLogList" |
|
|
@current-change="loadLogList" |
|
|
/> |
|
|
/> |
|
|
</el-dialog> |
|
|
</el-dialog> |
|
|
|
|
|
|
|
|
|
|
|
<!-- 同步记录详情弹窗(含快照JSON) --> |
|
|
|
|
|
<el-dialog |
|
|
|
|
|
v-model="recordDetailVisible" |
|
|
|
|
|
title="同步记录详情" |
|
|
|
|
|
width="1200px" |
|
|
|
|
|
append-to-body |
|
|
|
|
|
> |
|
|
|
|
|
<div v-loading="recordDetailLoading"> |
|
|
|
|
|
<template v-if="recordDetail"> |
|
|
|
|
|
<el-descriptions :column="3" border size="small" style="margin-bottom: 16px"> |
|
|
|
|
|
<el-descriptions-item label="配置Key">{{ recordDetail.configKey }}</el-descriptions-item> |
|
|
|
|
|
<el-descriptions-item label="主键值">{{ recordDetail.pkValue }}</el-descriptions-item> |
|
|
|
|
|
<el-descriptions-item label="归属方">{{ recordDetail.ownerSide }}</el-descriptions-item> |
|
|
|
|
|
<el-descriptions-item label="最近动作"> |
|
|
|
|
|
<el-tag :type="actionTagType(recordDetail.lastAction)" size="small"> |
|
|
|
|
|
{{ recordDetail.lastAction || '-' }} |
|
|
|
|
|
</el-tag> |
|
|
|
|
|
</el-descriptions-item> |
|
|
|
|
|
<el-descriptions-item label="状态"> |
|
|
|
|
|
<el-tag :type="recordStatusType(recordDetail.status)" size="small"> |
|
|
|
|
|
{{ recordDetail.status }} |
|
|
|
|
|
</el-tag> |
|
|
|
|
|
</el-descriptions-item> |
|
|
|
|
|
<el-descriptions-item label="重试次数">{{ recordDetail.retryCount }}</el-descriptions-item> |
|
|
|
|
|
<el-descriptions-item label="最近同步时间">{{ formatTime(recordDetail.lastSyncTime) }}</el-descriptions-item> |
|
|
|
|
|
<el-descriptions-item label="创建时间">{{ formatTime(recordDetail.createTime) }}</el-descriptions-item> |
|
|
|
|
|
<el-descriptions-item label="更新时间">{{ formatTime(recordDetail.updateTime) }}</el-descriptions-item> |
|
|
|
|
|
<el-descriptions-item label="V1内容哈希" :span="3"> |
|
|
|
|
|
<span class="hash-text">{{ recordDetail.contentHashV1 || '-' }}</span> |
|
|
|
|
|
</el-descriptions-item> |
|
|
|
|
|
<el-descriptions-item label="V2内容哈希" :span="3"> |
|
|
|
|
|
<span class="hash-text">{{ recordDetail.contentHashV2 || '-' }}</span> |
|
|
|
|
|
</el-descriptions-item> |
|
|
|
|
|
</el-descriptions> |
|
|
|
|
|
|
|
|
|
|
|
<el-row :gutter="12"> |
|
|
|
|
|
<el-col :span="12"> |
|
|
|
|
|
<div class="json-panel-title">V1 快照 (snapshot_v1_json)</div> |
|
|
|
|
|
<pre class="json-panel">{{ prettyJson(recordDetail.snapshotV1Json) }}</pre> |
|
|
|
|
|
</el-col> |
|
|
|
|
|
<el-col :span="12"> |
|
|
|
|
|
<div class="json-panel-title">V2 快照 (snapshot_v2_json)</div> |
|
|
|
|
|
<pre class="json-panel">{{ prettyJson(recordDetail.snapshotV2Json) }}</pre> |
|
|
|
|
|
</el-col> |
|
|
|
|
|
</el-row> |
|
|
|
|
|
</template> |
|
|
|
|
|
<el-empty v-else-if="!recordDetailLoading" description="未加载到详情数据" /> |
|
|
|
|
|
</div> |
|
|
|
|
|
</el-dialog> |
|
|
</div> |
|
|
</div> |
|
|
</template> |
|
|
</template> |
|
|
|
|
|
|
|
|
@ -617,6 +878,29 @@ onUnmounted(() => { |
|
|
.mt-4 { |
|
|
.mt-4 { |
|
|
margin-top: 16px; |
|
|
margin-top: 16px; |
|
|
} |
|
|
} |
|
|
|
|
|
.hash-text { |
|
|
|
|
|
font-family: Consolas, Monaco, monospace; |
|
|
|
|
|
font-size: 12px; |
|
|
|
|
|
word-break: break-all; |
|
|
|
|
|
} |
|
|
|
|
|
.json-panel-title { |
|
|
|
|
|
font-weight: bold; |
|
|
|
|
|
margin-bottom: 6px; |
|
|
|
|
|
} |
|
|
|
|
|
.json-panel { |
|
|
|
|
|
height: 420px; |
|
|
|
|
|
overflow: auto; |
|
|
|
|
|
margin: 0; |
|
|
|
|
|
padding: 10px; |
|
|
|
|
|
background: #f5f7fa; |
|
|
|
|
|
border: 1px solid #e4e7ed; |
|
|
|
|
|
border-radius: 4px; |
|
|
|
|
|
font-family: Consolas, Monaco, monospace; |
|
|
|
|
|
font-size: 12px; |
|
|
|
|
|
line-height: 1.6; |
|
|
|
|
|
white-space: pre-wrap; |
|
|
|
|
|
word-break: break-all; |
|
|
|
|
|
} |
|
|
</style> |
|
|
</style> |
|
|
|
|
|
|
|
|
<!-- 全局样式(非 scoped):ElLoading 挂在 body 上,scoped 样式无法命中 --> |
|
|
<!-- 全局样式(非 scoped):ElLoading 挂在 body 上,scoped 样式无法命中 --> |
|
|
|