fix: 重构虚拟机内网外网参数设置选择网络
Build and Deploy Vue3 / build (push) Successful in 1m28s
Build and Deploy Vue3 / deploy (push) Successful in 1m1s

This commit is contained in:
2026-03-26 16:36:25 +08:00
parent 40a5e486a6
commit 1a4587f893
13 changed files with 1028 additions and 135 deletions
+469 -80
View File
@@ -33,6 +33,7 @@
<el-dropdown-item divided command="rebuild">重装虚拟机</el-dropdown-item>
<el-dropdown-item command="rescue">救援模式</el-dropdown-item>
<el-dropdown-item command="exitRescue">退出救援</el-dropdown-item>
<el-dropdown-item divided command="migrateVm">迁移虚拟机</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
@@ -129,7 +130,7 @@
<div class="config-row">
<div class="config-cell">
<span class="config-label">流量上限(GB)</span>
<span class="config-value">{{ detail.traffic_max != null ? (detail.traffic_max / 1024 / 1024).toFixed(2) : '-' }}</span>
<span class="config-value">{{ detail.traffic_max != null ? (detail.traffic_max / 1024).toFixed(2) : '-' }}</span>
</div>
<div class="config-cell">
<span class="config-label">快照配额</span>
@@ -399,6 +400,63 @@
</div>
</el-tab-pane>
<el-tab-pane label="组网管理" name="userNetworking">
<div class="section-block">
<div class="section-header">
<h3 class="section-title">用户组网</h3>
<div style="display: flex; gap: 8px">
<el-button size="small" type="primary" @click="handleVnCreate"><el-icon><Plus /></el-icon>创建组网</el-button>
<el-button size="small" :icon="Refresh" @click="loadVmNetworkingList" :loading="vnLoading">刷新</el-button>
</div>
</div>
<el-table :data="vnList" v-loading="vnLoading" stripe size="small">
<el-table-column label="组网" min-width="130">
<template #default="{ row }">{{ row.networking?.name || '-' }} <span style="color:#909399">(ID: {{ row.networking?.id }})</span></template>
</el-table-column>
<el-table-column label="状态" width="90">
<template #default="{ row }">
<el-tag v-if="row.network" type="success" size="small">已加入</el-tag>
<el-tag v-else type="info" size="small">未加入</el-tag>
</template>
</el-table-column>
<el-table-column label="网桥" width="130">
<template #default="{ row }"><span class="mono-text">{{ row.network?.bridge_name || row.networking?.bridge_name || '-' }}</span></template>
</el-table-column>
<el-table-column label="IP地址" min-width="150">
<template #default="{ row }">
<span v-if="row.network" class="mono-text">{{ row.network.address || '-' }}</span>
<span v-else style="color:#c0c4cc">-</span>
</template>
</el-table-column>
<el-table-column label="网关" width="140">
<template #default="{ row }">
<span v-if="row.network" class="mono-text">{{ row.network.gateway || '-' }}</span>
<span v-else class="mono-text" style="color:#c0c4cc">{{ row.networking?.gateway || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="MAC地址" min-width="160">
<template #default="{ row }">
<span v-if="row.network" class="mono-text">{{ row.network.mac_address || '-' }}</span>
<span v-else style="color:#c0c4cc">-</span>
</template>
</el-table-column>
<el-table-column label="类型" width="80">
<template #default="{ row }">
<el-tag v-if="row.network" :type="row.network.type === 'bridge' ? 'success' : 'warning'" size="small">{{ row.network.type || '-' }}</el-tag>
<span v-else style="color:#c0c4cc">-</span>
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button v-if="!row.network" link type="primary" size="small" @click="handleJoinNetworking(row)">加入</el-button>
<el-button v-else link type="danger" size="small" @click="handleLeaveNetworking(row)">移除</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!vnList.length && !vnLoading" description="没有匹配的可用组网" :image-size="60" />
</div>
</el-tab-pane>
<el-tab-pane label="监控" name="monitor">
<div class="section-block">
<div class="section-header">
@@ -532,19 +590,20 @@
<el-input-number v-model="editForm.ssh_port" :min="1" :max="65535" controls-position="right" style="width: 200px" />
</el-form-item>
<el-form-item label="网络">
<div style="width: 100%">
<div style="display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 6px" v-if="editSelectedNetworks.length">
<el-tag v-for="n in editSelectedNetworks" :key="n.id" closable size="small" @close="removeEditNetwork(n.id)">
{{ n.name }} (ID:{{ n.id }})
</el-tag>
</div>
<el-button size="small" @click="showEditNetworkSelector = true">选择网络</el-button>
<div style="display: flex; align-items: center; gap: 8px; width: 100%">
<el-tag v-if="editSelectedNetworks.length" closable @close="removeEditNetwork(editSelectedNetworks[0].id)">
{{ editSelectedNetworks[0].name }} (ID:{{ editSelectedNetworks[0].id }})
</el-tag>
<el-button size="small" @click="showEditNetworkSelector = true">{{ editSelectedNetworks.length ? '更换网络' : '选择网络' }}</el-button>
</div>
</el-form-item>
<el-form-item label="内网组网">
<el-select v-model="editForm.internet_network_id" placeholder="选择内网组网可选" clearable filterable style="width: 100%">
<el-option v-for="n in networkingOptions" :key="n.id" :label="`${n.name} (ID: ${n.id})`" :value="n.id" />
</el-select>
<el-form-item label="内网">
<div style="display: flex; align-items: center; gap: 8px; width: 100%">
<el-tag v-if="editSelectedInternalNetworks.length" closable @close="removeEditInternalNetwork(editSelectedInternalNetworks[0].id)">
{{ editSelectedInternalNetworks[0].name }} (ID:{{ editSelectedInternalNetworks[0].id }})
</el-tag>
<el-button size="small" @click="showEditInternalNetworkSelector = true">{{ editSelectedInternalNetworks.length ? '更换内网' : '选择内网' }}</el-button>
</div>
</el-form-item>
<el-form-item label="安全组">
<el-select v-model="editForm.port_group_id" placeholder="选择安全组可选" filterable clearable style="width: 100%">
@@ -557,8 +616,10 @@
<el-button type="primary" :loading="actionLoading" @click="submitEditVm">确定</el-button>
</template>
</el-dialog>
<!-- 编辑用网络选择器 -->
<NetworkSelectorPopup v-model="showEditNetworkSelector" :service-id="serviceId" :host-id="vmHostId" @confirm="handleEditNetworkConfirm" />
<!-- 编辑用网络选择器(外网 bridge -->
<NetworkSelectorPopup v-model="showEditNetworkSelector" :service-id="serviceId" :host-id="vmHostId" filter-type="bridge" filter-used="false" @confirm="handleEditNetworkConfirm" @create="() => handleNetCreate('edit')" />
<!-- 编辑用内网选择器(内网 nat -->
<NetworkSelectorPopup v-model="showEditInternalNetworkSelector" :service-id="serviceId" :host-id="vmHostId" filter-type="nat" filter-used="false" @confirm="handleEditInternalNetworkConfirm" @create="() => handleNetCreate('editInternal')" />
<!-- 重构虚拟机弹窗 -->
<el-dialog v-model="refactorDialogVisible" title="重构虚拟机" width="650px" destroy-on-close>
@@ -627,19 +688,20 @@
<el-input v-model="refactorForm.vnc_password" placeholder="不填随机" show-password />
</el-form-item>
<el-form-item label="网络">
<div style="width: 100%">
<div style="display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 6px" v-if="refactorSelectedNetworks.length">
<el-tag v-for="n in refactorSelectedNetworks" :key="n.id" closable size="small" @close="removeRefactorNetwork(n.id)">
{{ n.name }} (ID:{{ n.id }})
</el-tag>
</div>
<el-button size="small" @click="showRefactorNetworkSelector = true">选择网络</el-button>
<div style="display: flex; align-items: center; gap: 8px; width: 100%">
<el-tag v-if="refactorSelectedNetworks.length" closable @close="removeRefactorNetwork(refactorSelectedNetworks[0].id)">
{{ refactorSelectedNetworks[0].name }} (ID:{{ refactorSelectedNetworks[0].id }})
</el-tag>
<el-button size="small" @click="showRefactorNetworkSelector = true">{{ refactorSelectedNetworks.length ? '更换网络' : '选择网络' }}</el-button>
</div>
</el-form-item>
<el-form-item label="内网组网">
<el-select v-model="refactorForm.internet_network_id" placeholder="选择内网组网可选" clearable filterable style="width: 100%">
<el-option v-for="n in networkingOptions" :key="n.id" :label="`${n.name} (ID: ${n.id})`" :value="n.id" />
</el-select>
<el-form-item label="内网">
<div style="display: flex; align-items: center; gap: 8px; width: 100%">
<el-tag v-if="refactorSelectedInternalNetworks.length" closable @close="removeRefactorInternalNetwork(refactorSelectedInternalNetworks[0].id)">
{{ refactorSelectedInternalNetworks[0].name }} (ID:{{ refactorSelectedInternalNetworks[0].id }})
</el-tag>
<el-button size="small" @click="showRefactorInternalNetworkSelector = true">{{ refactorSelectedInternalNetworks.length ? '更换内网' : '选择内网' }}</el-button>
</div>
</el-form-item>
<el-form-item label="安全组">
<el-select v-model="refactorForm.port_group_id" placeholder="选择安全组可选" filterable clearable style="width: 100%">
@@ -653,8 +715,10 @@
</template>
</el-dialog>
<!-- 重构用网络选择器 -->
<NetworkSelectorPopup v-model="showRefactorNetworkSelector" :service-id="serviceId" :host-id="vmHostId" @confirm="handleRefactorNetworkConfirm" />
<!-- 重构用网络选择器(外网 bridge -->
<NetworkSelectorPopup v-model="showRefactorNetworkSelector" :service-id="serviceId" :host-id="vmHostId" filter-type="bridge" filter-used="false" @confirm="handleRefactorNetworkConfirm" @create="() => handleNetCreate('refactor')" />
<!-- 重构用内网选择器(内网 nat -->
<NetworkSelectorPopup v-model="showRefactorInternalNetworkSelector" :service-id="serviceId" :host-id="vmHostId" filter-type="nat" filter-used="false" @confirm="handleRefactorInternalNetworkConfirm" @create="() => handleNetCreate('refactorInternal')" />
<!-- VNC 连接弹窗 -->
<el-dialog v-model="vncDialogVisible" title="获取 VNC 连接" width="560px" destroy-on-close>
@@ -690,7 +754,7 @@
<el-form-item label="上行带宽(Mbps)">
<el-input-number v-model="trafficForm.tx_bandwidth" :min="0" controls-position="right" style="width: 100%" />
</el-form-item>
<el-form-item label="每月最大流量(KB)">
<el-form-item label="每月最大流量(MB)">
<el-input-number v-model="trafficForm.traffic_max" :min="0" controls-position="right" style="width: 100%" />
</el-form-item>
</el-form>
@@ -700,8 +764,50 @@
</template>
</el-dialog>
<!-- 迁移虚拟机弹窗 -->
<el-dialog v-model="migrateDialogVisible" title="迁移虚拟机更换宿主机" width="560px" destroy-on-close>
<el-alert type="warning" :closable="false" style="margin-bottom: 16px">将虚拟机迁移到其他宿主机或宿主机组,请谨慎操作!</el-alert>
<el-form label-width="130px" v-loading="migrateOptionsLoading">
<el-form-item label="当前宿主机">
<el-tag>{{ detail?.host_name || detail?.host_id || '-' }}</el-tag>
</el-form-item>
<el-form-item label="迁移目标方式">
<el-radio-group v-model="migrateMode">
<el-radio value="host">指定宿主机</el-radio>
<el-radio value="group">指定宿主机组</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="目标宿主机" v-if="migrateMode === 'host'">
<el-select v-model="migrateForm.target_host_id" placeholder="选择目标宿主机" filterable style="width: 100%">
<el-option v-for="h in migrateHostOptions" :key="h.id" :label="`${h.name} (${h.ip || h.id})`" :value="h.id" :disabled="h.id === detail?.host_id" />
</el-select>
</el-form-item>
<el-form-item label="目标宿主机组" v-if="migrateMode === 'group'">
<el-select v-model="migrateForm.target_host_group_id" placeholder="选择目标宿主机组" filterable style="width: 100%">
<el-option v-for="g in migrateGroupOptions" :key="g.id" :label="`${g.name} (ID: ${g.id})`" :value="g.id" />
</el-select>
</el-form-item>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="新IPv4数量">
<el-input-number v-model="migrateForm.ipv4_num" :min="0" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="新IPv6数量">
<el-input-number v-model="migrateForm.ipv6_num" :min="0" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="migrateDialogVisible = false">取消</el-button>
<el-button type="warning" :loading="actionLoading" @click="submitMigrateVm">确定迁移</el-button>
</template>
</el-dialog>
<!-- 绑定网络选择器 -->
<NetworkSelectorPopup v-model="showNetBindSelector" :service-id="serviceId" :host-id="vmHostId" @confirm="handleNetBindConfirm" @create="handleNetCreate" />
<NetworkSelectorPopup v-model="showNetBindSelector" :service-id="serviceId" :host-id="vmHostId" filter-type="bridge" filter-used="false" @confirm="handleNetBindConfirm" @create="() => handleNetCreate('bind')" />
<!-- 创建/编辑网络弹窗 -->
<el-dialog v-model="netDialogVisible" :title="netDialogType === 'add' ? '创建网络' : '编辑网络'" width="600px" destroy-on-close>
@@ -723,7 +829,7 @@
<el-form-item label="逻辑端口名"><el-input v-model="netForm.ls_name" placeholder="不填使用默认" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="netDialogVisible = false">取消</el-button>
<el-button @click="handleNetDialogCancel">取消</el-button>
<el-button type="primary" :loading="actionLoading" @click="submitNetForm">确定</el-button>
</template>
</el-dialog>
@@ -968,6 +1074,47 @@
<el-button type="primary" :loading="sgSubmitLoading" @click="submitSgRule">确定</el-button>
</template>
</el-dialog>
<!-- 加入组网弹窗 -->
<el-dialog v-model="vnJoinVisible" title="加入组网" width="480px" destroy-on-close>
<el-form label-width="100px">
<el-form-item label="组网">{{ vnJoinTarget?.name || '-' }} (ID: {{ vnJoinTarget?.id }})</el-form-item>
<el-form-item label="网桥"><span class="mono-text">{{ vnJoinTarget?.bridge_name || '-' }}</span></el-form-item>
<el-form-item label="网关"><span class="mono-text">{{ vnJoinTarget?.gateway || '-' }}</span></el-form-item>
<el-form-item label="指定IP">
<el-input v-model="vnJoinIp" placeholder="留空自动分配" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="vnJoinVisible = false">取消</el-button>
<el-button type="primary" :loading="vnSubmitLoading" @click="submitJoinNetworking">加入</el-button>
</template>
</el-dialog>
<!-- 创建组网弹窗 -->
<el-dialog v-model="vnCreateVisible" title="创建组网" width="500px" destroy-on-close>
<el-form ref="vnCreateFormRef" :model="vnCreateForm" :rules="vnCreateRules" label-width="100px">
<el-form-item label="名称" prop="name">
<el-input v-model="vnCreateForm.name" placeholder="组网名称" />
</el-form-item>
<el-form-item label="网桥名称" prop="bridge_name">
<el-input v-model="vnCreateForm.bridge_name" placeholder="网桥名称" />
</el-form-item>
<el-form-item label="网关">
<el-input v-model="vnCreateForm.gateway" placeholder="可选 10.0.0.1/24" />
</el-form-item>
<el-form-item label="宿主机">
<span style="color: #606266">{{ vnCreateHostName || '加载中...' }} <span style="color: #909399">(ID: {{ vmHostId }})</span></span>
</el-form-item>
<el-form-item label="用户">
<span style="color: #606266">{{ vnCreateUserName || '加载中...' }} <span style="color: #909399">(ID: {{ detail?.user_id || '-' }})</span></span>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="vnCreateVisible = false">取消</el-button>
<el-button type="primary" :loading="vnSubmitLoading" @click="submitVnCreate">创建</el-button>
</template>
</el-dialog>
</div>
</template>
@@ -981,7 +1128,7 @@ import {
startVm, stopVm, rebootVm, suspendVm, resumeVm,
rebuildVm, refactorVm, updateVm, updateVmTraffic, rescueVm, exitRescueVm,
getVmVnc, getVncNodeList,
getSecurityGroupList, getUserNetworkingList,
getSecurityGroupList, getUserNetworkingList, createUserNetworking, assignUserNetworking, removeUserNetworkingNetwork,
getRemoteHostList, getSecurityGroupDetail, createSecurityGroup,
syncSecurityGroup, deleteSecurityGroup,
enableSecurityGroupWhitelist, disableSecurityGroupWhitelist,
@@ -990,8 +1137,10 @@ import {
createVolume, resizeVolume, mountVolume, unmountVolume, transferVolume, deleteVolume,
getVmList, bindSecurityGroup, unbindSecurityGroup,
getSnapshotList, createSnapshot, restoreSnapshot, deleteSnapshot, getSnapshotProgress, getSnapshotCount, setSnapshotLimit,
getBackupList, createBackup, restoreBackup, deleteBackup, getBackupProgress, getBackupCount, setBackupLimit
getBackupList, createBackup, restoreBackup, deleteBackup, getBackupProgress, getBackupCount, setBackupLimit,
migrateVm, getRemoteHostGroupList, getRemoteHostDetail
} from '@/api/admin/kvmService'
import { getUserInfo } from '@/api/admin/user'
import { extractApiError } from '@/utils/kvmErrorUtil'
import * as echarts from 'echarts'
import ImageSelectorPopup from '@/components/admin/ImageSelectorPopup.vue'
@@ -1007,7 +1156,7 @@ const tagsViewStore = useTagsViewStore()
const serviceId = computed(() => parseInt(route.query.service_id) || 0)
const serviceName = computed(() => route.query.service_name || '')
const vmId = computed(() => parseInt(route.query.vm_id) || parseInt(route.query.id) || 0)
const vmId = computed(() => parseInt(route.query.vm_id) || 0)
const loading = ref(false)
const actionLoading = ref(false)
@@ -1074,7 +1223,8 @@ const handleMoreCommand = (cmd) => {
if (powerActions.includes(cmd)) { handlePower(cmd); return }
const actionMap = {
editVm: handleEditVm, refactorVm: handleRefactorVm, updateTraffic: handleUpdateTraffic,
rebuild: handleRebuild, rescue: handleRescue, exitRescue: handleExitRescue
rebuild: handleRebuild, rescue: handleRescue, exitRescue: handleExitRescue,
migrateVm: handleMigrateVm
}
if (actionMap[cmd]) actionMap[cmd]()
}
@@ -1337,30 +1487,24 @@ const editFormRef = ref(null)
const editForm = reactive({
rx_bandwidth: 0, tx_bandwidth: 0,
root_password: '', ssh_port: 22,
internet_network_id: 0, port_group_id: 0
port_group_id: ''
})
const editSelectedNetworks = ref([])
const showEditNetworkSelector = ref(false)
const editSelectedInternalNetworks = ref([])
const showEditInternalNetworkSelector = ref(false)
const handleEditNetworkConfirm = (network) => {
if (!editSelectedNetworks.value.find(n => n.id === network.id)) {
editSelectedNetworks.value.push({ id: network.id, name: network.name })
}
editSelectedNetworks.value = [{ id: network.id, name: network.name }]
}
const removeEditNetwork = (id) => {
editSelectedNetworks.value = editSelectedNetworks.value.filter(n => n.id !== id)
if (editForm.internet_network_id === id) editForm.internet_network_id = 0
}
const networkingOptions = ref([])
const loadNetworkingOptions = async () => {
try {
const res = await getUserNetworkingList({ service_id: serviceId.value, page: 1, count: 10 })
if (res?.data?.code === 200 && res?.data?.data) {
const inner = res.data.data
networkingOptions.value = Array.isArray(inner) ? inner : (inner.data || [])
}
} catch { /* */ }
const handleEditInternalNetworkConfirm = (network) => {
editSelectedInternalNetworks.value = [{ id: network.id, name: network.name }]
}
const removeEditInternalNetwork = (id) => {
editSelectedInternalNetworks.value = editSelectedInternalNetworks.value.filter(n => n.id !== id)
}
const handleEditVm = async () => {
@@ -1371,16 +1515,17 @@ const handleEditVm = async () => {
tx_bandwidth: d.tx_bandwidth || 0,
root_password: '',
ssh_port: d.ssh_port || 22,
internet_network_id: '',
port_group_id: vmPortGroup.value?.id || 0
port_group_id: vmPortGroup.value?.id || ''
})
editSelectedNetworks.value = vmNetworks.value.map(n => ({ id: n.id, name: n.name }))
const bridgeNets = vmNetworks.value.filter(n => n.type === 'bridge')
const natNets = vmNetworks.value.filter(n => n.type === 'nat')
editSelectedNetworks.value = bridgeNets.length ? [{ id: bridgeNets[0].id, name: bridgeNets[0].name }] : []
editSelectedInternalNetworks.value = natNets.length ? [{ id: natNets[0].id, name: natNets[0].name }] : []
editDialogVisible.value = true
dialogOptionsLoading.value = true
try {
await Promise.all([
!sgOptions.value.length ? loadSgOptions() : Promise.resolve(),
loadNetworkingOptions()
!sgOptions.value.length ? loadSgOptions() : Promise.resolve()
])
} finally { dialogOptionsLoading.value = false }
}
@@ -1396,7 +1541,7 @@ const submitEditVm = async () => {
if (editForm.root_password) fd.append('root_password', editForm.root_password)
fd.append('ssh_port', editForm.ssh_port)
editSelectedNetworks.value.forEach(n => fd.append('network_ids', n.id))
if (editForm.internet_network_id) fd.append('internet_network_id', editForm.internet_network_id)
editSelectedInternalNetworks.value.forEach(n => fd.append('internet_network_id', n.id))
if (editForm.port_group_id) fd.append('port_group_id', editForm.port_group_id)
const res = await updateVm(fd)
if (res?.data?.code === 200) { ElMessage.success('修改成功'); editDialogVisible.value = false; loadDetail() }
@@ -1411,12 +1556,14 @@ const refactorForm = reactive({
memory: 0, vcpu: 0, rx_bandwidth: 0, tx_bandwidth: 0,
root_password: '', uuid: '', mate_data_id: '', physical_name: '', config_path: '',
ssh_port: 0, vnc_port: 0, vnc_password: '',
internet_network_id: 0, port_group_id: 0
port_group_id: ''
})
const refactorMemUnit = ref(1048576)
const refactorMemDisplay = ref(0)
const refactorSelectedNetworks = ref([])
const showRefactorNetworkSelector = ref(false)
const refactorSelectedInternalNetworks = ref([])
const showRefactorInternalNetworkSelector = ref(false)
const onRefactorMemUnitChange = () => {
refactorMemDisplay.value = refactorForm.memory ? Math.round(refactorForm.memory / refactorMemUnit.value * 100) / 100 : 0
@@ -1424,13 +1571,16 @@ const onRefactorMemUnitChange = () => {
watch(refactorMemDisplay, (v) => { refactorForm.memory = Math.round(v * refactorMemUnit.value) })
const handleRefactorNetworkConfirm = (network) => {
if (!refactorSelectedNetworks.value.find(n => n.id === network.id)) {
refactorSelectedNetworks.value.push({ id: network.id, name: network.name })
}
refactorSelectedNetworks.value = [{ id: network.id, name: network.name }]
}
const removeRefactorNetwork = (id) => {
refactorSelectedNetworks.value = refactorSelectedNetworks.value.filter(n => n.id !== id)
if (refactorForm.internet_network_id === id) refactorForm.internet_network_id = 0
}
const handleRefactorInternalNetworkConfirm = (network) => {
refactorSelectedInternalNetworks.value = [{ id: network.id, name: network.name }]
}
const removeRefactorInternalNetwork = (id) => {
refactorSelectedInternalNetworks.value = refactorSelectedInternalNetworks.value.filter(n => n.id !== id)
}
const handleRefactorVm = async () => {
@@ -1443,10 +1593,12 @@ const handleRefactorVm = async () => {
uuid: d.uuid || '', mate_data_id: d.mate_data_id || '',
physical_name: d.physical_name || '', config_path: d.config_path || '',
ssh_port: d.ssh_port || 0, vnc_port: 0, vnc_password: '',
internet_network_id: 0,
port_group_id: vmPortGroup.value?.id || 0
port_group_id: vmPortGroup.value?.id || ''
})
refactorSelectedNetworks.value = vmNetworks.value.map(n => ({ id: n.id, name: n.name }))
const bridgeNets = vmNetworks.value.filter(n => n.type === 'bridge')
const natNets = vmNetworks.value.filter(n => n.type === 'nat')
refactorSelectedNetworks.value = bridgeNets.length ? [{ id: bridgeNets[0].id, name: bridgeNets[0].name }] : []
refactorSelectedInternalNetworks.value = natNets.length ? [{ id: natNets[0].id, name: natNets[0].name }] : []
const mem = d.memory || 0
if (mem >= 1048576 && mem % 1048576 === 0) { refactorMemUnit.value = 1048576; refactorMemDisplay.value = mem / 1048576 }
else if (mem >= 1024 && mem % 1024 === 0) { refactorMemUnit.value = 1024; refactorMemDisplay.value = mem / 1024 }
@@ -1455,8 +1607,7 @@ const handleRefactorVm = async () => {
dialogOptionsLoading.value = true
try {
await Promise.all([
!sgOptions.value.length ? loadSgOptions() : Promise.resolve(),
loadNetworkingOptions()
!sgOptions.value.length ? loadSgOptions() : Promise.resolve()
])
} finally { dialogOptionsLoading.value = false }
}
@@ -1480,7 +1631,7 @@ const submitRefactorVm = async () => {
if (refactorForm.vnc_port) fd.append('vnc_port', refactorForm.vnc_port)
if (refactorForm.vnc_password) fd.append('vnc_password', refactorForm.vnc_password)
refactorSelectedNetworks.value.forEach(n => fd.append('network_ids', n.id))
if (refactorForm.internet_network_id) fd.append('internet_network_id', refactorForm.internet_network_id)
refactorSelectedInternalNetworks.value.forEach(n => fd.append('internet_network_id', n.id))
if (refactorForm.port_group_id) fd.append('port_group_id', refactorForm.port_group_id)
const res = await refactorVm(fd)
if (res?.data?.code === 200) { ElMessage.success('重构成功'); refactorDialogVisible.value = false; loadDetail() }
@@ -1517,6 +1668,53 @@ const submitUpdateTraffic = async () => {
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '修改失败')) } finally { actionLoading.value = false }
}
// ---- 迁移虚拟机 ----
const migrateDialogVisible = ref(false)
const migrateOptionsLoading = ref(false)
const migrateMode = ref('host')
const migrateHostOptions = ref([])
const migrateGroupOptions = ref([])
const migrateForm = reactive({ target_host_id: null, target_host_group_id: null, ipv4_num: 0, ipv6_num: 0 })
const handleMigrateVm = async () => {
Object.assign(migrateForm, { target_host_id: null, target_host_group_id: null, ipv4_num: 0, ipv6_num: 0 })
migrateMode.value = 'host'
migrateDialogVisible.value = true
migrateOptionsLoading.value = true
try {
const [hostRes, groupRes] = await Promise.all([
getRemoteHostList({ service_id: serviceId.value, page: 1, page_size: 10 }),
getRemoteHostGroupList({ service_id: serviceId.value, page: 1, page_size: 10 })
])
if (hostRes?.data?.code === 200 && hostRes?.data?.data) {
const inner = hostRes.data.data
migrateHostOptions.value = Array.isArray(inner) ? inner : (inner.hosts || inner.data || [])
}
if (groupRes?.data?.code === 200 && groupRes?.data?.data) {
const inner = groupRes.data.data
migrateGroupOptions.value = Array.isArray(inner) ? inner : (inner.host_groups || inner.data || [])
}
} catch { /* */ } finally { migrateOptionsLoading.value = false }
}
const submitMigrateVm = async () => {
if (migrateMode.value === 'host' && !migrateForm.target_host_id) { ElMessage.warning('请选择目标宿主机'); return }
if (migrateMode.value === 'group' && !migrateForm.target_host_group_id) { ElMessage.warning('请选择目标宿主机组'); return }
actionLoading.value = true
try {
const fd = new FormData()
fd.append('service_id', serviceId.value)
fd.append('vm_id', vmId.value)
if (migrateMode.value === 'host') fd.append('target_host_id', migrateForm.target_host_id)
else fd.append('target_host_group_id', migrateForm.target_host_group_id)
if (migrateForm.ipv4_num) fd.append('ipv4_num', migrateForm.ipv4_num)
if (migrateForm.ipv6_num) fd.append('ipv6_num', migrateForm.ipv6_num)
const res = await migrateVm(fd)
if (res?.data?.code === 200) { ElMessage.success('迁移成功'); migrateDialogVisible.value = false; loadDetail() }
else ElMessage.error(extractApiError(res?.data, '迁移失败'))
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '迁移失败')) } finally { actionLoading.value = false }
}
// ---- VNC 连接 ----
const vncDialogVisible = ref(false)
const vncNodeId = ref(null)
@@ -1560,7 +1758,7 @@ const sgOptions = ref([])
const loadSgOptions = async () => {
try {
const res = await getSecurityGroupList({ service_id: serviceId.value, page: 1, page_size: 200 })
const res = await getSecurityGroupList({ service_id: serviceId.value, page: 1, page_size: 10 })
if (res?.data?.code === 200 && res?.data?.data) {
const inner = res.data.data
sgOptions.value = inner.groups || inner.post_groups || inner.data || (Array.isArray(inner) ? inner : [])
@@ -1604,6 +1802,7 @@ const handleNetBindConfirm = async (selectedNetwork) => {
// ---- 网络操作(创建/编辑/删除/详情) ----
const netDialogVisible = ref(false)
const netDialogType = ref('add')
const netDialogSource = ref('')
const netFormRef = ref(null)
const netDetailVisible = ref(false)
const netDetailData = ref(null)
@@ -1615,11 +1814,23 @@ const netFormRules = {
type: [{ required: true, message: '请选择类型', trigger: 'change' }]
}
const handleNetCreate = () => {
const handleNetCreate = (source = '') => {
netDialogSource.value = source
netDialogType.value = 'add'
Object.assign(netForm, { id: 0, name: '', address: '', gateway: '', nameservers: '', type: 'bridge', mac_address: '', bridge_name: '', ls_bridge_name: '', ls_name: '', host_id: vmHostId.value })
netDialogVisible.value = true
}
const handleNetDialogCancel = () => {
netDialogVisible.value = false
const src = netDialogSource.value
netDialogSource.value = ''
if (src === 'edit') showEditNetworkSelector.value = true
else if (src === 'editInternal') showEditInternalNetworkSelector.value = true
else if (src === 'refactor') showRefactorNetworkSelector.value = true
else if (src === 'refactorInternal') showRefactorInternalNetworkSelector.value = true
else if (src === 'bind') showNetBindSelector.value = true
}
const handleNetEdit = (row) => {
netDialogType.value = 'edit'
Object.assign(netForm, { id: row.id, name: row.name || '', address: row.address || '', gateway: row.gateway || '', nameservers: row.nameservers || '', type: row.type || 'bridge', mac_address: row.mac_address || '', bridge_name: row.bridge_name || '', ls_bridge_name: row.ls_bridge_name || '', ls_name: row.ls_name || '', host_id: row.host_id || vmHostId.value })
@@ -1644,7 +1855,7 @@ const submitNetForm = () => {
if (netForm.ls_name) fd.append('ls_name', netForm.ls_name)
let res
if (netDialogType.value === 'add') { res = await createNetwork(fd) }
else { fd.append('network_id', netForm.id); res = await updateNetwork(fd) }
else { fd.append('id', netForm.id); res = await updateNetwork(fd) }
if (res?.data?.code === 200) { ElMessage.success(netDialogType.value === 'add' ? '创建成功' : '修改成功'); netDialogVisible.value = false; loadDetail() }
else ElMessage.error(extractApiError(res?.data, '操作失败'))
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '操作失败')) } finally { actionLoading.value = false }
@@ -1867,7 +2078,14 @@ const submitSgCreate = () => {
if (!valid) return
sgSubmitLoading.value = true
try {
const res = await createSecurityGroup({ service_id: serviceId.value, ...sgCreateForm })
const fd = new FormData()
fd.append('service_id', serviceId.value)
fd.append('name', sgCreateForm.name)
fd.append('host_id', sgCreateForm.host_id)
fd.append('direction', sgCreateForm.direction)
if (sgCreateForm.lock) fd.append('lock', true)
if (sgCreateForm.drop_all) fd.append('drop_all', true)
const res = await createSecurityGroup(fd)
if (res?.data?.code === 200) {
ElMessage.success('创建成功')
sgCreateDialogVisible.value = false
@@ -1894,7 +2112,8 @@ const submitSgSync = async () => {
if (!sgSyncHostId.value) { ElMessage.warning('请选择宿主机'); return }
sgSubmitLoading.value = true
try {
const res = await syncSecurityGroup({ service_id: serviceId.value, id: sgSyncTarget.value.id, host_id: sgSyncHostId.value })
const fd = new FormData(); fd.append('service_id', serviceId.value); fd.append('id', sgSyncTarget.value.id); fd.append('host_id', sgSyncHostId.value)
const res = await syncSecurityGroup(fd)
if (res?.data?.code === 200) {
ElMessage.success('同步成功')
sgSyncDialogVisible.value = false
@@ -1909,7 +2128,8 @@ const handleSgApply = (row) => {
confirmButtonText: '确定应用', cancelButtonText: '取消', type: 'info'
}).then(async () => {
try {
const res = await applySecurityGroup({ service_id: serviceId.value, id: row.id })
const fd = new FormData(); fd.append('service_id', serviceId.value); fd.append('id', row.id)
const res = await applySecurityGroup(fd)
if (res?.data?.code === 200) ElMessage.success('应用成功')
else ElMessage.error(extractApiError(res?.data, '应用失败'))
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '应用失败')) }
@@ -1918,7 +2138,7 @@ const handleSgApply = (row) => {
// 编辑(跳转安全组详情页)
const handleSgGoDetail = (row) => {
router.push({ path: '/virtualization/security-group-detail', query: { service_id: serviceId.value, service_name: serviceName.value, id: row.id } })
router.push({ path: '/virtualization/security-group-detail', query: { service_id: serviceId.value, service_name: serviceName.value, sg_id: row.id } })
}
// 白名单切换
@@ -1929,7 +2149,8 @@ const handleSgToggleWhitelist = (row) => {
}).then(async () => {
try {
const api = row.drop_all ? disableSecurityGroupWhitelist : enableSecurityGroupWhitelist
const res = await api({ service_id: serviceId.value, id: row.id })
const fd = new FormData(); fd.append('service_id', serviceId.value); fd.append('id', row.id)
const res = await api(fd)
if (res?.data?.code === 200) { ElMessage.success(`${action}成功`); loadDetail() }
else ElMessage.error(extractApiError(res?.data, `${action}失败`))
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, `${action}失败`)) }
@@ -1965,7 +2186,8 @@ const submitSgVmBind = async () => {
sgSubmitLoading.value = true
try {
const api = sgVmBindType.value === 'bind' ? bindSecurityGroup : unbindSecurityGroup
const res = await api({ service_id: serviceId.value, id: sgVmBindTarget.value.id, vm_id: vmId.value })
const fd = new FormData(); fd.append('service_id', serviceId.value); fd.append('id', sgVmBindTarget.value.id); fd.append('vm_id', vmId.value)
const res = await api(fd)
if (res?.data?.code === 200) {
ElMessage.success(sgVmBindType.value === 'bind' ? '绑定成功' : '解绑成功')
sgVmBindDialogVisible.value = false
@@ -1994,20 +2216,20 @@ const handleSgViewDetail = async (row) => {
const sgRuleDialogVisible = ref(false)
const sgRuleDialogType = ref('add')
const sgRuleFormRef = ref(null)
const sgRuleForm = reactive({ id: undefined, group_id: 0, protocol: 'tcp', action: 'allow', port_range: '', ip_range: '', priority: 0, port_group_id: 0 })
const sgRuleForm = reactive({ id: undefined, group_id: 0, protocol: 'tcp', action: 'allow', port_range: '', ip_range: '', priority: 0, port_group_id: '' })
const sgRuleRules = {
protocol: [{ required: true, message: '请选择协议', trigger: 'change' }],
action: [{ required: true, message: '请选择动作', trigger: 'change' }]
}
const handleSgAddRule = () => {
sgRuleDialogType.value = 'add'
Object.assign(sgRuleForm, { id: undefined, group_id: sgCurrentDetail.value?.id || 0, protocol: 'tcp', action: 'allow', port_range: '', ip_range: '', priority: 0, port_group_id: 0 })
Object.assign(sgRuleForm, { id: undefined, group_id: sgCurrentDetail.value?.id || 0, protocol: 'tcp', action: 'allow', port_range: '', ip_range: '', priority: 0, port_group_id: '' })
sgRuleDialogVisible.value = true
}
const handleSgEditRule = (rule) => {
sgRuleDialogType.value = 'edit'
Object.assign(sgRuleForm, {
id: rule.id, group_id: sgCurrentDetail.value?.id || 0, port_group_id: sgCurrentDetail.value?.id || 0,
id: rule.id, group_id: sgCurrentDetail.value?.id || 0, port_group_id: sgCurrentDetail.value?.id || '',
protocol: rule.protocol || 'tcp', action: rule.action || 'allow',
port_range: rule.port_range || '', ip_range: rule.ip_range || '', priority: rule.priority || 0
})
@@ -2018,9 +2240,19 @@ const submitSgRule = () => {
if (!valid) return
sgSubmitLoading.value = true
try {
const fd = new FormData()
fd.append('service_id', serviceId.value)
fd.append('group_id', sgRuleForm.group_id)
if (sgRuleForm.id) fd.append('id', sgRuleForm.id)
if (sgRuleForm.port_group_id) fd.append('port_group_id', sgRuleForm.port_group_id)
fd.append('protocol', sgRuleForm.protocol)
fd.append('action', sgRuleForm.action)
if (sgRuleForm.port_range) fd.append('port_range', sgRuleForm.port_range)
if (sgRuleForm.ip_range) fd.append('ip_range', sgRuleForm.ip_range)
fd.append('priority', sgRuleForm.priority || 0)
const res = sgRuleDialogType.value === 'add'
? await createSecurityGroupRule({ service_id: serviceId.value, ...sgRuleForm })
: await updateSecurityGroupRule({ service_id: serviceId.value, ...sgRuleForm })
? await createSecurityGroupRule(fd)
: await updateSecurityGroupRule(fd)
if (res?.data?.code === 200) {
ElMessage.success(sgRuleDialogType.value === 'add' ? '规则创建成功' : '规则修改成功')
sgRuleDialogVisible.value = false
@@ -2297,6 +2529,162 @@ const goBack = () => {
router.back()
}
// ---- 组网管理 ----
const vnLoading = ref(false)
const vnSubmitLoading = ref(false)
const vnList = ref([])
const vnJoinVisible = ref(false)
const vnJoinTarget = ref(null)
const vnJoinIp = ref('')
const vmBridgeNameMap = computed(() => {
const map = {}
for (const n of vmNetworks.value) {
if (n.bridge_name) map[n.bridge_name] = n
}
return map
})
const loadVmNetworkingList = async () => {
if (!detail.value) return
vnLoading.value = true
vnList.value = []
try {
const userId = detail.value.user_id
const hostId = vmHostId.value
if (!userId || !hostId) { vnLoading.value = false; return }
const res = await getUserNetworkingList({
service_id: serviceId.value, page: 1, count: 10,
host_id: hostId, user_id: userId
})
if (res?.data?.code === 200 && res?.data?.data) {
const inner = res.data.data
const networkings = Array.isArray(inner) ? inner : (inner.data || [])
const results = []
for (const nw of networkings) {
const matchedVmNet = nw.bridge_name ? vmBridgeNameMap.value[nw.bridge_name] : null
results.push({ networking: nw, network: matchedVmNet || null })
}
vnList.value = results
}
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '获取组网信息失败')) }
finally { vnLoading.value = false }
}
// 创建组网
const vnCreateVisible = ref(false)
const vnCreateFormRef = ref(null)
const vnCreateForm = reactive({ name: '', bridge_name: '', gateway: '' })
const vnCreateHostName = ref('')
const vnCreateUserName = ref('')
const vnCreateRules = {
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
bridge_name: [{ required: true, message: '请输入网桥名称', trigger: 'blur' }]
}
const handleVnCreate = async () => {
Object.assign(vnCreateForm, { name: '', bridge_name: '', gateway: '' })
vnCreateHostName.value = ''
vnCreateUserName.value = ''
vnCreateVisible.value = true
const userId = detail.value?.user_id
const hostId = vmHostId.value
const requests = []
if (userId) {
requests.push(
getUserInfo({ user_id: userId }).then(res => {
if (res?.data?.code === 200 && res?.data?.data) {
vnCreateUserName.value = res.data.data.UserName || ''
}
}).catch(() => {})
)
}
if (hostId) {
requests.push(
getRemoteHostDetail({ service_id: serviceId.value, id: hostId }).then(res => {
if (res?.data?.code === 200 && res?.data?.data) {
const h = res.data.data.host ?? res.data.data
vnCreateHostName.value = h.name || ''
}
}).catch(() => {})
)
}
await Promise.all(requests)
}
const submitVnCreate = () => {
vnCreateFormRef.value?.validate(async (valid) => {
if (!valid) return
vnSubmitLoading.value = true
try {
const fd = new FormData()
fd.append('service_id', serviceId.value)
fd.append('name', vnCreateForm.name)
fd.append('bridge_name', vnCreateForm.bridge_name)
fd.append('host_id', vmHostId.value)
fd.append('user_id', detail.value?.user_id || 0)
if (vnCreateForm.gateway) fd.append('gateway', vnCreateForm.gateway)
const res = await createUserNetworking(fd)
if (res?.data?.code === 200) {
ElMessage.success('创建组网成功')
vnCreateVisible.value = false
loadVmNetworkingList()
} else ElMessage.error(extractApiError(res?.data, '创建组网失败'))
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '创建组网失败')) }
finally { vnSubmitLoading.value = false }
})
}
const handleJoinNetworking = (row) => {
vnJoinTarget.value = row.networking
vnJoinIp.value = ''
vnJoinVisible.value = true
}
const submitJoinNetworking = async () => {
if (!vnJoinTarget.value) return
vnSubmitLoading.value = true
try {
const fd = new FormData()
fd.append('service_id', serviceId.value)
fd.append('networking_id', vnJoinTarget.value.id)
fd.append('vm_id', vmId.value)
if (vnJoinIp.value.trim()) fd.append('ip', vnJoinIp.value.trim())
const res = await assignUserNetworking(fd)
if (res?.data?.code === 200) {
ElMessage.success('加入组网成功')
vnJoinVisible.value = false
loadVmNetworkingList()
loadDetail()
} else ElMessage.error(extractApiError(res?.data, '加入组网失败'))
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '加入组网失败')) }
finally { vnSubmitLoading.value = false }
}
const handleLeaveNetworking = (row) => {
const netId = row.network?.id
const networkingId = row.networking?.id
if (!netId || !networkingId) { ElMessage.warning('缺少网络信息'); return }
ElMessageBox.confirm(
`确定要将该虚拟机从组网「${row.networking?.name || networkingId}」中移除?`,
'移除确认', { type: 'warning' }
).then(async () => {
try {
const fd = new FormData()
fd.append('service_id', serviceId.value)
fd.append('networking_id', networkingId)
fd.append('network_id', netId)
fd.append('vm_id', vmId.value)
const res = await removeUserNetworkingNetwork(fd)
if (res?.data?.code === 200) {
ElMessage.success('已移除')
loadVmNetworkingList()
loadDetail()
} else ElMessage.error(extractApiError(res?.data, '移除失败'))
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '移除失败')) }
}).catch(() => {})
}
let loadedVmId = null
const initPage = () => {
if (!vmId.value || loadedVmId === vmId.value) return
@@ -2316,6 +2704,7 @@ watch(activeTab, (tab) => {
else stopPolling()
if (tab === 'snapshot') { loadSnapshots(); loadSnapshotQuota() }
if (tab === 'backup') { loadBackups(); loadBackupQuota() }
if (tab === 'userNetworking') loadVmNetworkingList()
})
onActivated(() => {
isPageActive = true