feat: 对接用户组网管理
Build and Deploy Vue3 / build (push) Successful in 1m43s
Build and Deploy Vue3 / deploy (push) Successful in 1m7s

This commit is contained in:
2026-03-24 18:57:52 +08:00
parent 3357566b02
commit 40a5e486a6
29 changed files with 1895 additions and 9381 deletions
+74 -52
View File
@@ -54,10 +54,10 @@
<el-tag :type="vmStatusType(row.status)" size="small">{{ vmStatusLabel(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="宿主机" width="140">
<!-- <el-table-column label="宿主机" width="140">
<template #default="{ row }">{{ getHostLabel(row.host_id) }}</template>
</el-table-column>
<el-table-column prop="user_id" label="用户" width="80" />
</el-table-column> -->
<!-- <el-table-column prop="user_id" label="用户" width="80" /> -->
<el-table-column label="操作" width="280" fixed="right">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="handleGoDetail(row)">详情</el-button>
@@ -70,7 +70,7 @@
<el-dropdown-item command="reboot">重启</el-dropdown-item>
<el-dropdown-item command="suspend">暂停</el-dropdown-item>
<el-dropdown-item command="resume" v-if="row.status === 'paused'">恢复</el-dropdown-item>
<el-dropdown-item command="rebuild" divided></el-dropdown-item>
<el-dropdown-item command="rebuild" divided></el-dropdown-item>
<el-dropdown-item command="rescue">救援模式</el-dropdown-item>
<el-dropdown-item command="exit_rescue">退出救援</el-dropdown-item>
<el-dropdown-item command="detail" divided>查看详情</el-dropdown-item>
@@ -109,24 +109,31 @@
</el-form-item>
<el-divider content-position="left">宿主机配置(二选一)</el-divider>
<el-form-item label="分配方式">
<el-radio-group v-model="hostMode">
<el-radio value="host">指定宿主机</el-radio>
<el-radio value="group">指定宿主机组</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="宿主机" v-if="hostMode === 'host'">
<el-select v-model="createForm.host_id" placeholder="选择宿主机" filterable style="width: 100%" @change="(v) => loadNetworkOptions(v)">
<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="宿主机组" v-if="hostMode === 'group'">
<div class="bind-selector-row">
<el-input :model-value="createForm.host_group_id ? `${createForm._groupName || ''} (ID: ${createForm.host_group_id})` : '未选择'" disabled style="flex: 1" />
<el-button type="primary" @click="showHostGroupSelector = true" style="margin-left: 8px">选择</el-button>
<el-button v-if="createForm.host_group_id" @click="createForm.host_group_id = null; createForm._groupName = ''" style="margin-left: 4px">清除</el-button>
</div>
</el-form-item>
<template v-if="isEmbeddedHost">
<el-form-item label="宿主机">
<el-input :model-value="embeddedHostLabel" disabled />
</el-form-item>
</template>
<template v-else>
<el-form-item label="分配方式">
<el-radio-group v-model="hostMode">
<el-radio value="host">指定宿主机</el-radio>
<el-radio value="group">指定宿主机组</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="宿主机" v-if="hostMode === 'host'">
<el-select v-model="createForm.host_id" placeholder="选择宿主机" filterable style="width: 100%" @change="(v) => loadNetworkOptions(v)">
<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="宿主机组" v-if="hostMode === 'group'">
<div class="bind-selector-row">
<el-input :model-value="createForm.host_group_id ? `${createForm._groupName || ''} (ID: ${createForm.host_group_id})` : '未选择'" disabled style="flex: 1" />
<el-button type="primary" @click="showHostGroupSelector = true" style="margin-left: 8px">选择</el-button>
<el-button v-if="createForm.host_group_id" @click="createForm.host_group_id = null; createForm._groupName = ''" style="margin-left: 4px">清除</el-button>
</div>
</el-form-item>
</template>
<el-divider content-position="left">资源配置</el-divider>
<div class="resource-row">
@@ -183,9 +190,9 @@
</template>
</el-dialog>
<!-- 重弹窗 -->
<el-dialog v-model="rebuildDialogVisible" title="虚拟机" width="440px" destroy-on-close>
<el-alert title="会使用新镜像重置虚拟机原数据可能丢失" type="warning" :closable="false" show-icon style="margin-bottom: 16px" />
<!-- 重弹窗 -->
<el-dialog v-model="rebuildDialogVisible" title="虚拟机" width="440px" destroy-on-close>
<el-alert title="会使用新镜像重置虚拟机原数据可能丢失" type="warning" :closable="false" show-icon style="margin-bottom: 16px" />
<el-form label-width="100px">
<el-form-item label="虚拟机">{{ rebuildTarget?.name }} (#{{ rebuildTarget?.id }})</el-form-item>
<el-form-item label="新镜像" required>
@@ -197,7 +204,7 @@
</el-form>
<template #footer>
<el-button @click="rebuildDialogVisible = false">取消</el-button>
<el-button type="danger" :loading="submitLoading" @click="submitRebuild">确认重</el-button>
<el-button type="danger" :loading="submitLoading" @click="submitRebuild">确认重</el-button>
</template>
</el-dialog>
@@ -234,7 +241,7 @@
<!-- 网络信息 -->
<template v-if="currentDetail?.networks && currentDetail.networks.length">
<h4 style="margin: 16px 0 8px">🌐 网络</h4>
<h4 style="margin: 16px 0 8px">网络</h4>
<el-table :data="currentDetail.networks" size="small" stripe border>
<el-table-column prop="id" label="ID" width="60" />
<el-table-column prop="name" label="名称" min-width="120" />
@@ -246,7 +253,7 @@
<!-- 数据卷信息 -->
<template v-if="currentDetail?.volumes && currentDetail.volumes.length">
<h4 style="margin: 16px 0 8px">💿 数据卷</h4>
<h4 style="margin: 16px 0 8px">数据卷</h4>
<el-table :data="currentDetail.volumes" size="small" stripe border>
<el-table-column prop="id" label="ID" width="60" />
<el-table-column prop="name" label="名称" min-width="120" />
@@ -262,7 +269,7 @@
<!-- 镜像信息 -->
<template v-if="currentDetail?.image">
<h4 style="margin: 16px 0 8px">🖼️ 镜像</h4>
<h4 style="margin: 16px 0 8px">镜像</h4>
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="ID">{{ currentDetail.image.id }}</el-descriptions-item>
<el-descriptions-item label="名称">{{ currentDetail.image.name }}</el-descriptions-item>
@@ -299,7 +306,7 @@
<!-- 镜像选择器 (创建) -->
<ImageSelectorPopup v-model="showCreateImageSelector" :service-id="serviceId" :current-id="createForm.image_id" @confirm="handleCreateImageSelected" />
<!-- 镜像选择器 (重) -->
<!-- 镜像选择器 (重) -->
<ImageSelectorPopup v-model="showRebuildImageSelector" :service-id="serviceId" :current-id="rebuildImageId" @confirm="handleRebuildImageSelected" />
<!-- 宿主机组选择器 -->
<HostGroupSelectorPopup v-model="showHostGroupSelector" :service-id="serviceId" :current-id="createForm.host_group_id" @confirm="handleHostGroupSelected" />
@@ -329,8 +336,15 @@ const embedded = inject('embedded', false)
const injectedServiceId = inject('serviceId', null)
const injectedServiceName = inject('serviceName', null)
const injectedHostId = inject('hostId', null)
const injectedHostDetail = inject('hostDetail', null)
const serviceId = computed(() => injectedServiceId?.value || parseInt(route.query.service_id) || 0)
const hostId = computed(() => injectedHostId?.value || parseInt(route.query.host_id) || 0)
const isEmbeddedHost = computed(() => !!(embedded && injectedHostId?.value))
const embeddedHostLabel = computed(() => {
const d = injectedHostDetail?.value
if (!d) return `宿主机 (ID: ${injectedHostId?.value || ''})`
return `${d.name || ''} (${d.ip || d.id || ''})`
})
const serviceName = computed(() => injectedServiceName?.value || route.query.service_name || '')
const loading = ref(false)
@@ -381,10 +395,12 @@ const diskDisplay = computed({
set: (v) => { createForm.system_size = Math.round(v * getDiskFactor()) }
})
const loadNetworkOptions = async (hostId) => {
if (!hostId) return
const loadNetworkOptions = async (hid) => {
if (!hid) return
try {
const res = await getNetworkList({ service_id: serviceId.value, host_id: hostId, used: false, page: 1, page_size: 200 })
const params = { service_id: serviceId.value, host_id: hid, used: false, page: 1, page_size: 10 }
if (injectedHostId?.value) params.type = 'bridge'
const res = await getNetworkList(params)
const body = res?.data
if (body?.code === 200 && body?.data) {
const inner = body.data
@@ -400,7 +416,7 @@ const getHostLabel = (hid) => {
const loadHostOptions = async () => {
try {
const res = await getRemoteHostList({ service_id: serviceId.value, page: 1, page_size: 100 })
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
@@ -513,7 +529,7 @@ const handleSearch = () => { queryParams.page = 1; loadList() }
const handleAdd = () => {
Object.assign(createForm, {
name: '', host_id: null, image_id: 0,
name: '', host_id: injectedHostId?.value || null, image_id: 0,
vcpu: 0, memory: 0, system_size: 0,
rx_bandwidth: 0, tx_bandwidth: 0, host_group_id: null, user_id: 0, ip_num: 0, network_ids: [],
_imageName: '', _groupName: '', _userName: ''
@@ -524,6 +540,9 @@ const handleAdd = () => {
ipMode.value = 'num'
networkOptions.value = []
createDialogVisible.value = true
if (injectedHostId?.value) {
loadNetworkOptions(injectedHostId.value)
}
}
const submitCreate = () => {
@@ -536,20 +555,21 @@ const submitCreate = () => {
if (!valid) return
submitLoading.value = true
try {
const payload = {
service_id: serviceId.value,
image_id: createForm.image_id,
vcpu: createForm.vcpu, memory: createForm.memory, system_size: createForm.system_size,
user_id: createForm.user_id
}
if (createForm.name) payload.name = createForm.name
if (createForm.rx_bandwidth) payload.rx_bandwidth = createForm.rx_bandwidth
if (createForm.tx_bandwidth) payload.tx_bandwidth = createForm.tx_bandwidth
if (hostMode.value === 'host') payload.host_id = createForm.host_id
else payload.host_group_id = createForm.host_group_id
if (ipMode.value === 'num') payload.ip_num = createForm.ip_num
else payload.network_ids = createForm.network_ids
const res = await createVm(payload)
const fd = new FormData()
fd.append('service_id', serviceId.value)
fd.append('image_id', createForm.image_id)
fd.append('vcpu', createForm.vcpu)
fd.append('memory', createForm.memory)
fd.append('system_size', createForm.system_size)
fd.append('user_id', createForm.user_id)
if (createForm.name) fd.append('name', createForm.name)
if (createForm.rx_bandwidth) fd.append('rx_bandwidth', createForm.rx_bandwidth)
if (createForm.tx_bandwidth) fd.append('tx_bandwidth', createForm.tx_bandwidth)
if (hostMode.value === 'host') fd.append('host_id', createForm.host_id)
else fd.append('host_group_id', createForm.host_group_id)
if (ipMode.value === 'num') fd.append('ip_num', createForm.ip_num)
else createForm.network_ids.forEach(id => fd.append('network_ids', id))
const res = await createVm(fd)
if (res?.data?.code === 200) { ElMessage.success('创建成功'); createDialogVisible.value = false; loadList() }
else ElMessage.error(extractApiError(res?.data, '创建失败'))
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '创建失败')) }
@@ -604,9 +624,9 @@ const submitRebuild = async () => {
submitLoading.value = true
try {
const res = await rebuildVm({ service_id: serviceId.value, vm_id: rebuildTarget.value.id, image_id: rebuildImageId.value })
if (res?.data?.code === 200) { ElMessage.success('重成功'); rebuildDialogVisible.value = false; loadList() }
else ElMessage.error(extractApiError(res?.data, '重失败'))
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '重失败')) } finally { submitLoading.value = false }
if (res?.data?.code === 200) { ElMessage.success('重成功'); rebuildDialogVisible.value = false; loadList() }
else ElMessage.error(extractApiError(res?.data, '重失败'))
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '重失败')) } finally { submitLoading.value = false }
}
const handleRescue = (row) => {
@@ -702,6 +722,8 @@ onMounted(async () => {
loadList()
}
})
defineExpose({ loadList })
</script>
<style scoped>