fix: 数据迁移模块
Build and Deploy Vue3 / build (push) Successful in 1m30s
Build and Deploy Vue3 / deploy (push) Successful in 1m5s

This commit is contained in:
2026-04-15 16:18:15 +08:00
parent b3ed406f84
commit cf188bb94a
2 changed files with 70 additions and 7 deletions
+13 -2
View File
@@ -1611,7 +1611,9 @@ const handleDetailMigrateState = () => {
loadDataMigrateProgress() loadDataMigrateProgress()
startMigratePolling() startMigratePolling()
} }
startDetailAutoRefresh()
} else if (!vm.migrating) { } else if (!vm.migrating) {
stopDetailAutoRefresh()
if (migratePollingTimer) { if (migratePollingTimer) {
stopMigratePolling() stopMigratePolling()
dataMigrateProgressData.value = null dataMigrateProgressData.value = null
@@ -2287,12 +2289,21 @@ const migrateProgressBarStatus = (stage) => {
let migratePollingTimer = null let migratePollingTimer = null
const startMigratePolling = () => { const startMigratePolling = () => {
stopMigratePolling() stopMigratePolling()
migratePollingTimer = setInterval(loadDataMigrateProgress, 5000) migratePollingTimer = setInterval(loadDataMigrateProgress, 3000)
} }
const stopMigratePolling = () => { const stopMigratePolling = () => {
if (migratePollingTimer) { clearInterval(migratePollingTimer); migratePollingTimer = null } if (migratePollingTimer) { clearInterval(migratePollingTimer); migratePollingTimer = null }
} }
let detailAutoRefreshTimer = null
const startDetailAutoRefresh = () => {
if (detailAutoRefreshTimer) return
detailAutoRefreshTimer = setInterval(() => { loadDetail() }, 3000)
}
const stopDetailAutoRefresh = () => {
if (detailAutoRefreshTimer) { clearInterval(detailAutoRefreshTimer); detailAutoRefreshTimer = null }
}
const abortLoading = ref(false) const abortLoading = ref(false)
const handleAbortMigrate = () => { const handleAbortMigrate = () => {
ElMessageBox.confirm('确定要中断当前数据迁移吗?此操作不可恢复!', '中断迁移', { ElMessageBox.confirm('确定要中断当前数据迁移吗?此操作不可恢复!', '中断迁移', {
@@ -3361,7 +3372,7 @@ onActivated(() => {
if (loadedVmId !== vmId.value) initPage() if (loadedVmId !== vmId.value) initPage()
}) })
onDeactivated(() => { isPageActive = false; stopMigratePolling() }) onDeactivated(() => { isPageActive = false; stopMigratePolling() })
onBeforeUnmount(() => { isPageActive = false; disposeCharts(); stopMigratePolling() }) onBeforeUnmount(() => { isPageActive = false; disposeCharts(); stopMigratePolling(); stopDetailAutoRefresh() })
onMounted(() => { isPageActive = true; initPage() }) onMounted(() => { isPageActive = true; initPage() })
</script> </script>
+57 -5
View File
@@ -49,10 +49,21 @@
<span v-else class="text-muted">-</span> <span v-else class="text-muted">-</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="状态" width="140"> <el-table-column label="状态" width="180">
<template #default="{ row }"> <template #default="{ row }">
<el-tag :type="vmStatusType(row.status)" size="small">{{ vmStatusLabel(row.status) }}</el-tag> <template v-if="row.migrating">
<el-tag v-if="row.data_migrate_status && !['completed','failed','aborted','cancelled'].includes(row.data_migrate_status)" type="warning" size="small" effect="dark" style="margin-left:4px">迁移中</el-tag> <div class="migrate-inline-status">
<span class="migrate-inline-label">迁移中</span>
<el-progress
v-if="migrateProgressMap[row.id] != null"
:percentage="Math.min(migrateProgressMap[row.id], 100)"
:stroke-width="6" :show-text="false" status="warning"
style="flex:1;min-width:50px"
/>
<span class="migrate-inline-pct" v-if="migrateProgressMap[row.id] != null">{{ migrateProgressMap[row.id] }}%</span>
</div>
</template>
<el-tag v-else :type="vmStatusType(row.status)" size="small">{{ vmStatusLabel(row.status) }}</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<!-- <el-table-column label="宿主机" width="140"> <!-- <el-table-column label="宿主机" width="140">
@@ -357,14 +368,15 @@
</template> </template>
<script setup> <script setup>
import { ref, reactive, computed, inject, onMounted } from 'vue' import { ref, reactive, computed, inject, onMounted, onBeforeUnmount } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Refresh, Search, ArrowLeft, ArrowDown, WarningFilled } from '@element-plus/icons-vue' import { Plus, Refresh, Search, ArrowLeft, ArrowDown, WarningFilled } from '@element-plus/icons-vue'
import { import {
getRemoteHostList, getVmList, getVmDetail, getVmStatus, getRemoteHostList, getVmList, getVmDetail, getVmStatus,
createVm, rebuildVm, startVm, stopVm, rebootVm, suspendVm, createVm, rebuildVm, startVm, stopVm, rebootVm, suspendVm,
resumeVm, rescueVm, exitRescueVm, deleteVm, getNetworkList, getMetricsHistory resumeVm, rescueVm, exitRescueVm, deleteVm, getNetworkList, getMetricsHistory,
getDataMigrateProgress
} from '@/api/admin/kvmService' } from '@/api/admin/kvmService'
import { extractApiError } from '@/utils/kvmErrorUtil' import { extractApiError } from '@/utils/kvmErrorUtil'
import ImageSelectorPopup from '@/components/admin/ImageSelectorPopup.vue' import ImageSelectorPopup from '@/components/admin/ImageSelectorPopup.vue'
@@ -581,9 +593,46 @@ const loadList = async () => {
vmList.value = inner.data || inner.vms || (Array.isArray(inner) ? inner : []) vmList.value = inner.data || inner.vms || (Array.isArray(inner) ? inner : [])
total.value = inner.meta?.count ?? inner.all_count ?? inner.total ?? vmList.value.length total.value = inner.meta?.count ?? inner.all_count ?? inner.total ?? vmList.value.length
} else { vmList.value = []; total.value = 0 } } else { vmList.value = []; total.value = 0 }
handleMigrateAutoRefresh()
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '获取虚拟机列表失败')) } finally { loading.value = false } } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '获取虚拟机列表失败')) } finally { loading.value = false }
} }
const migrateProgressMap = reactive({})
let migrateAutoRefreshTimer = null
const fetchMigrateProgress = async () => {
const migratingVms = vmList.value.filter(v => v.migrating && v.migrate_task_id)
for (const vm of migratingVms) {
try {
const res = await getDataMigrateProgress({
service_id: serviceId.value,
migration_id: vm.migrate_task_id
})
if (res?.data?.code === 200 && res.data.data) {
migrateProgressMap[vm.id] = res.data.data.progress ?? null
}
} catch { /* ignore */ }
}
}
const handleMigrateAutoRefresh = () => {
const hasMigrating = vmList.value.some(v => v.migrating)
if (hasMigrating) {
fetchMigrateProgress()
if (!migrateAutoRefreshTimer) {
migrateAutoRefreshTimer = setInterval(() => { loadList() }, 3000)
}
} else {
stopMigrateAutoRefresh()
}
}
const stopMigrateAutoRefresh = () => {
if (migrateAutoRefreshTimer) { clearInterval(migrateAutoRefreshTimer); migrateAutoRefreshTimer = null }
}
onBeforeUnmount(() => stopMigrateAutoRefresh())
const handleSearch = () => { queryParams.page = 1; loadList() } const handleSearch = () => { queryParams.page = 1; loadList() }
const handleAdd = () => { const handleAdd = () => {
@@ -820,4 +869,7 @@ defineExpose({ loadList })
<style scoped> <style scoped>
.vm-manage-container { padding: 20px; } .vm-manage-container { padding: 20px; }
.vm-config { display: flex; gap: 4px; flex-wrap: wrap; } .vm-config { display: flex; gap: 4px; flex-wrap: wrap; }
.migrate-inline-status { display: flex; align-items: center; gap: 6px; margin-top: 4px; }
.migrate-inline-label { color: #e6a23c; font-size: 13px; font-weight: 600; white-space: nowrap; }
.migrate-inline-pct { color: #e6a23c; font-size: 12px; white-space: nowrap; }
</style> </style>