feat: 对接主控服务接口
This commit is contained in:
@@ -0,0 +1,331 @@
|
||||
<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 @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 @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="hostIdInput" placeholder="选择宿主机" clearable filterable style="width: 220px" @change="handleSearch">
|
||||
<el-option v-for="h in hostOptions" :key="h.id" :label="`${h.name} (${h.ip || h.id})`" :value="h.id" />
|
||||
</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 > queryParams.count">
|
||||
<el-pagination v-model:current-page="queryParams.page" v-model:page-size="queryParams.count"
|
||||
:page-sizes="[10, 20, 50]" :total="total" layout="total, sizes, prev, pager, next"
|
||||
@size-change="s => { queryParams.count = 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>
|
||||
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="120px">
|
||||
<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>
|
||||
<el-divider content-position="left">高级配置(可选)</el-divider>
|
||||
<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>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
|
||||
</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>
|
||||
</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, updateNetwork, deleteNetwork } from '@/api/admin/kvmService'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const embedded = inject('embedded', false)
|
||||
const injectedServiceId = inject('serviceId', null)
|
||||
const injectedServiceName = inject('serviceName', null)
|
||||
const serviceId = computed(() => injectedServiceId?.value || parseInt(route.query.service_id) || 0)
|
||||
const hostId = computed(() => 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 hostIdInput = ref(0)
|
||||
const hostOptions = ref([])
|
||||
const queryParams = reactive({ page: 1, count: 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: 100 })
|
||||
const body = res?.data
|
||||
if (body?.code === 200 && body?.data) {
|
||||
const inner = body.data
|
||||
hostOptions.value = inner.hosts || inner.data || (Array.isArray(inner) ? inner : [])
|
||||
}
|
||||
} 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.count }
|
||||
if (keyword.value) params.key = keyword.value
|
||||
if (filterType.value) params.type = filterType.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 payload = { ...formData, service_id: serviceId.value }
|
||||
// 空高级参数不提交
|
||||
const optionalFields = ['mac_address', 'bridge_name', 'ls_bridge_name', 'ls_name', 'nameservers', 'target_device']
|
||||
optionalFields.forEach(f => { if (!payload[f]) delete payload[f] })
|
||||
let res
|
||||
if (dialogType.value === 'add') {
|
||||
delete payload.id
|
||||
res = await createNetwork(payload)
|
||||
} else {
|
||||
res = await updateNetwork(payload)
|
||||
}
|
||||
if (res?.data?.code === 200) {
|
||||
ElMessage.success(dialogType.value === 'add' ? '创建成功' : '修改成功')
|
||||
dialogVisible.value = false
|
||||
loadList()
|
||||
} else {
|
||||
ElMessage.error(res?.data?.message || '操作失败')
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error('操作失败: ' + (e?.response?.data?.message || e.message))
|
||||
} 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(res?.data?.message || '删除失败')
|
||||
} catch (e) { ElMessage.error('删除失败') }
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.network-manage-container { padding: 20px; }
|
||||
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 16px; border-bottom: 1px solid #ebeef5; }
|
||||
.header-left { display: flex; align-items: center; gap: 16px; }
|
||||
.header-info h3 { margin: 0; font-size: 18px; color: #303133; }
|
||||
.sub-info { font-size: 13px; color: #909399; }
|
||||
.header-right { display: flex; gap: 8px; }
|
||||
.embedded-toolbar { display: flex; align-items: center; gap: 8px; margin-bottom: 16px; }
|
||||
.filter-bar { display: flex; gap: 12px; margin-bottom: 16px; flex-wrap: wrap; }
|
||||
.pagination-wrapper { display: flex; justify-content: flex-end; margin-top: 16px; }
|
||||
:deep(.el-table) { --el-table-header-bg-color: #fafafa; }
|
||||
:deep(.el-table th) { font-weight: 600; color: #303133; font-size: 13px; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user