feat: 新增虚拟机批量数据迁移
This commit is contained in:
@@ -37,6 +37,9 @@
|
||||
<el-button type="warning" size="small" @click="handleBatchPower('stop')" :loading="batchLoading">
|
||||
<el-icon><SwitchButton /></el-icon>批量关机
|
||||
</el-button>
|
||||
<el-button type="primary" size="small" @click="openBatchDataMigrate" :loading="batchLoading">
|
||||
<el-icon><Promotion /></el-icon>批量数据迁移
|
||||
</el-button>
|
||||
<el-button type="danger" size="small" @click="handleBatchDelete" :loading="batchLoading">
|
||||
<el-icon><Delete /></el-icon>批量删除
|
||||
</el-button>
|
||||
@@ -443,6 +446,47 @@
|
||||
<!-- 用户选择器 -->
|
||||
<UserListSelector v-model="showUserSelector" :current-user-id="createForm.user_id" @confirm="handleUserSelected" />
|
||||
|
||||
<!-- 批量数据迁移 -->
|
||||
<el-dialog v-model="batchMigrateVisible" title="批量数据迁移" width="620px" destroy-on-close class="tk-dialog">
|
||||
<el-alert type="warning" :closable="false" show-icon style="margin-bottom:16px">
|
||||
将 {{ batchMigrateTargets.length }} 台虚拟机的数据迁移到同一目标宿主机。迁移任务将逐台提交,已在迁移中的实例会被跳过。
|
||||
</el-alert>
|
||||
<el-form :model="batchMigrateForm" label-width="110px">
|
||||
<div class="tk-section">
|
||||
<div class="tk-section-title">目标位置</div>
|
||||
<el-form-item label="目标主控服务" required>
|
||||
<div class="bind-selector-row">
|
||||
<el-input :model-value="batchMigrateForm.target_service_id ? `${batchMigrateForm._serviceName} (ID: ${batchMigrateForm.target_service_id})` : ''" readonly placeholder="请选择目标主控服务" />
|
||||
<el-button type="primary" @click="showBatchServiceSelector=true">选择</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="目标宿主机" required>
|
||||
<div class="bind-selector-row">
|
||||
<el-input :model-value="batchMigrateForm.target_host_id ? `${batchMigrateForm._hostName} (ID: ${batchMigrateForm.target_host_id})` : ''" readonly placeholder="请先选择目标主控服务" />
|
||||
<el-button type="primary" :disabled="!batchMigrateForm.target_service_id" @click="showBatchHostSelector=true">选择</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="tk-section">
|
||||
<div class="tk-section-title">网络配置</div>
|
||||
<div class="tk-resource-grid">
|
||||
<el-form-item label="IPv4 数量"><el-input-number v-model="batchMigrateForm.ipv4_num" :min="0" controls-position="right" /><span class="tk-res-unit">个/实例</span></el-form-item>
|
||||
<el-form-item label="IPv6 数量"><el-input-number v-model="batchMigrateForm.ipv6_num" :min="0" controls-position="right" /><span class="tk-res-unit">个/实例</span></el-form-item>
|
||||
</div>
|
||||
<el-form-item label="目标网络">
|
||||
<div class="batch-network-field">
|
||||
<div class="batch-network-tags" v-if="batchMigrateNetworks.length"><el-tag v-for="network in batchMigrateNetworks" :key="network.id" closable @close="removeBatchMigrateNetwork(network.id)">{{ network.name || network.id }} (#{{ network.id }})</el-tag></div>
|
||||
<el-button :disabled="!batchMigrateForm.target_host_id" @click="showBatchNetworkSelector=true">选择网络</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer><el-button @click="batchMigrateVisible=false">取消</el-button><el-button type="primary" :loading="batchMigrateSubmitting" @click="submitBatchDataMigrate">提交迁移</el-button></template>
|
||||
</el-dialog>
|
||||
<KvmServiceSelector v-model="showBatchServiceSelector" @confirm="handleBatchServiceSelected" />
|
||||
<HostSelectorPopup v-model="showBatchHostSelector" :service-id="batchMigrateForm.target_service_id || 0" :current-id="batchMigrateForm.target_host_id || 0" @confirm="handleBatchHostSelected" />
|
||||
<NetworkSelectorPopup v-model="showBatchNetworkSelector" :service-id="batchMigrateForm.target_service_id || 0" :host-id="batchMigrateForm.target_host_id || 0" filter-type="bridge" filter-used="false" @confirm="handleBatchNetworkSelected" />
|
||||
|
||||
<!-- 电源操作确认弹窗 -->
|
||||
<el-dialog v-model="powerDialogVisible" :title="`${powerLabels[powerAction] || ''}虚拟机`" width="400px" destroy-on-close>
|
||||
<div style="display: flex; align-items: flex-start; gap: 12px; padding: 8px 0">
|
||||
@@ -469,18 +513,20 @@
|
||||
import { ref, reactive, computed, inject, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Refresh, Search, ArrowLeft, ArrowDown, WarningFilled, VideoPlay, SwitchButton, Delete } from '@element-plus/icons-vue'
|
||||
import { Plus, Refresh, Search, ArrowLeft, ArrowDown, WarningFilled, VideoPlay, SwitchButton, Delete, Promotion } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getRemoteHostList, getVmList, getVmDetail, getVmStatus,
|
||||
createVm, rebuildVm, startVm, stopVm, rebootVm, suspendVm,
|
||||
resumeVm, rescueVm, exitRescueVm, deleteVm, getNetworkList, createNetwork, getMetricsHistory,
|
||||
getDataMigrateProgress
|
||||
getDataMigrateProgress, dataMigrateVm
|
||||
} from '@/api/admin/kvmService'
|
||||
import { extractApiError } from '@/utils/kvmErrorUtil'
|
||||
import ImageSelectorPopup from '@/components/admin/ImageSelectorPopup.vue'
|
||||
import HostGroupSelectorPopup from '@/components/admin/HostGroupSelectorPopup.vue'
|
||||
import UserListSelector from '@/components/admin/UserListSelector.vue'
|
||||
import NetworkSelectorPopup from '@/components/admin/NetworkSelectorPopup.vue'
|
||||
import KvmServiceSelector from '@/components/admin/KvmServiceSelector.vue'
|
||||
import HostSelectorPopup from '@/components/admin/HostSelectorPopup.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -516,6 +562,135 @@ const selectedVms = ref([])
|
||||
const handleSelectionChange = (selection) => { selectedVms.value = selection }
|
||||
const clearSelection = () => { vmTableRef.value?.clearSelection() }
|
||||
|
||||
// Batch data migration
|
||||
const batchMigrateVisible = ref(false)
|
||||
const batchMigrateSubmitting = ref(false)
|
||||
const showBatchServiceSelector = ref(false)
|
||||
const showBatchHostSelector = ref(false)
|
||||
const showBatchNetworkSelector = ref(false)
|
||||
const batchMigrateTargets = ref([])
|
||||
const batchMigrateNetworks = ref([])
|
||||
const batchMigrateForm = reactive({
|
||||
target_service_id: 0,
|
||||
target_host_id: 0,
|
||||
ipv4_num: 0,
|
||||
ipv6_num: 0,
|
||||
network_ids: [],
|
||||
_serviceName: '',
|
||||
_hostName: ''
|
||||
})
|
||||
|
||||
const resetBatchMigrateForm = () => {
|
||||
Object.assign(batchMigrateForm, {
|
||||
target_service_id: 0,
|
||||
target_host_id: 0,
|
||||
ipv4_num: 0,
|
||||
ipv6_num: 0,
|
||||
network_ids: [],
|
||||
_serviceName: '',
|
||||
_hostName: ''
|
||||
})
|
||||
batchMigrateNetworks.value = []
|
||||
}
|
||||
|
||||
const openBatchDataMigrate = () => {
|
||||
const targets = selectedVms.value.filter(vm => !vm.migrating)
|
||||
if (!targets.length) {
|
||||
ElMessage.warning('所选虚拟机均正在迁移中,请重新选择')
|
||||
return
|
||||
}
|
||||
const skipped = selectedVms.value.length - targets.length
|
||||
if (skipped) ElMessage.warning(`已跳过 ${skipped} 台正在迁移的虚拟机`)
|
||||
batchMigrateTargets.value = [...targets]
|
||||
resetBatchMigrateForm()
|
||||
batchMigrateVisible.value = true
|
||||
}
|
||||
|
||||
const handleBatchServiceSelected = (service) => {
|
||||
if (!service?.id) return
|
||||
batchMigrateForm.target_service_id = Number(service.id)
|
||||
batchMigrateForm._serviceName = service.name || ''
|
||||
batchMigrateForm.target_host_id = 0
|
||||
batchMigrateForm._hostName = ''
|
||||
batchMigrateForm.network_ids = []
|
||||
batchMigrateNetworks.value = []
|
||||
}
|
||||
|
||||
const handleBatchHostSelected = (host) => {
|
||||
if (!host?.id) return
|
||||
batchMigrateForm.target_host_id = Number(host.id)
|
||||
batchMigrateForm._hostName = host.name || host.ip || ''
|
||||
batchMigrateForm.network_ids = []
|
||||
batchMigrateNetworks.value = []
|
||||
}
|
||||
|
||||
const handleBatchNetworkSelected = (networks) => {
|
||||
const list = Array.isArray(networks) ? networks : (networks ? [networks] : [])
|
||||
const merged = [...batchMigrateNetworks.value, ...list]
|
||||
batchMigrateNetworks.value = [...new Map(merged.filter(item => item?.id).map(item => [Number(item.id), item])).values()]
|
||||
batchMigrateForm.network_ids = batchMigrateNetworks.value.map(item => Number(item.id))
|
||||
}
|
||||
|
||||
const removeBatchMigrateNetwork = (networkId) => {
|
||||
batchMigrateNetworks.value = batchMigrateNetworks.value.filter(item => Number(item.id) !== Number(networkId))
|
||||
batchMigrateForm.network_ids = batchMigrateNetworks.value.map(item => Number(item.id))
|
||||
}
|
||||
|
||||
const submitBatchDataMigrate = async () => {
|
||||
if (!batchMigrateForm.target_service_id) {
|
||||
ElMessage.warning('请选择目标主控服务')
|
||||
return
|
||||
}
|
||||
if (!batchMigrateForm.target_host_id) {
|
||||
ElMessage.warning('请选择目标宿主机')
|
||||
return
|
||||
}
|
||||
if (!batchMigrateTargets.value.length) {
|
||||
ElMessage.warning('没有可迁移的虚拟机')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定将 ${batchMigrateTargets.value.length} 台虚拟机迁移到宿主机「${batchMigrateForm._hostName || batchMigrateForm.target_host_id}」吗?`,
|
||||
'批量数据迁移',
|
||||
{ confirmButtonText: '确定迁移', cancelButtonText: '取消', type: 'warning' }
|
||||
)
|
||||
} catch { return }
|
||||
|
||||
batchMigrateSubmitting.value = true
|
||||
batchLoading.value = true
|
||||
let success = 0
|
||||
const failed = []
|
||||
for (const vm of batchMigrateTargets.value) {
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('source_service_id', serviceId.value)
|
||||
formData.append('source_vm_id', vm.id)
|
||||
formData.append('target_service_id', batchMigrateForm.target_service_id)
|
||||
formData.append('target_host_id', batchMigrateForm.target_host_id)
|
||||
if (batchMigrateForm.ipv4_num > 0) formData.append('ipv4_num', batchMigrateForm.ipv4_num)
|
||||
if (batchMigrateForm.ipv6_num > 0) formData.append('ipv6_num', batchMigrateForm.ipv6_num)
|
||||
batchMigrateForm.network_ids.forEach(id => formData.append('network_ids', id))
|
||||
const res = await dataMigrateVm(formData)
|
||||
if (res?.data?.code === 200) success++
|
||||
else failed.push(vm.name || `ID ${vm.id}`)
|
||||
} catch {
|
||||
failed.push(vm.name || `ID ${vm.id}`)
|
||||
}
|
||||
}
|
||||
batchMigrateSubmitting.value = false
|
||||
batchLoading.value = false
|
||||
batchMigrateVisible.value = false
|
||||
clearSelection()
|
||||
if (failed.length) {
|
||||
ElMessage.warning(`批量迁移提交完成:成功 ${success},失败 ${failed.length}(${failed.slice(0, 3).join('、')}${failed.length > 3 ? ' 等' : ''})`)
|
||||
} else {
|
||||
ElMessage.success(`已提交 ${success} 台虚拟机的数据迁移任务`)
|
||||
}
|
||||
loadList()
|
||||
}
|
||||
|
||||
// 选择器
|
||||
const showCreateImageSelector = ref(false)
|
||||
const showRebuildImageSelector = ref(false)
|
||||
@@ -1177,6 +1352,8 @@ defineExpose({ loadList })
|
||||
color: #409eff;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.batch-network-field { display: flex; align-items: flex-start; gap: 8px; width: 100%; }
|
||||
.batch-network-tags { display: flex; flex: 1; flex-wrap: wrap; gap: 6px; min-width: 0; }
|
||||
.threshold-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 76px minmax(220px, 1fr) minmax(220px, 1fr);
|
||||
|
||||
Reference in New Issue
Block a user