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
+303 -4
View File
@@ -201,7 +201,136 @@
<el-tab-pane label="备份管理" name="backup">
<BackupManage v-if="hostTabLoaded['backup']" ref="backupManageRef" />
</el-tab-pane>
<el-tab-pane label="组网管理" name="networking">
<div class="section-block">
<div class="section-header">
<h3 class="section-title">用户组网列表</h3>
<div style="display: flex; gap: 8px; align-items: center">
<el-input v-model="nwFilterUserId" placeholder="按用户ID筛选" style="width: 140px" size="small" clearable @clear="loadNetworkingList" @keyup.enter="loadNetworkingList" />
<el-input v-model="nwKeyword" placeholder="关键词搜索" style="width: 160px" size="small" clearable @clear="loadNetworkingList" @keyup.enter="loadNetworkingList" />
<el-button size="small" :icon="Search" @click="loadNetworkingList">搜索</el-button>
<el-button size="small" type="primary" @click="handleNwCreate">创建组网</el-button>
<el-button size="small" :icon="Refresh" @click="loadNetworkingList" :loading="nwLoading">刷新</el-button>
</div>
</div>
<el-table :data="nwList" v-loading="nwLoading" stripe size="small">
<el-table-column prop="id" label="ID" width="60" />
<el-table-column prop="name" label="名称" min-width="140" show-overflow-tooltip />
<el-table-column prop="user_id" label="用户ID" width="80" />
<el-table-column prop="host_id" label="宿主机ID" width="90" />
<el-table-column label="网桥" width="120">
<template #default="{ row }">{{ row.bridge_name || '-' }}</template>
</el-table-column>
<el-table-column label="网关" min-width="140">
<template #default="{ row }"><span class="mono-text">{{ row.gateway || '-' }}</span></template>
</el-table-column>
<el-table-column label="创建时间" width="170">
<template #default="{ row }">{{ formatTimestamp(row.created_at) }}</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="handleNwDetail(row)">详情</el-button>
<el-button link type="success" size="small" @click="handleNwAssign(row)">分配IP</el-button>
<el-button link type="danger" size="small" @click="handleNwDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!nwList.length && !nwLoading" description="暂无组网" :image-size="60" />
<div class="pagination-wrapper" v-if="nwTotal > 0">
<el-pagination v-model:current-page="nwPage" v-model:page-size="nwPageSize"
:page-sizes="[10, 20, 50]" :total="nwTotal" layout="total, sizes, prev, pager, next" small
@size-change="s => { nwPageSize = s; nwPage = 1; loadNetworkingList() }"
@current-change="p => { nwPage = p; loadNetworkingList() }" />
</div>
</div>
</el-tab-pane>
</el-tabs>
<!-- 组网详情弹窗 -->
<el-dialog v-model="nwDetailVisible" title="组网详情" width="800px" destroy-on-close>
<el-descriptions :column="2" border v-if="nwDetailData" style="margin-bottom: 16px">
<el-descriptions-item label="ID">{{ nwDetailData.id }}</el-descriptions-item>
<el-descriptions-item label="名称">{{ nwDetailData.name }}</el-descriptions-item>
<el-descriptions-item label="用户ID">{{ nwDetailData.user_id }}</el-descriptions-item>
<el-descriptions-item label="宿主机ID">{{ nwDetailData.host_id }}</el-descriptions-item>
<el-descriptions-item label="网桥">{{ nwDetailData.bridge_name || '-' }}</el-descriptions-item>
<el-descriptions-item label="网关">{{ nwDetailData.gateway || '-' }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ formatTimestamp(nwDetailData.created_at) }}</el-descriptions-item>
<el-descriptions-item label="更新时间">{{ formatTimestamp(nwDetailData.updated_at) }}</el-descriptions-item>
</el-descriptions>
<h4 style="margin: 0 0 12px">组网下的网络 (已分配)</h4>
<el-table :data="nwDetailNetworks" size="small" stripe>
<el-table-column label="网络ID" width="70">
<template #default="{ row }">{{ row.network?.id || '-' }}</template>
</el-table-column>
<el-table-column label="IP地址" min-width="150">
<template #default="{ row }"><span class="mono-text">{{ row.network?.address || '-' }}</span></template>
</el-table-column>
<el-table-column label="MAC地址" min-width="160">
<template #default="{ row }"><span class="mono-text">{{ row.network?.mac_address || '-' }}</span></template>
</el-table-column>
<el-table-column label="类型" width="80">
<template #default="{ row }"><el-tag size="small">{{ row.network?.type || '-' }}</el-tag></template>
</el-table-column>
<el-table-column label="虚拟机ID" width="90">
<template #default="{ row }">{{ row.network?.vm_id || '-' }}</template>
</el-table-column>
<el-table-column label="操作" width="80">
<template #default="{ row }">
<el-button link type="danger" size="small" @click="handleNwRemoveNet(row)">移除</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!nwDetailNetworks.length" description="暂无分配的网络" :image-size="50" />
<template #footer><el-button @click="nwDetailVisible = false">关闭</el-button></template>
</el-dialog>
<!-- 创建组网弹窗 -->
<el-dialog v-model="nwCreateVisible" title="创建组网" width="480px" destroy-on-close>
<el-form ref="nwCreateFormRef" :model="nwCreateForm" :rules="nwCreateRules" label-width="100px">
<el-form-item label="用户" prop="user_id">
<div style="display: flex; gap: 8px; width: 100%">
<el-input :model-value="nwCreateForm.user_id ? `${nwCreateUserName} (ID: ${nwCreateForm.user_id})` : '未选择'" disabled style="flex: 1" />
<el-button type="primary" @click="showNwUserSelector = true">选择</el-button>
<el-button v-if="nwCreateForm.user_id" @click="nwCreateForm.user_id = 0; nwCreateUserName = ''">清除</el-button>
</div>
</el-form-item>
<el-form-item label="网桥名称">
<el-input v-model="nwCreateForm.bridge_name" placeholder="可选" />
</el-form-item>
<el-form-item label="网关">
<el-input v-model="nwCreateForm.gateway" placeholder="可选 10.0.0.1" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="nwCreateVisible = false">取消</el-button>
<el-button type="primary" :loading="nwSubmitLoading" @click="submitNwCreate">创建</el-button>
</template>
</el-dialog>
<!-- 分配IP弹窗 -->
<el-dialog v-model="nwAssignVisible" title="为虚拟机分配组网IP" width="480px" destroy-on-close>
<el-form label-width="100px">
<el-form-item label="组网">{{ nwAssignTarget?.name || '-' }} (ID: {{ nwAssignTarget?.id }})</el-form-item>
<el-form-item label="虚拟机" required>
<div style="display: flex; gap: 8px; width: 100%">
<el-input :model-value="nwAssignVmId ? `${nwAssignVmName} (ID: ${nwAssignVmId})` : '未选择'" disabled style="flex: 1" />
<el-button type="primary" @click="showNwVmSelector = true">选择</el-button>
</div>
</el-form-item>
<el-form-item label="指定IP">
<el-input v-model="nwAssignIp" placeholder="留空自动分配" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="nwAssignVisible = false">取消</el-button>
<el-button type="primary" :loading="nwSubmitLoading" @click="submitNwAssign" :disabled="!nwAssignVmId">分配</el-button>
</template>
</el-dialog>
<UserListSelector v-model="showNwUserSelector" @confirm="handleNwUserSelected" />
<VmSelectorPopup v-model="showNwVmSelector" :service-id="serviceId" :host-id="hostId" @confirm="handleNwVmSelected" />
</div>
<!-- 编辑弹窗 -->
@@ -267,9 +396,11 @@
import { ref, reactive, computed, onMounted, onActivated, onDeactivated, onBeforeUnmount, watch, nextTick, provide } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ArrowLeft, Refresh, Edit, Delete, Monitor, Coin, Box, Connection } from '@element-plus/icons-vue'
import { ArrowLeft, Refresh, Edit, Delete, Monitor, Coin, Box, Connection, Search, Plus } from '@element-plus/icons-vue'
import {
getRemoteHostDetail, getRemoteHostMetrics, updateRemoteHost, deleteRemoteHost
getRemoteHostDetail, getRemoteHostMetrics, updateRemoteHost, deleteRemoteHost,
getUserNetworkingList, getUserNetworkingDetail, createUserNetworking, deleteUserNetworking,
assignUserNetworking, removeUserNetworkingNetwork
} from '@/api/admin/kvmService'
import { extractApiError } from '@/utils/kvmErrorUtil'
import HostGroupSelectorPopup from '@/components/admin/HostGroupSelectorPopup.vue'
@@ -280,6 +411,8 @@ import VmManage from '@/views/virtualization/VmManage.vue'
import SnapshotManage from '@/views/virtualization/SnapshotManage.vue'
import BackupManage from '@/views/virtualization/BackupManage.vue'
import { useTagsViewStore } from '@/store/tagsViewStore'
import UserListSelector from '@/components/admin/UserListSelector.vue'
import VmSelectorPopup from '@/components/admin/VmSelectorPopup.vue'
import * as echarts from 'echarts'
const route = useRoute()
@@ -291,7 +424,7 @@ const serviceName = computed(() => route.query.service_name || '')
const hostId = computed(() => parseInt(route.query.id) || 0)
const activeTab = ref('info')
const hostTabLoaded = reactive({ image: false, network: false, volume: false, vm: false, snapshot: false, backup: false })
const hostTabLoaded = reactive({ image: false, network: false, volume: false, vm: false, snapshot: false, backup: false, networking: false })
const imageManageRef = ref(null)
const networkManageRef = ref(null)
@@ -302,7 +435,7 @@ const backupManageRef = ref(null)
const tabRefMap = { image: imageManageRef, network: networkManageRef, volume: volumeManageRef, vm: vmManageRef, snapshot: snapshotManageRef, backup: backupManageRef }
watch(activeTab, (tab) => {
if (!['info', 'monitor'].includes(tab)) {
if (!['info', 'monitor', 'networking'].includes(tab)) {
if (!hostTabLoaded[tab]) {
hostTabLoaded[tab] = true
} else {
@@ -311,6 +444,7 @@ watch(activeTab, (tab) => {
}
if (tab === 'monitor' && detail.value) { loadMetrics(); startPolling() }
else stopPolling()
if (tab === 'networking') loadNetworkingList()
})
const loading = ref(false)
@@ -607,6 +741,170 @@ const goBack = () => {
router.push({ path: '/virtualization/kvm-service-detail', query: { service_id: serviceId.value, service_name: serviceName.value } })
}
// ---- 组网管理 ----
const nwLoading = ref(false)
const nwSubmitLoading = ref(false)
const nwList = ref([])
const nwTotal = ref(0)
const nwPage = ref(1)
const nwPageSize = ref(10)
const nwFilterUserId = ref('')
const nwKeyword = ref('')
const nwDetailVisible = ref(false)
const nwDetailData = ref(null)
const nwDetailNetworks = ref([])
const nwDetailLoading = ref(false)
const nwCreateVisible = ref(false)
const nwCreateFormRef = ref(null)
const nwCreateForm = reactive({ user_id: 0, bridge_name: '', gateway: '' })
const nwCreateUserName = ref('')
const showNwUserSelector = ref(false)
const nwCreateRules = {
user_id: [{ required: true, message: '请选择用户', trigger: 'change', type: 'number', min: 1 }]
}
const nwAssignVisible = ref(false)
const nwAssignTarget = ref(null)
const nwAssignVmId = ref(0)
const nwAssignVmName = ref('')
const nwAssignIp = ref('')
const showNwVmSelector = ref(false)
const loadNetworkingList = async () => {
nwLoading.value = true
try {
const params = {
service_id: serviceId.value,
page: nwPage.value,
count: nwPageSize.value,
host_id: hostId.value
}
if (nwFilterUserId.value) params.user_id = parseInt(nwFilterUserId.value)
if (nwKeyword.value) params.keyword = nwKeyword.value
const res = await getUserNetworkingList(params)
if (res?.data?.code === 200 && res?.data?.data) {
const inner = res.data.data
nwList.value = Array.isArray(inner) ? inner : (inner.data || [])
nwTotal.value = inner.meta?.count ?? inner.total ?? nwList.value.length
}
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '获取组网列表失败')) }
finally { nwLoading.value = false }
}
const handleNwDetail = async (row) => {
nwDetailVisible.value = true
nwDetailData.value = row
nwDetailNetworks.value = []
nwDetailLoading.value = true
try {
const res = await getUserNetworkingDetail({ service_id: serviceId.value, networking_id: row.id })
if (res?.data?.code === 200 && res?.data?.data) {
const inner = res.data.data
nwDetailData.value = inner.data ?? inner
nwDetailNetworks.value = inner.networks || []
}
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '获取详情失败')) }
finally { nwDetailLoading.value = false }
}
const handleNwCreate = () => {
Object.assign(nwCreateForm, { user_id: 0, bridge_name: '', gateway: '' })
nwCreateUserName.value = ''
nwCreateVisible.value = true
}
const handleNwUserSelected = (user) => {
nwCreateForm.user_id = user.user_id || user.id
nwCreateUserName.value = user.user_name || user.name || ''
}
const submitNwCreate = async () => {
if (!nwCreateForm.user_id) { ElMessage.warning('请选择用户'); return }
nwSubmitLoading.value = true
try {
const fd = new FormData()
fd.append('service_id', serviceId.value)
fd.append('host_id', hostId.value)
fd.append('user_id', nwCreateForm.user_id)
if (nwCreateForm.bridge_name) fd.append('bridge_name', nwCreateForm.bridge_name)
if (nwCreateForm.gateway) fd.append('gateway', nwCreateForm.gateway)
const res = await createUserNetworking(fd)
if (res?.data?.code === 200) {
ElMessage.success('创建成功')
nwCreateVisible.value = false
loadNetworkingList()
} else ElMessage.error(extractApiError(res?.data, '创建失败'))
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '创建失败')) }
finally { nwSubmitLoading.value = false }
}
const handleNwDelete = (row) => {
ElMessageBox.confirm(`确定删除组网「${row.name || row.id}」?该操作不可撤销。`, '删除确认', { type: 'warning' })
.then(async () => {
try {
const res = await deleteUserNetworking({ service_id: serviceId.value, networking_id: row.id })
if (res?.data?.code === 200) { ElMessage.success('已删除'); loadNetworkingList() }
else ElMessage.error(extractApiError(res?.data, '删除失败'))
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '删除失败')) }
}).catch(() => {})
}
const handleNwAssign = (row) => {
nwAssignTarget.value = row
nwAssignVmId.value = 0
nwAssignVmName.value = ''
nwAssignIp.value = ''
nwAssignVisible.value = true
}
const handleNwVmSelected = (vm) => {
nwAssignVmId.value = vm.id
nwAssignVmName.value = vm.name || ''
}
const submitNwAssign = async () => {
if (!nwAssignVmId.value || !nwAssignTarget.value) { ElMessage.warning('请选择虚拟机'); return }
nwSubmitLoading.value = true
try {
const fd = new FormData()
fd.append('service_id', serviceId.value)
fd.append('networking_id', nwAssignTarget.value.id)
fd.append('vm_id', nwAssignVmId.value)
if (nwAssignIp.value.trim()) fd.append('ip', nwAssignIp.value.trim())
const res = await assignUserNetworking(fd)
if (res?.data?.code === 200) {
ElMessage.success('分配成功')
nwAssignVisible.value = false
if (nwDetailVisible.value && nwDetailData.value?.id === nwAssignTarget.value.id) {
handleNwDetail(nwAssignTarget.value)
}
} else ElMessage.error(extractApiError(res?.data, '分配失败'))
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '分配失败')) }
finally { nwSubmitLoading.value = false }
}
const handleNwRemoveNet = (netItem) => {
const net = netItem.network || netItem
if (!net.id || !nwDetailData.value?.id) { ElMessage.warning('缺少网络信息'); return }
ElMessageBox.confirm(`确定移除网络 (ID: ${net.id}, VM: ${net.vm_id || '-'}) `, '移除确认', { type: 'warning' })
.then(async () => {
try {
const fd = new FormData()
fd.append('service_id', serviceId.value)
fd.append('networking_id', nwDetailData.value.id)
fd.append('network_id', net.id)
fd.append('vm_id', net.vm_id || 0)
const res = await removeUserNetworkingNetwork(fd)
if (res?.data?.code === 200) {
ElMessage.success('已移除')
handleNwDetail(nwDetailData.value)
} else ElMessage.error(extractApiError(res?.data, '移除失败'))
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '移除失败')) }
}).catch(() => {})
}
let loadedHostId = null
const initPage = () => {
@@ -694,4 +992,5 @@ onBeforeUnmount(() => { isPageActive = false; stopPolling(); disposeCharts() })
.unit-input-row { display: flex; gap: 6px; width: 100%; }
.wide-number { flex: 1; min-width: 140px; }
.pagination-wrapper { display: flex; justify-content: flex-end; margin-top: 16px; }
</style>