445 lines
20 KiB
Vue
445 lines
20 KiB
Vue
<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>
|