fix: 重构虚拟机内网外网参数设置选择网络
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user