fix: 虚拟机模块
This commit is contained in:
@@ -138,7 +138,7 @@
|
||||
</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-select v-model="createForm.host_id" placeholder="选择宿主机" filterable style="width: 100%" @change="onHostChange">
|
||||
<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>
|
||||
@@ -203,10 +203,16 @@
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="网络IP列表" v-if="ipMode === 'ids'">
|
||||
<el-select v-model="createForm.network_ids" multiple filterable placeholder="选择可用网络IP" style="width: 100%">
|
||||
<el-option v-for="n in networkOptions" :key="n.id" :label="`${n.name || ''} - ${n.address || n.ip || ''}`" :value="n.id" />
|
||||
</el-select>
|
||||
<div class="form-tip" v-if="!networkOptions.length">请先选择宿主机以加载可用网络(仅显示未使用的网络)</div>
|
||||
<div style="display:flex;align-items:center;gap:8px;width:100%">
|
||||
<el-button :disabled="!createForm.host_id" @click="showCreateNetSelector = true">选择网络</el-button>
|
||||
<span v-if="createNetSelected.length" style="font-size:13px;color:#606266">已选 {{ createNetSelected.length }} 个</span>
|
||||
<span v-else style="font-size:13px;color:#909399">{{ createForm.host_id ? '未选择' : '请先选择宿主机' }}</span>
|
||||
</div>
|
||||
<div v-if="createNetSelected.length" style="margin-top:6px;display:flex;flex-wrap:wrap;gap:4px">
|
||||
<el-tag v-for="n in createNetSelected" :key="n.id" closable size="small" @close="removeCreateNet(n.id)">
|
||||
{{ n.name || n.bridge_name || n.id }} - {{ n.address || '' }} (ID:{{ n.id }})
|
||||
</el-tag>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
@@ -218,6 +224,48 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<NetworkSelectorPopup ref="createNetSelectorRef" v-model="showCreateNetSelector"
|
||||
:service-id="serviceId" :host-id="createForm.host_id || 0"
|
||||
filter-type="bridge" filter-used="false" :multiple="true"
|
||||
@confirm="handleCreateNetConfirm" @create="handleCreateNetCreate" />
|
||||
|
||||
<!-- 创建网络弹窗 -->
|
||||
<el-dialog v-model="netCreateVisible" title="创建网络" width="600px" destroy-on-close append-to-body class="tk-dialog">
|
||||
<el-form ref="netCreateFormRef" :model="netCreateForm" :rules="netCreateRules" label-width="100px">
|
||||
<div class="tk-section">
|
||||
<div class="tk-section-title">基本信息</div>
|
||||
<el-form-item label="名称" prop="name"><el-input v-model="netCreateForm.name" placeholder="网络名称" /></el-form-item>
|
||||
<el-form-item label="网络类型" prop="type">
|
||||
<el-select v-model="netCreateForm.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" prop="address">
|
||||
<div style="display:flex;align-items:center;gap:8px;width:100%">
|
||||
<el-input v-model="netCreateForm.address" placeholder="例如 192.168.1.0/24" />
|
||||
<span style="color:#909399;white-space:nowrap">CIDR</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="网关地址" prop="gateway"><el-input v-model="netCreateForm.gateway" placeholder="例如 192.168.1.1" /></el-form-item>
|
||||
<el-form-item label="DNS 服务器"><el-input v-model="netCreateForm.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="netCreateForm.mac_address" placeholder="不填则随机" /></el-form-item>
|
||||
<el-form-item label="虚拟网桥名"><el-input v-model="netCreateForm.bridge_name" placeholder="不填使用默认" /></el-form-item>
|
||||
<el-form-item label="逻辑网桥名"><el-input v-model="netCreateForm.ls_bridge_name" placeholder="不填使用默认" /></el-form-item>
|
||||
<el-form-item label="逻辑端口名"><el-input v-model="netCreateForm.ls_name" placeholder="不填使用默认" /></el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div style="display:flex;justify-content:flex-end;gap:8px">
|
||||
<el-button @click="netCreateVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="netCreateLoading" @click="submitNetCreate">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 重装弹窗 -->
|
||||
<el-dialog v-model="rebuildDialogVisible" title="重装虚拟机" width="480px" destroy-on-close class="tk-dialog">
|
||||
<el-form label-width="100px">
|
||||
@@ -368,20 +416,21 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, inject, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { ref, reactive, computed, inject, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Refresh, Search, ArrowLeft, ArrowDown, WarningFilled } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getRemoteHostList, getVmList, getVmDetail, getVmStatus,
|
||||
createVm, rebuildVm, startVm, stopVm, rebootVm, suspendVm,
|
||||
resumeVm, rescueVm, exitRescueVm, deleteVm, getNetworkList, getMetricsHistory,
|
||||
resumeVm, rescueVm, exitRescueVm, deleteVm, getNetworkList, createNetwork, getMetricsHistory,
|
||||
getDataMigrateProgress
|
||||
} from '@/api/admin/kvmService'
|
||||
import { extractApiError } from '@/utils/kvmErrorUtil'
|
||||
import ImageSelectorPopup from '@/components/admin/ImageSelectorPopup.vue'
|
||||
import HostGroupSelectorPopup from '@/components/admin/HostGroupSelectorPopup.vue'
|
||||
import UserListSelector from '@/components/admin/UserListSelector.vue'
|
||||
import NetworkSelectorPopup from '@/components/admin/NetworkSelectorPopup.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -420,6 +469,9 @@ const showUserSelector = ref(false)
|
||||
const hostMode = ref('host')
|
||||
const ipMode = ref('num')
|
||||
const networkOptions = ref([])
|
||||
const showCreateNetSelector = ref(false)
|
||||
const createNetSelectorRef = ref(null)
|
||||
const createNetSelected = ref([])
|
||||
|
||||
// 内存单位: API传输单位为 bytes
|
||||
const memoryUnitOptions = [
|
||||
@@ -462,6 +514,65 @@ const loadNetworkOptions = async (hid) => {
|
||||
} catch { networkOptions.value = [] }
|
||||
}
|
||||
|
||||
const onHostChange = (v) => {
|
||||
createNetSelected.value = []
|
||||
createForm.network_ids = []
|
||||
loadNetworkOptions(v)
|
||||
}
|
||||
|
||||
const handleCreateNetConfirm = (items) => {
|
||||
const arr = Array.isArray(items) ? items : [items]
|
||||
const existIds = new Set(createNetSelected.value.map(n => n.id))
|
||||
arr.forEach(n => { if (!existIds.has(n.id)) createNetSelected.value.push(n) })
|
||||
createForm.network_ids = createNetSelected.value.map(n => n.id)
|
||||
}
|
||||
const removeCreateNet = (id) => {
|
||||
createNetSelected.value = createNetSelected.value.filter(n => n.id !== id)
|
||||
createForm.network_ids = createNetSelected.value.map(n => n.id)
|
||||
}
|
||||
const netCreateVisible = ref(false)
|
||||
const netCreateLoading = ref(false)
|
||||
const netCreateFormRef = ref(null)
|
||||
const netCreateForm = reactive({ name: '', address: '', gateway: '', nameservers: '', type: 'bridge', mac_address: '', bridge_name: '', ls_bridge_name: '', ls_name: '' })
|
||||
const netCreateRules = {
|
||||
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
|
||||
address: [{ required: true, message: '请输入 CIDR 格式网段', trigger: 'blur' }],
|
||||
gateway: [{ required: true, message: '请输入网关', trigger: 'blur' }],
|
||||
type: [{ required: true, message: '请选择类型', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const handleCreateNetCreate = () => {
|
||||
Object.assign(netCreateForm, { name: '', address: '', gateway: '', nameservers: '', type: 'bridge', mac_address: '', bridge_name: '', ls_bridge_name: '', ls_name: '' })
|
||||
netCreateVisible.value = true
|
||||
}
|
||||
|
||||
const submitNetCreate = () => {
|
||||
netCreateFormRef.value?.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
netCreateLoading.value = true
|
||||
try {
|
||||
const fd = new FormData()
|
||||
fd.append('service_id', serviceId.value)
|
||||
fd.append('name', netCreateForm.name)
|
||||
fd.append('address', netCreateForm.address)
|
||||
fd.append('gateway', netCreateForm.gateway)
|
||||
fd.append('type', netCreateForm.type)
|
||||
fd.append('host_id', createForm.host_id)
|
||||
if (netCreateForm.nameservers) fd.append('nameservers', netCreateForm.nameservers)
|
||||
if (netCreateForm.mac_address) fd.append('mac_address', netCreateForm.mac_address)
|
||||
if (netCreateForm.bridge_name) fd.append('bridge_name', netCreateForm.bridge_name)
|
||||
if (netCreateForm.ls_bridge_name) fd.append('ls_bridge_name', netCreateForm.ls_bridge_name)
|
||||
if (netCreateForm.ls_name) fd.append('ls_name', netCreateForm.ls_name)
|
||||
const res = await createNetwork(fd)
|
||||
if (res?.data?.code === 200) {
|
||||
ElMessage.success('创建成功')
|
||||
netCreateVisible.value = false
|
||||
nextTick(() => createNetSelectorRef.value?.loadList())
|
||||
} else ElMessage.error(extractApiError(res?.data, '创建失败'))
|
||||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '创建失败')) } finally { netCreateLoading.value = false }
|
||||
})
|
||||
}
|
||||
|
||||
const getHostLabel = (hid) => {
|
||||
const h = hostOptions.value.find(x => x.id === hid)
|
||||
return h ? `${h.name}` : (hid || '-')
|
||||
@@ -648,6 +759,7 @@ const handleAdd = () => {
|
||||
hostMode.value = 'host'
|
||||
ipMode.value = 'num'
|
||||
networkOptions.value = []
|
||||
createNetSelected.value = []
|
||||
loadHostOptions()
|
||||
createDialogVisible.value = true
|
||||
if (injectedHostId?.value) {
|
||||
|
||||
Reference in New Issue
Block a user