diff --git a/src/components/admin/NetworkSelectorPopup.vue b/src/components/admin/NetworkSelectorPopup.vue new file mode 100644 index 0000000..81b575b --- /dev/null +++ b/src/components/admin/NetworkSelectorPopup.vue @@ -0,0 +1,113 @@ + + + + + diff --git a/src/components/admin/SecurityGroupSelectorPopup.vue b/src/components/admin/SecurityGroupSelectorPopup.vue new file mode 100644 index 0000000..7c9610d --- /dev/null +++ b/src/components/admin/SecurityGroupSelectorPopup.vue @@ -0,0 +1,102 @@ + + + + + diff --git a/src/components/admin/VolumeSelectorPopup.vue b/src/components/admin/VolumeSelectorPopup.vue new file mode 100644 index 0000000..2309c44 --- /dev/null +++ b/src/components/admin/VolumeSelectorPopup.vue @@ -0,0 +1,124 @@ + + + + + diff --git a/src/views/virtualization/NetworkManage.vue b/src/views/virtualization/NetworkManage.vue index 855c0d2..d6f75f4 100644 --- a/src/views/virtualization/NetworkManage.vue +++ b/src/views/virtualization/NetworkManage.vue @@ -27,9 +27,7 @@ - - - + @@ -255,16 +253,24 @@ const handleSubmit = () => { 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] }) + 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') { - delete payload.id - res = await createNetwork(payload) + res = await createNetwork(fd) } else { - res = await updateNetwork(payload) + fd.append('network_id', formData.id) + res = await updateNetwork(fd) } if (res?.data?.code === 200) { ElMessage.success(dialogType.value === 'add' ? '创建成功' : '修改成功') @@ -274,7 +280,7 @@ const handleSubmit = () => { ElMessage.error(extractApiError(res?.data, '操作失败')) } } catch (e) { - ElMessage.error('操作失败: ' + (e?.response?.data?.message || e.message)) + ElMessage.error(extractApiError(e?.response?.data, '操作失败')) } finally { submitLoading.value = false } }) } diff --git a/src/views/virtualization/SecurityGroupManage.vue b/src/views/virtualization/SecurityGroupManage.vue index 73ff139..ff21234 100644 --- a/src/views/virtualization/SecurityGroupManage.vue +++ b/src/views/virtualization/SecurityGroupManage.vue @@ -57,7 +57,7 @@ 编辑 同步 应用 - + 更多 + + + @@ -629,22 +684,8 @@ - - - {{ detail?.name || '-' }} - - - - - - - - + + @@ -695,62 +736,78 @@ - - - - - - + + + + + + + + + + - - - - + + + + + + + + + + 高级配置(可选) + + + + + + + + + + + -
- - {{ selectedNetworkInfo?.name || '-' }} - {{ selectedNetworkInfo?.address || '-' }} - {{ selectedNetworkInfo?.gateway || '-' }} - {{ selectedNetworkInfo?.type || '-' }} - -
- - - - - - - + + + + + + + + - - - - + + + + + + + + + + + -
- - {{ selectedVolumeInfo?.name || '-' }} - {{ selectedVolumeInfo?.size ? selectedVolumeInfo.size + ' GB' : '-' }} - {{ selectedVolumeInfo?.status || '-' }} - {{ selectedVolumeInfo?.path || '-' }} - -
+ + + @@ -808,6 +865,9 @@ import { import { extractApiError } from '@/utils/kvmErrorUtil' import * as echarts from 'echarts' import ImageSelectorPopup from '@/components/admin/ImageSelectorPopup.vue' +import VolumeSelectorPopup from '@/components/admin/VolumeSelectorPopup.vue' +import NetworkSelectorPopup from '@/components/admin/NetworkSelectorPopup.vue' +import SecurityGroupSelectorPopup from '@/components/admin/SecurityGroupSelectorPopup.vue' import { useTagsViewStore } from '@/store/tagsViewStore' const route = useRoute() @@ -823,6 +883,7 @@ const actionLoading = ref(false) const statusLoading = ref(false) const metricsLoading = ref(false) const detail = ref(null) +const vmHostId = ref(0) const vmNetworks = ref([]) const vmVolumes = ref([]) const vmImage = ref(null) @@ -919,13 +980,14 @@ const loadDetail = async () => { vmVolumes.value = d.volumes || [] vmImage.value = d.image || null vmPortGroup.value = d.in_port_group || null + vmHostId.value = detail.value?.host_id || vmVolumes.value[0]?.host_id || vmNetworks.value[0]?.host_id || 0 } else ElMessage.error(extractApiError(res?.data, '加载失败')) } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '加载失败')) } finally { loading.value = false } } const loadVmVolumes = async () => { if (!detail.value) return - const hid = detail.value.host_id + const hid = vmHostId.value if (!hid) return try { const res = await getVolumeList({ service_id: serviceId.value, host_id: hid, vm_id: vmId.value, page: 1, count: 200 }) @@ -938,7 +1000,7 @@ const loadVmVolumes = async () => { const loadVmNetworks = async () => { if (!detail.value) return - const hid = detail.value.host_id + const hid = vmHostId.value if (!hid) return try { const res = await getNetworkList({ service_id: serviceId.value, host_id: hid, page: 1, page_size: 200 }) @@ -1226,7 +1288,31 @@ const submitEditVm = async () => { // ---- 重构虚拟机 ---- const refactorDialogVisible = ref(false) const refactorFormRef = ref(null) -const refactorForm = reactive({ memory: 0, vcpu: 0, rx_bandwidth: 0, tx_bandwidth: 0, root_password: '', ssh_port: 0, vnc_port: 0, vnc_password: '', port_group_id: 0 }) +const refactorForm = reactive({ + memory: 0, vcpu: 0, rx_bandwidth: 0, tx_bandwidth: 0, + root_password: '', uuid: '', mate_data_id: '', physical_name: '', config_path: '', + ssh_port: 0, vnc_port: 0, vnc_password: '', + internet_network_id: 0, port_group_id: 0 +}) +const refactorMemUnit = ref(1048576) +const refactorMemDisplay = ref(0) +const refactorSelectedNetworks = ref([]) +const showRefactorNetworkSelector = ref(false) + +const onRefactorMemUnitChange = () => { + refactorMemDisplay.value = refactorForm.memory ? Math.round(refactorForm.memory / refactorMemUnit.value * 100) / 100 : 0 +} +watch(refactorMemDisplay, (v) => { refactorForm.memory = Math.round(v * refactorMemUnit.value) }) + +const handleRefactorNetworkConfirm = (network) => { + if (!refactorSelectedNetworks.value.find(n => n.id === network.id)) { + refactorSelectedNetworks.value.push({ id: network.id, name: network.name }) + } +} +const removeRefactorNetwork = (id) => { + refactorSelectedNetworks.value = refactorSelectedNetworks.value.filter(n => n.id !== id) + if (refactorForm.internet_network_id === id) refactorForm.internet_network_id = 0 +} const handleRefactorVm = async () => { if (!detail.value) return @@ -1234,10 +1320,18 @@ const handleRefactorVm = async () => { Object.assign(refactorForm, { memory: d.memory || 0, vcpu: d.vcpu || 0, rx_bandwidth: d.rx_bandwidth || 0, tx_bandwidth: d.tx_bandwidth || 0, - root_password: '', ssh_port: d.ssh_port || 0, - vnc_port: 0, vnc_password: '', - port_group_id: vmPortGroup.value?.id || null + root_password: d.root_password || '', + uuid: d.uuid || '', mate_data_id: d.mate_data_id || '', + physical_name: d.physical_name || '', config_path: d.config_path || '', + ssh_port: d.ssh_port || 0, vnc_port: 0, vnc_password: '', + internet_network_id: 0, + port_group_id: vmPortGroup.value?.id || 0 }) + refactorSelectedNetworks.value = vmNetworks.value.map(n => ({ id: n.id, name: n.name })) + const mem = d.memory || 0 + if (mem >= 1048576 && mem % 1048576 === 0) { refactorMemUnit.value = 1048576; refactorMemDisplay.value = mem / 1048576 } + else if (mem >= 1024 && mem % 1024 === 0) { refactorMemUnit.value = 1024; refactorMemDisplay.value = mem / 1024 } + else { refactorMemUnit.value = 1; refactorMemDisplay.value = mem } if (!sgOptions.value.length) await loadSgOptions() refactorDialogVisible.value = true } @@ -1253,9 +1347,15 @@ const submitRefactorVm = async () => { fd.append('rx_bandwidth', refactorForm.rx_bandwidth) fd.append('tx_bandwidth', refactorForm.tx_bandwidth) if (refactorForm.root_password) fd.append('root_password', refactorForm.root_password) + if (refactorForm.uuid) fd.append('uuid', refactorForm.uuid) + if (refactorForm.mate_data_id) fd.append('mate_data_id', refactorForm.mate_data_id) + if (refactorForm.physical_name) fd.append('physical_name', refactorForm.physical_name) + if (refactorForm.config_path) fd.append('config_path', refactorForm.config_path) if (refactorForm.ssh_port) fd.append('ssh_port', refactorForm.ssh_port) if (refactorForm.vnc_port) fd.append('vnc_port', refactorForm.vnc_port) if (refactorForm.vnc_password) fd.append('vnc_password', refactorForm.vnc_password) + if (refactorSelectedNetworks.value.length) fd.append('network_ids', refactorSelectedNetworks.value.map(n => n.id).join(',')) + if (refactorForm.internet_network_id) fd.append('internet_network_id', refactorForm.internet_network_id) if (refactorForm.port_group_id) fd.append('port_group_id', refactorForm.port_group_id) const res = await refactorVm(fd) if (res?.data?.code === 200) { ElMessage.success('重构成功'); refactorDialogVisible.value = false; loadDetail() } @@ -1317,7 +1417,6 @@ const handleGetVnc = async () => { } const submitGetVnc = async () => { - if (!vncNodeId.value) { ElMessage.warning('请选择VNC节点'); return } vncLoading.value = true vncResult.value = null try { @@ -1333,9 +1432,14 @@ const vmSecurityGroups = ref([]) const sgListLoading = ref(false) const securityPage = ref(1) const securityPageSize = ref(10) +const sgDirectionFilter = ref('') +const filteredSecurityGroups = computed(() => { + if (!sgDirectionFilter.value) return vmSecurityGroups.value + return vmSecurityGroups.value.filter(g => g.direction === sgDirectionFilter.value) +}) const pagedSecurityGroups = computed(() => { const start = (securityPage.value - 1) * securityPageSize.value - return vmSecurityGroups.value.slice(start, start + securityPageSize.value) + return filteredSecurityGroups.value.slice(start, start + securityPageSize.value) }) const loadVmSecurityGroups = async () => { @@ -1350,9 +1454,7 @@ const loadVmSecurityGroups = async () => { } // ---- 绑定/解绑安全组 ---- -const sgDialogVisible = ref(false) -const sgDialogType = ref('bind') -const sgSelectedId = ref(null) +const sgBindSelectorVisible = ref(false) const sgOptions = ref([]) const loadSgOptions = async () => { @@ -1365,21 +1467,22 @@ const loadSgOptions = async () => { } catch { /* */ } } -const handleBindSg = async () => { - sgDialogType.value = 'bind'; sgSelectedId.value = null - if (!sgOptions.value.length) await loadSgOptions() - sgDialogVisible.value = true -} -const handleUnbindSg = async () => { - sgDialogType.value = 'unbind'; sgSelectedId.value = null - if (!sgOptions.value.length) await loadSgOptions() - sgDialogVisible.value = true -} +const handleBindSg = () => { sgBindSelectorVisible.value = true } +const handleUnbindSg = () => { sgBindSelectorVisible.value = true } +const handleBindSgFromTab = () => { sgBindSelectorVisible.value = true } -const handleBindSgFromTab = async () => { - sgDialogType.value = 'bind'; sgSelectedId.value = null - if (!sgOptions.value.length) await loadSgOptions() - sgDialogVisible.value = true +const handleSgBindConfirm = async (sg) => { + if (!sg?.id) return + actionLoading.value = true + try { + const fd = new FormData() + fd.append('service_id', serviceId.value) + fd.append('id', sg.id) + fd.append('vm_id', vmId.value) + const res = await bindSecurityGroup(fd) + if (res?.data?.code === 200) { ElMessage.success('绑定成功'); loadVmSecurityGroups() } + else ElMessage.error(extractApiError(res?.data, '绑定失败')) + } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '绑定失败')) } finally { actionLoading.value = false } } const handleUnbindSgFromTab = async (row) => { @@ -1398,67 +1501,62 @@ const handleUnbindSgFromTab = async (row) => { } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '解绑失败')) } finally { actionLoading.value = false } } -const submitSgAction = async () => { - if (!sgSelectedId.value) { ElMessage.warning('请选择安全组'); return } - actionLoading.value = true - try { - const api = sgDialogType.value === 'bind' ? bindSecurityGroup : unbindSecurityGroup - const fd = new FormData() - fd.append('service_id', serviceId.value) - fd.append('id', sgSelectedId.value) - fd.append('vm_id', vmId.value) - const res = await api(fd) - if (res?.data?.code === 200) { - ElMessage.success(sgDialogType.value === 'bind' ? '绑定成功' : '解绑成功') - sgDialogVisible.value = false - loadVmSecurityGroups() - } - else ElMessage.error(extractApiError(res?.data, '操作失败')) - } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '操作失败')) } finally { actionLoading.value = false } +// ---- 创建网络(表单弹窗) ---- +const netCreateVisible = 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: '请输入IP地址(CIDR)', trigger: 'blur' }], + gateway: [{ required: true, message: '请输入网关地址', trigger: 'blur' }], + type: [{ required: true, message: '请选择网络类型', trigger: 'change' }] } -// ---- 添加网络(从已有列表选择) ---- +const showCreateNetworkDialog = () => { + Object.assign(netCreateForm, { name: '', address: '', gateway: '', nameservers: '', type: 'bridge', mac_address: '', bridge_name: '', ls_bridge_name: '', ls_name: '' }) + netCreateVisible.value = true +} + +const submitCreateNetwork = () => { + netCreateFormRef.value?.validate(async (valid) => { + if (!valid) return + actionLoading.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', vmHostId.value) + 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; loadDetail() } + else ElMessage.error(extractApiError(res?.data, '创建失败')) + } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '创建失败')) } finally { actionLoading.value = false } + }) +} + +// ---- 添加已有网络(弹窗选择组件) ---- const netAddVisible = ref(false) -const netAddHostId = ref(null) -const netAddSelectedId = ref(null) -const availableNetworks = ref([]) -const netOptionsLoading = ref(false) -const selectedNetworkInfo = computed(() => availableNetworks.value.find(n => n.id === netAddSelectedId.value) || null) +const handleAddNetwork = () => { netAddVisible.value = true } -const loadAvailableNetworks = async (hostId) => { - netAddSelectedId.value = null - availableNetworks.value = [] - if (!hostId) return - netOptionsLoading.value = true - try { - const res = await getNetworkList({ service_id: serviceId.value, host_id: hostId, used: false, page: 1, page_size: 200 }) - if (res?.data?.code === 200 && res?.data?.data) { - const inner = res.data.data - availableNetworks.value = inner.networks || inner.data || (Array.isArray(inner) ? inner : []) - } - } catch { /* */ } finally { netOptionsLoading.value = false } -} - -const handleAddNetwork = () => { - netAddHostId.value = null - netAddSelectedId.value = null - availableNetworks.value = [] - netAddVisible.value = true -} - -const submitAddNetwork = async () => { - if (!netAddSelectedId.value) { ElMessage.warning('请选择网络'); return } +const handleNetworkSelectorConfirm = async (network) => { + if (!network?.id) return actionLoading.value = true try { - const net = selectedNetworkInfo.value const fd = new FormData() fd.append('service_id', serviceId.value) fd.append('vm_id', vmId.value) - fd.append('network_id', netAddSelectedId.value) - if (net?.host_id) fd.append('host_id', net.host_id) + fd.append('network_id', network.id) + if (network.host_id) fd.append('host_id', network.host_id) const res = await createNetwork(fd) - if (res?.data?.code === 200) { ElMessage.success('网络添加成功'); netAddVisible.value = false; loadDetail() } + if (res?.data?.code === 200) { ElMessage.success('网络添加成功'); loadDetail() } else ElMessage.error(extractApiError(res?.data, '添加失败')) } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '添加失败')) } finally { actionLoading.value = false } } @@ -1508,47 +1606,56 @@ const handleDeleteNetwork = (row) => { }).catch(() => {}) } -// ---- 挂载数据卷(从已有列表选择) ---- +// ---- 创建数据卷(表单弹窗) ---- +const volCreateVisible = ref(false) +const volCreateFormRef = ref(null) +const volCreateForm = reactive({ name: '', size: 10, is_system: false, target_device: '', image_id: 0 }) +const volCreateRules = { + name: [{ required: true, message: '请输入名称', trigger: 'blur' }], + size: [{ required: true, message: '请输入大小', trigger: 'blur' }] +} + +const showCreateVolumeDialog = () => { + Object.assign(volCreateForm, { name: '', size: 10, is_system: false, target_device: '', image_id: 0 }) + volCreateVisible.value = true +} + +const submitCreateVolume = () => { + volCreateFormRef.value?.validate(async (valid) => { + if (!valid) return + actionLoading.value = true + try { + const fd = new FormData() + fd.append('service_id', serviceId.value) + fd.append('name', volCreateForm.name) + fd.append('size', volCreateForm.size) + fd.append('is_system', volCreateForm.is_system) + fd.append('vm_id', vmId.value) + fd.append('host_id', vmHostId.value) + if (volCreateForm.target_device) fd.append('target_device', volCreateForm.target_device) + if (volCreateForm.image_id) fd.append('image_id', volCreateForm.image_id) + const res = await createVolume(fd) + if (res?.data?.code === 200) { ElMessage.success('数据卷创建成功'); volCreateVisible.value = false; loadDetail() } + else ElMessage.error(extractApiError(res?.data, '创建失败')) + } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '创建失败')) } finally { actionLoading.value = false } + }) +} + +// ---- 挂载已有数据卷(弹窗选择组件) ---- const volAddVisible = ref(false) -const volAddHostId = ref(null) -const volAddSelectedId = ref(null) -const availableVolumes = ref([]) -const volOptionsLoading = ref(false) -const selectedVolumeInfo = computed(() => availableVolumes.value.find(v => v.id === volAddSelectedId.value) || null) +const handleAddVolume = () => { volAddVisible.value = true } -const loadAvailableVolumes = async (hostId) => { - volAddSelectedId.value = null - availableVolumes.value = [] - if (!hostId) return - volOptionsLoading.value = true - try { - const res = await getVolumeList({ service_id: serviceId.value, host_id: hostId, page: 1, page_size: 200 }) - if (res?.data?.code === 200 && res?.data?.data) { - const inner = res.data.data - const list = inner.volumes || inner.data || (Array.isArray(inner) ? inner : []) - availableVolumes.value = list.filter(v => !v.is_mount) - } - } catch { /* */ } finally { volOptionsLoading.value = false } -} - -const handleAddVolume = () => { - volAddHostId.value = null - volAddSelectedId.value = null - availableVolumes.value = [] - volAddVisible.value = true -} - -const submitAddVolume = async () => { - if (!volAddSelectedId.value) { ElMessage.warning('请选择数据卷'); return } +const handleVolumeSelectorConfirm = async (volume) => { + if (!volume?.id) return actionLoading.value = true try { const fd = new FormData() fd.append('service_id', serviceId.value) fd.append('vm_id', vmId.value) - fd.append('volume_id', volAddSelectedId.value) + fd.append('volume_id', volume.id) const res = await mountVolume(fd) - if (res?.data?.code === 200) { ElMessage.success('数据卷挂载成功'); volAddVisible.value = false; loadDetail() } + if (res?.data?.code === 200) { ElMessage.success('数据卷挂载成功'); loadDetail() } else ElMessage.error(extractApiError(res?.data, '挂载失败')) } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '挂载失败')) } finally { actionLoading.value = false } } diff --git a/src/views/virtualization/VmManage.vue b/src/views/virtualization/VmManage.vue index 43a6aa9..93551cc 100644 --- a/src/views/virtualization/VmManage.vue +++ b/src/views/virtualization/VmManage.vue @@ -63,7 +63,7 @@ 详情 启动 关机 - + 更多