@@ -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);