Files
ApiServer-Web-admin_dashboa…/src/views/virtualization/NetworkManage.vue
T
lin b3ed406f84
Build and Deploy Vue3 / build (push) Successful in 1m31s
Build and Deploy Vue3 / deploy (push) Successful in 1m9s
fix: 提交修改
2026-04-15 16:02:36 +08:00

445 lines
20 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="network-manage-container">
<div class="page-header" v-if="!embedded">
<div class="header-left">
<el-button @click="goBack" :icon="ArrowLeft">返回</el-button>
<div class="header-info">
<h3>网络管理</h3>
<span class="sub-info" v-if="serviceName">主控服务{{ serviceName }} | 宿主机{{ selectedHostName || '请选择' }}</span>
</div>
</div>
<div class="header-right">
<el-button type="primary" @click="handleAdd"><el-icon><Plus /></el-icon>创建网络</el-button>
<el-button type="success" @click="handleBatchAdd">批量创建</el-button>
<el-button @click="loadList"><el-icon><Refresh /></el-icon>刷新</el-button>
</div>
</div>
<div class="embedded-toolbar" v-if="embedded">
<el-button type="primary" @click="handleAdd"><el-icon><Plus /></el-icon>创建网络</el-button>
<el-button type="success" @click="handleBatchAdd">批量创建</el-button>
<el-button @click="loadList"><el-icon><Refresh /></el-icon>刷新</el-button>
</div>
<!-- 筛选 -->
<div class="filter-bar">
<el-input v-model="keyword" placeholder="搜索网络" clearable style="width: 220px" @keyup.enter="handleSearch" @clear="handleSearch">
<template #prefix><el-icon><Search /></el-icon></template>
</el-input>
<el-select v-model="filterType" placeholder="网络类型" clearable style="width: 130px" @change="handleSearch">
<el-option label="网桥(Bridge)" value="bridge" />
<el-option label="内网(NAT)" value="nat" />
</el-select>
<el-select v-model="filterIpVersion" placeholder="IP版本" clearable style="width: 120px" @change="handleSearch">
<el-option label="IPv4" value="ipv4" />
<el-option label="IPv6" value="ipv6" />
</el-select>
</div>
<!-- 网络列表 -->
<el-table :data="networkList" v-loading="loading" stripe>
<el-table-column prop="id" label="ID" width="70" />
<el-table-column prop="name" label="名称" min-width="140" show-overflow-tooltip />
<el-table-column label="类型" width="100">
<template #default="{ row }">
<el-tag :type="row.type === 'bridge' ? 'success' : 'warning'" size="small">
{{ row.type === 'bridge' ? '网桥' : 'NAT' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="address" label="地址(CIDR)" min-width="160" show-overflow-tooltip />
<el-table-column prop="gateway" label="网关" width="140" />
<el-table-column prop="nameservers" label="DNS" min-width="140" show-overflow-tooltip />
<el-table-column prop="bridge_name" label="网桥名称" width="120" show-overflow-tooltip />
<el-table-column prop="target_device" label="目标设备" width="120" show-overflow-tooltip />
<el-table-column label="宿主机" width="140">
<template #default="{ row }">{{ getHostLabel(row.host_id) }}</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="handleViewDetail(row)">详情</el-button>
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-wrapper" v-if="total > 0">
<el-pagination v-model:current-page="queryParams.page" v-model:page-size="queryParams.page_size"
:page-sizes="[10, 20, 50]" :total="total" layout="total, sizes, prev, pager, next"
@size-change="s => { queryParams.page_size = s; queryParams.page = 1; loadList() }"
@current-change="p => { queryParams.page = p; loadList() }" />
</div>
<!-- 新建/编辑弹窗 -->
<el-dialog v-model="dialogVisible" :title="dialogType === 'add' ? '创建网络' : '编辑网络'" width="600px" destroy-on-close class="tk-dialog">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
<div class="tk-section">
<div class="tk-section-title">基本信息</div>
<el-form-item label="名称" prop="name">
<el-input v-model="formData.name" placeholder="网络名称" />
</el-form-item>
<el-form-item label="宿主机" prop="host_id">
<el-select v-model="formData.host_id" placeholder="选择宿主机" filterable style="width: 100%">
<el-option v-for="h in hostOptions" :key="h.id" :label="`${h.name} (${h.ip || h.id})`" :value="h.id" />
</el-select>
</el-form-item>
<el-form-item label="网络类型" prop="type">
<el-select v-model="formData.type" style="width: 100%">
<el-option label="网桥(Bridge/外网)" value="bridge" />
<el-option label="内网(NAT)" value="nat" />
</el-select>
</el-form-item>
<el-form-item label="IP(CIDR)" prop="address">
<el-input v-model="formData.address" placeholder="例如 192.168.1.0/24" />
</el-form-item>
<el-form-item label="网关地址" prop="gateway">
<el-input v-model="formData.gateway" placeholder="例如 192.168.1.1" />
</el-form-item>
<el-form-item label="DNS 服务器">
<el-input v-model="formData.nameservers" placeholder="默认 114.114.114.114,8.8.8.8" />
</el-form-item>
</div>
<div class="tk-section">
<div class="tk-section-title">高级配置</div>
<el-form-item label="MAC 地址">
<el-input v-model="formData.mac_address" placeholder="不填则随机" />
</el-form-item>
<el-form-item label="虚拟网桥名">
<el-input v-model="formData.bridge_name" placeholder="不填使用默认" />
</el-form-item>
<el-form-item label="逻辑网桥名">
<el-input v-model="formData.ls_bridge_name" placeholder="不填使用默认" />
</el-form-item>
<el-form-item label="逻辑端口名">
<el-input v-model="formData.ls_name" placeholder="不填使用默认" />
</el-form-item>
</div>
</el-form>
<template #footer>
<div class="tk-dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
</div>
</template>
</el-dialog>
<!-- 详情弹窗 -->
<el-dialog v-model="detailVisible" title="网络详情" width="600px" destroy-on-close>
<el-descriptions :column="2" border v-if="currentDetail">
<el-descriptions-item label="ID">{{ currentDetail.id }}</el-descriptions-item>
<el-descriptions-item label="名称">{{ currentDetail.name }}</el-descriptions-item>
<el-descriptions-item label="类型">
<el-tag :type="currentDetail.type === 'bridge' ? 'success' : 'warning'" size="small">
{{ currentDetail.type === 'bridge' ? '网桥' : 'NAT' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="宿主机">{{ getHostLabel(currentDetail.host_id) }}</el-descriptions-item>
<el-descriptions-item label="地址(CIDR)">{{ currentDetail.address }}</el-descriptions-item>
<el-descriptions-item label="网关">{{ currentDetail.gateway }}</el-descriptions-item>
<el-descriptions-item label="DNS">{{ currentDetail.nameservers || '-' }}</el-descriptions-item>
<el-descriptions-item label="MAC 地址">{{ currentDetail.mac_address || '-' }}</el-descriptions-item>
<el-descriptions-item label="虚拟网桥">{{ currentDetail.bridge_name || '-' }}</el-descriptions-item>
<el-descriptions-item label="逻辑网桥">{{ currentDetail.ls_bridge_name || '-' }}</el-descriptions-item>
<el-descriptions-item label="逻辑端口">{{ currentDetail.ls_name || '-' }}</el-descriptions-item>
<el-descriptions-item label="目标设备">{{ currentDetail.target_device || '-' }}</el-descriptions-item>
</el-descriptions>
<template #footer><el-button @click="detailVisible = false">关闭</el-button></template>
</el-dialog>
<!-- 批量创建弹窗 -->
<el-dialog v-model="batchDialogVisible" title="批量创建网络" width="560px" destroy-on-close class="tk-dialog">
<el-form ref="batchFormRef" :model="batchForm" :rules="batchFormRules" label-width="100px">
<el-alert type="info" :closable="false" show-icon style="margin-bottom: 16px">通过指定 IP 范围(start_ip ~ end_ip)批量创建网络条目</el-alert>
<div class="tk-section">
<div class="tk-section-title">IP 范围</div>
<el-form-item label="宿主机" prop="host_id">
<el-select v-model="batchForm.host_id" placeholder="选择宿主机" filterable style="width: 100%">
<el-option v-for="h in hostOptions" :key="h.id" :label="`${h.name} (${h.ip || h.id})`" :value="h.id" />
</el-select>
</el-form-item>
<el-form-item label="起始IP" prop="start_ip">
<el-input v-model="batchForm.start_ip" placeholder=" 192.168.1.10" />
</el-form-item>
<el-form-item label="结束IP" prop="end_ip">
<el-input v-model="batchForm.end_ip" placeholder=" 192.168.1.50" />
</el-form-item>
</div>
<div class="tk-section">
<div class="tk-section-title">网络配置</div>
<el-form-item label="网关">
<el-input v-model="batchForm.gateway" placeholder="可选 192.168.1.1" />
</el-form-item>
<el-form-item label="子网掩码">
<el-input v-model="batchForm.mask" placeholder="可选 24" />
</el-form-item>
<el-form-item label="DNS">
<el-input v-model="batchForm.nameservers" placeholder="可选 114.114.114.114,8.8.8.8" />
</el-form-item>
<el-form-item label="网桥名称">
<el-input v-model="batchForm.bridge_name" placeholder="可选" />
</el-form-item>
<el-form-item label="网络类型">
<el-select v-model="batchForm.type" style="width: 100%">
<el-option label="网桥(Bridge)" value="bridge" />
<el-option label="内网(NAT)" value="nat" />
</el-select>
</el-form-item>
</div>
</el-form>
<template #footer>
<div class="tk-dialog-footer">
<el-button @click="batchDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleBatchSubmit">确定创建</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, computed, inject, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Refresh, Search, ArrowLeft } from '@element-plus/icons-vue'
import { getRemoteHostList, getNetworkList, getNetworkDetail, createNetwork, batchCreateNetwork, updateNetwork, deleteNetwork } from '@/api/admin/kvmService'
import { extractApiError } from '@/utils/kvmErrorUtil'
const route = useRoute()
const router = useRouter()
const embedded = inject('embedded', false)
const injectedServiceId = inject('serviceId', null)
const injectedServiceName = inject('serviceName', null)
const injectedHostId = inject('hostId', null)
const serviceId = computed(() => injectedServiceId?.value || parseInt(route.query.service_id) || 0)
const hostId = computed(() => injectedHostId?.value || parseInt(route.query.host_id) || 0)
const serviceName = computed(() => injectedServiceName?.value || route.query.service_name || '')
const loading = ref(false)
const submitLoading = ref(false)
const networkList = ref([])
const total = ref(0)
const keyword = ref('')
const filterType = ref('')
const filterIpVersion = ref('')
const hostIdInput = ref(0)
const hostOptions = ref([])
const queryParams = reactive({ page: 1, page_size: 10 })
const selectedHostName = computed(() => {
const h = hostOptions.value.find(x => x.id === hostIdInput.value)
return h ? `${h.name} (${h.ip || h.id})` : (hostIdInput.value || '')
})
const getHostLabel = (hid) => {
const h = hostOptions.value.find(x => x.id === hid)
return h ? `${h.name}` : (hid || '-')
}
const loadHostOptions = async () => {
try {
const res = await getRemoteHostList({ service_id: serviceId.value, page: 1, page_size: 10 })
const body = res?.data
if (body?.code === 200 && body?.data) {
const inner = body.data
hostOptions.value = Array.isArray(inner) ? inner : (inner.hosts || inner.list || inner.data || [])
}
} catch (e) { console.error('加载宿主机列表失败:', e) }
}
const dialogVisible = ref(false)
const dialogType = ref('add')
const formRef = ref(null)
const detailVisible = ref(false)
const currentDetail = ref(null)
const formData = reactive({
id: undefined, name: '', address: '', gateway: '', nameservers: '',
type: 'bridge', mac_address: '', bridge_name: '', ls_bridge_name: '', ls_name: '', host_id: 0
})
const formRules = {
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
address: [{ required: true, message: '请输入IP地址(CIDR)', trigger: 'blur' }],
gateway: [{ required: true, message: '请输入网关', trigger: 'blur' }],
type: [{ required: true, message: '请选择类型', trigger: 'change' }],
host_id: [{ required: true, message: '请选择宿主机', trigger: 'change' }]
}
const loadList = async () => {
if (!serviceId.value) return
const hid = hostIdInput.value || hostId.value
if (!hid) { ElMessage.warning('请先选择宿主机'); return }
loading.value = true
try {
const params = { service_id: serviceId.value, host_id: hid, page: queryParams.page, count: queryParams.page_size }
if (keyword.value) params.key = keyword.value
if (filterType.value) params.type = filterType.value
if (filterIpVersion.value) params.ip_version = filterIpVersion.value
const res = await getNetworkList(params)
const body = res?.data
if (body?.code === 200 && body?.data) {
const inner = body.data
networkList.value = inner.data || []
total.value = inner.meta?.count ?? inner.all_count ?? 0
} else {
networkList.value = []
total.value = 0
}
} catch (e) {
console.error('获取网络列表失败:', e)
ElMessage.error('获取网络列表失败')
} finally { loading.value = false }
}
const handleSearch = () => { queryParams.page = 1; loadList() }
const handleAdd = () => {
dialogType.value = 'add'
Object.assign(formData, { id: undefined, name: '', address: '', gateway: '', nameservers: '', type: 'bridge', mac_address: '', bridge_name: '', ls_bridge_name: '', ls_name: '', host_id: hostIdInput.value || hostId.value || 0 })
dialogVisible.value = true
}
const handleEdit = (row) => {
dialogType.value = 'edit'
Object.assign(formData, {
id: row.id, name: row.name, address: row.address, gateway: row.gateway,
nameservers: row.nameservers || '', type: row.type, 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
})
dialogVisible.value = true
}
const handleSubmit = () => {
formRef.value?.validate(async (valid) => {
if (!valid) return
submitLoading.value = true
try {
const fd = new FormData()
fd.append('service_id', serviceId.value)
fd.append('name', formData.name)
fd.append('address', formData.address)
fd.append('gateway', formData.gateway)
fd.append('type', formData.type)
fd.append('host_id', formData.host_id)
if (formData.nameservers) fd.append('nameservers', formData.nameservers)
if (formData.mac_address) fd.append('mac_address', formData.mac_address)
if (formData.bridge_name) fd.append('bridge_name', formData.bridge_name)
if (formData.ls_bridge_name) fd.append('ls_bridge_name', formData.ls_bridge_name)
if (formData.ls_name) fd.append('ls_name', formData.ls_name)
let res
if (dialogType.value === 'add') {
res = await createNetwork(fd)
} else {
fd.append('id', formData.id)
res = await updateNetwork(fd)
}
if (res?.data?.code === 200) {
ElMessage.success(dialogType.value === 'add' ? '创建成功' : '修改成功')
dialogVisible.value = false
loadList()
} else {
ElMessage.error(extractApiError(res?.data, '操作失败'))
}
} catch (e) {
ElMessage.error(extractApiError(e?.response?.data, '操作失败'))
} finally { submitLoading.value = false }
})
}
const handleViewDetail = async (row) => {
detailVisible.value = true
currentDetail.value = row
try {
const res = await getNetworkDetail({ service_id: serviceId.value, network_id: row.id, host_id: row.host_id })
if (res?.data?.code === 200 && res?.data?.data) {
const d = res.data.data
currentDetail.value = d.network ?? d.data ?? d
}
} catch { /* fallback */ }
}
const handleDelete = (row) => {
ElMessageBox.confirm(`确定要删除网络「${row.name}」吗?`, '删除确认', {
confirmButtonText: '确定删除', cancelButtonText: '取消', type: 'warning'
}).then(async () => {
try {
const res = await deleteNetwork({ service_id: serviceId.value, network_id: row.id, host_id: row.host_id })
if (res?.data?.code === 200) { ElMessage.success('删除成功'); loadList() }
else ElMessage.error(extractApiError(res?.data, '删除失败'))
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '删除失败')) }
}).catch(() => {})
}
// ---- 批量创建网络 ----
const batchDialogVisible = ref(false)
const batchFormRef = ref(null)
const batchForm = reactive({
host_id: 0, start_ip: '', end_ip: '', gateway: '', mask: '',
nameservers: '', bridge_name: '', type: 'bridge'
})
const batchFormRules = {
host_id: [{ required: true, message: '请选择宿主机', trigger: 'change' }],
start_ip: [{ required: true, message: '请输入起始IP', trigger: 'blur' }],
end_ip: [{ required: true, message: '请输入结束IP', trigger: 'blur' }]
}
const handleBatchAdd = () => {
Object.assign(batchForm, {
host_id: hostIdInput.value || hostId.value || 0,
start_ip: '', end_ip: '', gateway: '', mask: '',
nameservers: '', bridge_name: '', type: 'bridge'
})
batchDialogVisible.value = true
}
const handleBatchSubmit = () => {
batchFormRef.value?.validate(async (valid) => {
if (!valid) return
submitLoading.value = true
try {
const fd = new FormData()
fd.append('service_id', serviceId.value)
fd.append('host_id', batchForm.host_id)
fd.append('start_ip', batchForm.start_ip)
fd.append('end_ip', batchForm.end_ip)
if (batchForm.gateway) fd.append('gateway', batchForm.gateway)
if (batchForm.mask) fd.append('mask', batchForm.mask)
if (batchForm.nameservers) fd.append('nameservers', batchForm.nameservers)
if (batchForm.bridge_name) fd.append('bridge_name', batchForm.bridge_name)
if (batchForm.type) fd.append('type', batchForm.type)
const res = await batchCreateNetwork(fd)
if (res?.data?.code === 200) {
ElMessage.success('批量创建成功')
batchDialogVisible.value = false
loadList()
} else {
ElMessage.error(extractApiError(res?.data, '批量创建失败'))
}
} catch (e) {
ElMessage.error(extractApiError(e?.response?.data, '批量创建失败'))
} finally { submitLoading.value = false }
})
}
const goBack = () => { router.push('/virtualization/kvm-service') }
onMounted(async () => {
if (serviceId.value) {
await loadHostOptions()
if (hostId.value) {
hostIdInput.value = hostId.value
} else if (hostOptions.value.length > 0) {
hostIdInput.value = hostOptions.value[0].id
}
if (hostIdInput.value) loadList()
}
})
defineExpose({ loadList })
</script>
<style scoped>
.network-manage-container { padding: 20px; }
</style>