fix: improve VM migration state and chain details #34
@@ -2048,15 +2048,17 @@ const loadDetail = async () => {
|
||||
const handleDetailMigrateState = () => {
|
||||
const vm = detail.value
|
||||
if (!vm) return
|
||||
if (vm.migrating && vm.migrate_task_id) {
|
||||
dataMigrateTaskId.value = vm.migrate_task_id
|
||||
if (!dataMigrationId.value) dataMigrationId.value = vm.migrate_task_id
|
||||
if (!migratePollingTimer) {
|
||||
loadDataMigrateProgress()
|
||||
startMigratePolling()
|
||||
if (vm.migrating === true) {
|
||||
if (vm.migrate_task_id) {
|
||||
dataMigrateTaskId.value = vm.migrate_task_id
|
||||
if (!dataMigrationId.value) dataMigrationId.value = vm.migrate_task_id
|
||||
if (!migratePollingTimer) {
|
||||
loadDataMigrateProgress()
|
||||
startMigratePolling()
|
||||
}
|
||||
}
|
||||
startDetailAutoRefresh()
|
||||
} else if (!vm.migrating) {
|
||||
} else {
|
||||
stopDetailAutoRefresh()
|
||||
if (migratePollingTimer) {
|
||||
stopMigratePolling()
|
||||
@@ -2905,11 +2907,7 @@ const loadDataMigrateProgress = async () => {
|
||||
|
||||
const currentMigrateStage = computed(() => dataMigrateProgressData.value?.status || dataMigrateProgressData.value?.stage || '')
|
||||
|
||||
const isMigrating = computed(() => {
|
||||
if (detail.value?.migrating) return true
|
||||
const stage = currentMigrateStage.value
|
||||
return stage && !MIGRATE_DONE_STAGES.includes(stage)
|
||||
})
|
||||
const isMigrating = computed(() => detail.value?.migrating === true)
|
||||
|
||||
const migrateStageLabel = (stage) => ({
|
||||
exporting: '导出中', importing: '导入中', transferring: '传输中',
|
||||
|
||||
@@ -91,7 +91,27 @@
|
||||
<div class="chain-id">链路 ID:<span class="mono">{{ chainData.chain_id || '-' }}</span></div>
|
||||
<el-timeline v-if="chainData.records?.length">
|
||||
<el-timeline-item v-for="item in chainData.records" :key="item.id" :type="statusType(item.status)" :timestamp="formatTime(item.started_at || item.CreatedAt)" placement="top">
|
||||
<el-card shadow="never"><div class="chain-card-title"><span>#{{ item.id }} · 第 {{ item.chain_seq || 1 }} 步 · {{ typeLabel(item.migration_type) }}</span><el-tag :type="statusType(item.status)" size="small">{{ statusLabel(item.status) }}</el-tag></div><div class="chain-route">服务 {{ item.source_service_id || '-' }} / VM {{ item.source_vm_id || '-' }} → 服务 {{ item.target_service_id || '-' }} / VM {{ item.target_vm_id || '-' }}</div><el-button link type="primary" @click="chainVisible=false; openDetail(item)">查看详情</el-button></el-card>
|
||||
<el-card shadow="never">
|
||||
<div class="chain-card-title"><span>#{{ item.id }} · 第 {{ item.chain_seq || 1 }} 步 · {{ typeLabel(item.migration_type) }}</span><el-tag :type="statusType(item.status)" size="small">{{ statusLabel(item.status) }}</el-tag></div>
|
||||
<div class="chain-route">
|
||||
<div class="chain-endpoint">
|
||||
<span class="chain-endpoint-label">源虚拟机</span>
|
||||
<el-link v-if="item.source_vm_id" type="primary" @click="goVm(item.source_service_id, item.source_vm_id)">{{ chainVmName(item, 'source') || `VM #${item.source_vm_id}` }}</el-link>
|
||||
<strong v-else>{{ chainVmName(item, 'source') || '-' }}</strong>
|
||||
<span class="muted">服务 #{{ item.source_service_id || '-' }} · VM #{{ item.source_vm_id || '-' }}</span>
|
||||
<span class="chain-ip">IP:{{ chainVmIps(item, 'source') || '-' }}</span>
|
||||
</div>
|
||||
<span class="chain-arrow">→</span>
|
||||
<div class="chain-endpoint">
|
||||
<span class="chain-endpoint-label">目标虚拟机</span>
|
||||
<el-link v-if="item.target_vm_id" type="primary" @click="goVm(item.target_service_id, item.target_vm_id)">{{ chainVmName(item, 'target') || `VM #${item.target_vm_id}` }}</el-link>
|
||||
<strong v-else>{{ chainVmName(item, 'target') || '-' }}</strong>
|
||||
<span class="muted">服务 #{{ item.target_service_id || '-' }} · VM #{{ item.target_vm_id || '-' }}</span>
|
||||
<span class="chain-ip">IP:{{ chainVmIps(item, 'target') || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-button link type="primary" @click="chainVisible=false; openDetail(item)">查看详情</el-button>
|
||||
</el-card>
|
||||
</el-timeline-item>
|
||||
</el-timeline><el-empty v-else description="暂无迁移链记录" />
|
||||
</div>
|
||||
@@ -138,6 +158,20 @@ const resetFilters = () => { Object.assign(filters,{ keyword:'',migration_type:'
|
||||
const detailVisible=ref(false), detailLoading=ref(false), currentRecord=ref(null)
|
||||
const snapshotFields=[{key:'request_snapshot',label:'请求参数'},{key:'before_snapshot',label:'迁移前快照'},{key:'after_snapshot',label:'迁移后快照'},{key:'manifest_snapshot',label:'迁移清单'},{key:'warning_snapshot',label:'警告'}]
|
||||
const prettySnapshot = value => { if(!value) return '暂无数据'; try { return JSON.stringify(typeof value==='string'?JSON.parse(value):value,null,2) } catch { return String(value) } }
|
||||
const parseSnapshot = value => { if (!value) return {}; if (typeof value === 'object') return value; try { return JSON.parse(value) } catch { return {} } }
|
||||
const chainVmSnapshot = (record, side) => parseSnapshot(side === 'source' ? record.before_snapshot : record.after_snapshot)
|
||||
const chainVmName = (record, side) => {
|
||||
const snapshot = chainVmSnapshot(record, side)
|
||||
return record[`${side}_vm_name`] || snapshot.vm_detail?.data?.name || snapshot.vm_summary?.vm_name || ''
|
||||
}
|
||||
const chainVmIps = (record, side) => {
|
||||
const snapshot = chainVmSnapshot(record, side)
|
||||
const directIps = snapshot.vm_detail?.data?.ips
|
||||
const values = directIps
|
||||
? String(directIps).split(',')
|
||||
: [...(snapshot.ip_addresses?.ipv4 || []), ...(snapshot.ip_addresses?.ipv6 || [])]
|
||||
return [...new Set(values.map(ip => String(ip).trim()).filter(Boolean))].join(', ')
|
||||
}
|
||||
const openDetail = async row => { detailVisible.value=true; currentRecord.value=row; detailLoading.value=true; try { const res=await getVmMigrationRecordDetail({id:row.id}); if(res?.data?.code!==200) throw new Error(extractApiError(res?.data,'加载详情失败')); currentRecord.value=res.data.data?.record||res.data.data||row } catch(e){ ElMessage.error(e.message||extractApiError(e?.response?.data,'加载详情失败')) } finally { detailLoading.value=false } }
|
||||
const chainVisible=ref(false), chainLoading=ref(false), chainData=reactive({chain_id:'',records:[]})
|
||||
const openChain = async row => { chainVisible.value=true; chainLoading.value=true; Object.assign(chainData,{chain_id:row.chain_id||'',records:[]}); try { const params=row.chain_id?{chain_id:row.chain_id}:{id:row.id}; const res=await getVmMigrationRecordChain(params); if(res?.data?.code!==200) throw new Error(extractApiError(res?.data,'加载迁移链失败')); Object.assign(chainData,res.data.data||{}) } catch(e){ ElMessage.error(e.message||extractApiError(e?.response?.data,'加载迁移链失败')) } finally { chainLoading.value=false } }
|
||||
@@ -154,5 +188,5 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.migration-record-page{padding:20px}.page-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px}.page-header h2{font-size:20px;margin:0 0 6px}.page-header p{margin:0;color:#86909c;font-size:13px}.filter-card{margin-bottom:16px}.filter-card :deep(.el-form-item){margin-bottom:8px}.selector-field{display:flex;gap:6px;width:300px}.selector-field .el-input{flex:1}.endpoint{display:flex;flex-direction:column;align-items:flex-start;gap:3px}.mono{font-family:Consolas,monospace;font-size:12px}.muted{font-size:12px;color:#86909c;margin-top:3px}.pagination{display:flex;justify-content:flex-end;margin-top:16px}.snapshot-tabs{margin-top:18px}.json-view{margin:0;padding:14px;max-height:460px;overflow:auto;background:#f7f8fa;border:1px solid #ebeef5;border-radius:6px;white-space:pre-wrap;word-break:break-all;font:12px/1.6 Consolas,monospace}.error-text{color:#f56c6c}.chain-id{margin-bottom:20px;color:#606266}.chain-card-title{display:flex;justify-content:space-between;align-items:center;font-weight:600}.chain-route{margin:10px 0;color:#606266;font-size:13px}
|
||||
.migration-record-page{padding:20px}.page-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px}.page-header h2{font-size:20px;margin:0 0 6px}.page-header p{margin:0;color:#86909c;font-size:13px}.filter-card{margin-bottom:16px}.filter-card :deep(.el-form-item){margin-bottom:8px}.selector-field{display:flex;gap:6px;width:300px}.selector-field .el-input{flex:1}.endpoint{display:flex;flex-direction:column;align-items:flex-start;gap:3px}.mono{font-family:Consolas,monospace;font-size:12px}.muted{font-size:12px;color:#86909c;margin-top:3px}.pagination{display:flex;justify-content:flex-end;margin-top:16px}.snapshot-tabs{margin-top:18px}.json-view{margin:0;padding:14px;max-height:460px;overflow:auto;background:#f7f8fa;border:1px solid #ebeef5;border-radius:6px;white-space:pre-wrap;word-break:break-all;font:12px/1.6 Consolas,monospace}.error-text{color:#f56c6c}.chain-id{margin-bottom:20px;color:#606266}.chain-card-title{display:flex;justify-content:space-between;align-items:center;font-weight:600}.chain-route{display:grid;grid-template-columns:minmax(0,1fr) auto minmax(0,1fr);align-items:center;gap:16px;margin:14px 0;color:#606266;font-size:13px}.chain-endpoint{display:flex;flex-direction:column;align-items:flex-start;gap:4px;min-width:0;padding:12px;background:#f7f8fa;border-radius:6px}.chain-endpoint-label{font-size:12px;color:#86909c}.chain-arrow{color:#409eff;font-size:20px;font-weight:600}.chain-ip{max-width:100%;font-family:Consolas,monospace;font-size:12px;word-break:break-all}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user