{{ network.name || network.id }} (#{{ network.id }})
@@ -541,7 +594,7 @@ import {
getRemoteHostList, getVmList, getVmDetail, getVmStatus,
createVm, rebuildVm, startVm, stopVm, rebootVm, suspendVm,
resumeVm, rescueVm, exitRescueVm, deleteVm, getNetworkList, createNetwork, getMetricsHistory,
- getDataMigrateProgress, dataMigrateVm
+ getDataMigrateProgress, dataMigrateVm, migrateVm, getRemoteHostGroupList
} from '@/api/admin/kvmService'
import { extractApiError } from '@/utils/kvmErrorUtil'
import ImageSelectorPopup from '@/components/admin/ImageSelectorPopup.vue'
@@ -585,6 +638,145 @@ const selectedVms = ref([])
const handleSelectionChange = (selection) => { selectedVms.value = selection }
const clearSelection = () => { vmTableRef.value?.clearSelection() }
+// 批量宿主机迁移
+const batchHostMigrateVisible = ref(false)
+const batchHostMigrateSubmitting = ref(false)
+const batchHostMigrateMode = ref('host')
+const batchHostMigrateTargets = ref([])
+const batchHostMigrateNetworks = ref([])
+const batchHostMigrateGroupOptions = ref([])
+const batchHostMigrateOptionsLoading = ref(false)
+const showBatchHostMigrateHostSelector = ref(false)
+const showBatchHostMigrateNetworkSelector = ref(false)
+const batchHostMigrateForm = reactive({
+ target_host_id: 0,
+ target_host_group_id: 0,
+ ipv4_num: 0,
+ ipv6_num: 0,
+ expire_offset_days: 0,
+ network_ids: [],
+ _hostName: ''
+})
+
+const resetBatchHostMigrateForm = () => {
+ Object.assign(batchHostMigrateForm, {
+ target_host_id: 0,
+ target_host_group_id: 0,
+ ipv4_num: 0,
+ ipv6_num: 0,
+ expire_offset_days: 0,
+ network_ids: [],
+ _hostName: ''
+ })
+ batchHostMigrateMode.value = 'host'
+ batchHostMigrateNetworks.value = []
+}
+
+const openBatchHostMigrate = async () => {
+ 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} 台正在迁移的虚拟机`)
+ batchHostMigrateTargets.value = [...targets]
+ resetBatchHostMigrateForm()
+ batchHostMigrateVisible.value = true
+ batchHostMigrateOptionsLoading.value = true
+ try {
+ const res = await getRemoteHostGroupList({ service_id: serviceId.value, page: 1, page_size: 100 })
+ if (res?.data?.code === 200 && res?.data?.data) {
+ const inner = res.data.data
+ batchHostMigrateGroupOptions.value = Array.isArray(inner) ? inner : (inner.host_groups || inner.data || [])
+ } else {
+ batchHostMigrateGroupOptions.value = []
+ }
+ } catch {
+ batchHostMigrateGroupOptions.value = []
+ } finally {
+ batchHostMigrateOptionsLoading.value = false
+ }
+}
+
+const handleBatchHostMigrateHostSelected = (host) => {
+ if (!host?.id) return
+ batchHostMigrateForm.target_host_id = Number(host.id)
+ batchHostMigrateForm._hostName = host.name || host.ip || ''
+ batchHostMigrateForm.network_ids = []
+ batchHostMigrateNetworks.value = []
+}
+
+const handleBatchHostMigrateNetworkSelected = (networks) => {
+ const list = Array.isArray(networks) ? networks : (networks ? [networks] : [])
+ const merged = [...batchHostMigrateNetworks.value, ...list]
+ batchHostMigrateNetworks.value = [...new Map(merged.filter(item => item?.id).map(item => [Number(item.id), item])).values()]
+ batchHostMigrateForm.network_ids = batchHostMigrateNetworks.value.map(item => Number(item.id))
+}
+
+const removeBatchHostMigrateNetwork = (networkId) => {
+ batchHostMigrateNetworks.value = batchHostMigrateNetworks.value.filter(item => Number(item.id) !== Number(networkId))
+ batchHostMigrateForm.network_ids = batchHostMigrateNetworks.value.map(item => Number(item.id))
+}
+
+const submitBatchHostMigrate = async () => {
+ if (batchHostMigrateMode.value === 'host' && !batchHostMigrateForm.target_host_id) {
+ ElMessage.warning('请选择目标宿主机')
+ return
+ }
+ if (batchHostMigrateMode.value === 'group' && !batchHostMigrateForm.target_host_group_id) {
+ ElMessage.warning('请选择目标宿主机组')
+ return
+ }
+ if (!batchHostMigrateTargets.value.length) {
+ ElMessage.warning('没有可迁移的虚拟机')
+ return
+ }
+ const targetName = batchHostMigrateMode.value === 'host'
+ ? (batchHostMigrateForm._hostName || batchHostMigrateForm.target_host_id)
+ : (batchHostMigrateGroupOptions.value.find(group => Number(group.id) === Number(batchHostMigrateForm.target_host_group_id))?.name || batchHostMigrateForm.target_host_group_id)
+ try {
+ await ElMessageBox.confirm(
+ `确定将 ${batchHostMigrateTargets.value.length} 台虚拟机迁移到「${targetName}」吗?`,
+ '批量宿主机迁移',
+ { confirmButtonText: '确定迁移', cancelButtonText: '取消', type: 'warning' }
+ )
+ } catch { return }
+
+ batchHostMigrateSubmitting.value = true
+ batchLoading.value = true
+ let success = 0
+ const failed = []
+ for (const vm of batchHostMigrateTargets.value) {
+ try {
+ const formData = new FormData()
+ formData.append('service_id', serviceId.value)
+ formData.append('vm_id', vm.id)
+ if (batchHostMigrateMode.value === 'host') formData.append('target_host_id', batchHostMigrateForm.target_host_id)
+ else formData.append('target_host_group_id', batchHostMigrateForm.target_host_group_id)
+ if (batchHostMigrateForm.ipv4_num > 0) formData.append('ipv4_num', batchHostMigrateForm.ipv4_num)
+ if (batchHostMigrateForm.ipv6_num > 0) formData.append('ipv6_num', batchHostMigrateForm.ipv6_num)
+ if (batchHostMigrateForm.expire_offset_days > 0) formData.append('expire_offset_days', batchHostMigrateForm.expire_offset_days)
+ if (batchHostMigrateMode.value === 'host') batchHostMigrateForm.network_ids.forEach(id => formData.append('network_ids', id))
+ const res = await migrateVm(formData)
+ if (res?.data?.code === 200) success++
+ else failed.push(vm.name || `ID ${vm.id}`)
+ } catch {
+ failed.push(vm.name || `ID ${vm.id}`)
+ }
+ }
+ batchHostMigrateSubmitting.value = false
+ batchLoading.value = false
+ batchHostMigrateVisible.value = false
+ clearSelection()
+ if (failed.length) {
+ ElMessage.warning(`批量宿主机迁移提交完成:成功 ${success},失败 ${failed.length}(${failed.slice(0, 3).join('、')}${failed.length > 3 ? ' 等' : ''})`)
+ } else {
+ ElMessage.success(`已提交 ${success} 台虚拟机的宿主机迁移任务`)
+ }
+ loadList()
+}
+
// Batch data migration
const batchMigrateVisible = ref(false)
const batchMigrateSubmitting = ref(false)
@@ -598,6 +790,7 @@ const batchMigrateForm = reactive({
target_host_id: 0,
ipv4_num: 0,
ipv6_num: 0,
+ expire_offset_days: 0,
network_ids: [],
_serviceName: '',
_hostName: ''
@@ -609,6 +802,7 @@ const resetBatchMigrateForm = () => {
target_host_id: 0,
ipv4_num: 0,
ipv6_num: 0,
+ expire_offset_days: 0,
network_ids: [],
_serviceName: '',
_hostName: ''
@@ -694,6 +888,7 @@ const submitBatchDataMigrate = async () => {
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)
+ if (batchMigrateForm.expire_offset_days > 0) formData.append('expire_offset_days', batchMigrateForm.expire_offset_days)
batchMigrateForm.network_ids.forEach(id => formData.append('network_ids', id))
const res = await dataMigrateVm(formData)
if (res?.data?.code === 200) success++
@@ -1398,6 +1593,7 @@ defineExpose({ loadList })
.migrate-inline-pct { color: #e6a23c; font-size: 12px; white-space: nowrap; }
.batch-bar {
display: flex;
+ flex-wrap: wrap;
align-items: center;
gap: 10px;
padding: 10px 16px;