fex: 样式修改
Build and Deploy Vue3 / build (push) Successful in 1m39s
Build and Deploy Vue3 / deploy (push) Successful in 1m0s

This commit is contained in:
2026-03-21 19:07:04 +08:00
parent 25d782b050
commit 3357566b02
7 changed files with 665 additions and 200 deletions
+286 -179
View File
@@ -154,7 +154,10 @@
<div class="section-block">
<div class="section-header">
<h3 class="section-title">网络信息</h3>
<el-button size="small" type="primary" @click="handleAddNetwork">添加网络</el-button>
<div style="display: flex; gap: 8px">
<el-button size="small" type="primary" @click="showCreateNetworkDialog">创建网络</el-button>
<el-button size="small" @click="handleAddNetwork">添加已有网络</el-button>
</div>
</div>
<el-table v-if="vmNetworks.length" :data="pagedNetworks" size="small" stripe>
<el-table-column prop="id" label="ID" width="60" />
@@ -187,7 +190,10 @@
<div class="section-block">
<div class="section-header">
<h3 class="section-title">磁盘卷信息</h3>
<el-button size="small" type="primary" @click="handleAddVolume">添加数据卷</el-button>
<div style="display: flex; gap: 8px">
<el-button size="small" type="primary" @click="showCreateVolumeDialog">创建数据卷</el-button>
<el-button size="small" @click="handleAddVolume">挂载已有数据卷</el-button>
</div>
</div>
<el-table v-if="vmVolumes.length" :data="pagedVolumes" size="small" stripe>
<el-table-column prop="id" label="ID" width="60" />
@@ -232,20 +238,31 @@
<div class="section-block">
<div class="section-header">
<h3 class="section-title">安全组管理</h3>
<el-button size="small" type="primary" @click="handleBindSgFromTab">绑定安全组</el-button>
<div style="display: flex; gap: 8px; align-items: center">
<el-select v-model="sgDirectionFilter" placeholder="方向" clearable style="width: 100px" size="small">
<el-option label="入站" value="in" />
<el-option label="出站" value="out" />
</el-select>
<el-button size="small" type="primary" @click="handleBindSgFromTab">绑定安全组</el-button>
</div>
</div>
<el-table v-if="vmSecurityGroups.length" :data="pagedSecurityGroups" size="small" stripe>
<el-table v-if="filteredSecurityGroups.length" :data="pagedSecurityGroups" size="small" stripe>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="name" label="名称" min-width="120" />
<el-table-column prop="note" label="备注" min-width="160" show-overflow-tooltip />
<el-table-column label="方向" width="80">
<template #default="{ row }">
<el-tag :type="row.direction === 'in' ? 'primary' : 'warning'" size="small">{{ row.direction === 'in' ? '入站' : '出站' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="note" label="备注" min-width="140" show-overflow-tooltip />
<el-table-column label="锁定" width="80">
<template #default="{ row }">
<el-tag :type="row.lock ? 'danger' : 'info'" size="small">{{ row.lock ? '已锁定' : '未锁定' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="白名单" width="90">
<el-table-column label="白名单" width="80">
<template #default="{ row }">
<el-tag :type="row.drop_all ? 'warning' : 'info'" size="small">{{ row.drop_all ? '开启' : '未开启' }}</el-tag>
<el-tag :type="row.drop_all ? 'warning' : 'info'" size="small">{{ row.drop_all ? '开启' : '关闭' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="共享" width="80">
@@ -260,9 +277,9 @@
</el-table-column>
</el-table>
<el-empty v-else description="暂无绑定的安全组" :image-size="60" />
<div class="pagination-wrapper" v-if="vmSecurityGroups.length > 0">
<div class="pagination-wrapper" v-if="filteredSecurityGroups.length > 0">
<el-pagination v-model:current-page="securityPage" v-model:page-size="securityPageSize"
:page-sizes="[10, 20, 50]" :total="vmSecurityGroups.length" layout="total, sizes, prev, pager, next" small
:page-sizes="[10, 20, 50]" :total="filteredSecurityGroups.length" layout="total, sizes, prev, pager, next" small
@size-change="s => { securityPageSize = s; securityPage = 1 }"
@current-change="p => { securityPage = p }" />
</div>
@@ -547,13 +564,21 @@
</el-dialog>
<!-- 重构虚拟机弹窗 -->
<el-dialog v-model="refactorDialogVisible" title="重构虚拟机" width="600px" destroy-on-close>
<el-dialog v-model="refactorDialogVisible" title="重构虚拟机" width="650px" destroy-on-close>
<el-alert title="重构会修改虚拟机的底层配置参数请谨慎操作" type="warning" :closable="false" style="margin-bottom: 16px" />
<el-form ref="refactorFormRef" :model="refactorForm" label-width="120px">
<el-form ref="refactorFormRef" :model="refactorForm" label-width="130px">
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="内存(KB)">
<el-input-number v-model="refactorForm.memory" :min="0" :step="1024" controls-position="right" style="width: 100%" />
<el-form-item label="内存">
<div style="display: flex; align-items: center; gap: 6px; width: 100%">
<el-input-number v-model="refactorMemDisplay" :min="0" :step="refactorMemUnit === 1048576 ? 1 : (refactorMemUnit === 1024 ? 512 : 1024)" :precision="refactorMemUnit === 1048576 ? 2 : 0" controls-position="right" style="flex: 1" />
<el-select v-model="refactorMemUnit" style="width: 80px" @change="onRefactorMemUnitChange">
<el-option label="KB" :value="1" />
<el-option label="MB" :value="1024" />
<el-option label="GB" :value="1048576" />
</el-select>
</div>
<span style="color: #909399; font-size: 12px">{{ refactorForm.memory }} KB</span>
</el-form-item>
</el-col>
<el-col :span="12">
@@ -564,12 +589,12 @@
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="下行带宽">
<el-form-item label="下行带宽(Mbps)">
<el-input-number v-model="refactorForm.rx_bandwidth" :min="0" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="上行带宽">
<el-form-item label="上行带宽(Mbps)">
<el-input-number v-model="refactorForm.tx_bandwidth" :min="0" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
@@ -577,6 +602,18 @@
<el-form-item label="Root密码">
<el-input v-model="refactorForm.root_password" placeholder="不修改留空" show-password />
</el-form-item>
<el-form-item label="UUID">
<el-input v-model="refactorForm.uuid" placeholder="虚拟机UUID" />
</el-form-item>
<el-form-item label="Mate Data ID">
<el-input v-model="refactorForm.mate_data_id" placeholder="元数据ID" />
</el-form-item>
<el-form-item label="物理机名称">
<el-input v-model="refactorForm.physical_name" placeholder="不修改留空" />
</el-form-item>
<el-form-item label="配置路径">
<el-input v-model="refactorForm.config_path" placeholder="不修改留空" />
</el-form-item>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="SSH端口">
@@ -592,6 +629,21 @@
<el-form-item label="VNC密码">
<el-input v-model="refactorForm.vnc_password" placeholder="不填随机" show-password />
</el-form-item>
<el-form-item label="网络">
<div style="width: 100%">
<div style="display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 6px" v-if="refactorSelectedNetworks.length">
<el-tag v-for="n in refactorSelectedNetworks" :key="n.id" closable size="small" @close="removeRefactorNetwork(n.id)">
{{ n.name }} (ID:{{ n.id }})
</el-tag>
</div>
<el-button size="small" @click="showRefactorNetworkSelector = true">选择网络</el-button>
</div>
</el-form-item>
<el-form-item label="外网网络">
<el-select v-model="refactorForm.internet_network_id" placeholder="选择外网网络可选" clearable filterable style="width: 100%">
<el-option v-for="n in refactorSelectedNetworks" :key="n.id" :label="`${n.name} (ID: ${n.id})`" :value="n.id" />
</el-select>
</el-form-item>
<el-form-item label="安全组">
<el-select v-model="refactorForm.port_group_id" placeholder="选择安全组可选" filterable clearable style="width: 100%">
<el-option v-for="g in sgOptions" :key="g.id" :label="`${g.name} (ID: ${g.id})`" :value="g.id" />
@@ -604,6 +656,9 @@
</template>
</el-dialog>
<!-- 重构用网络选择器 -->
<NetworkSelectorPopup v-model="showRefactorNetworkSelector" :service-id="serviceId" :host-id="vmHostId" @confirm="handleRefactorNetworkConfirm" />
<!-- VNC 连接弹窗 -->
<el-dialog v-model="vncDialogVisible" title="获取 VNC 连接" width="560px" destroy-on-close>
<el-form label-width="100px">
@@ -629,22 +684,8 @@
</el-dialog>
<!-- 绑定/解绑安全组弹窗 -->
<el-dialog v-model="sgDialogVisible" :title="sgDialogType === 'bind' ? '绑定安全组' : '解绑安全组'" width="480px" destroy-on-close>
<el-form label-width="100px">
<el-form-item label="虚拟机">{{ detail?.name || '-' }}</el-form-item>
<el-form-item label="安全组">
<el-select v-model="sgSelectedId" placeholder="选择安全组" filterable style="width: 100%">
<el-option v-for="g in sgOptions" :key="g.id" :label="`${g.name} (ID: ${g.id})`" :value="g.id" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="sgDialogVisible = false">取消</el-button>
<el-button :type="sgDialogType === 'bind' ? 'success' : 'warning'" :loading="actionLoading" @click="submitSgAction">
{{ sgDialogType === 'bind' ? '绑定' : '解绑' }}
</el-button>
</template>
</el-dialog>
<!-- 绑定安全组(弹窗选择组件) -->
<SecurityGroupSelectorPopup v-model="sgBindSelectorVisible" :service-id="serviceId" @confirm="handleSgBindConfirm" />
<!-- 编辑网络弹窗 -->
<el-dialog v-model="netEditVisible" title="编辑网络" width="560px" destroy-on-close>
@@ -695,62 +736,78 @@
</template>
</el-dialog>
<!-- 添加网络弹窗(从已有列表选择) -->
<el-dialog v-model="netAddVisible" title="添加网络到虚拟机" width="600px" destroy-on-close>
<el-form label-width="100px">
<el-form-item label="选择宿主机">
<el-select v-model="netAddHostId" placeholder="选择宿主机以加载网络列表" filterable style="width: 100%" @change="loadAvailableNetworks">
<el-option v-for="h in hostOptions" :key="h.id" :label="`${h.name} (${h.ip || h.id})`" :value="h.id" />
<!-- 创建网络弹窗 -->
<el-dialog v-model="netCreateVisible" title="创建网络" width="600px" destroy-on-close>
<el-form ref="netCreateFormRef" :model="netCreateForm" :rules="netCreateRules" label-width="120px">
<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="选择网络">
<el-select v-model="netAddSelectedId" placeholder="请先选择宿主机" filterable style="width: 100%" :loading="netOptionsLoading" :disabled="!netAddHostId">
<el-option v-for="n in availableNetworks" :key="n.id" :label="`${n.name} - ${n.address || ''}`" :value="n.id" />
</el-select>
<el-form-item label="IP 地址(CIDR)" prop="address">
<el-input v-model="netCreateForm.address" placeholder="例如 192.168.1.0/24" />
</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>
<el-divider content-position="left">高级配置(可选)</el-divider>
<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>
</el-form>
<div v-if="netAddSelectedId" style="margin-top: 8px">
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="名称">{{ selectedNetworkInfo?.name || '-' }}</el-descriptions-item>
<el-descriptions-item label="IP地址">{{ selectedNetworkInfo?.address || '-' }}</el-descriptions-item>
<el-descriptions-item label="网关">{{ selectedNetworkInfo?.gateway || '-' }}</el-descriptions-item>
<el-descriptions-item label="类型">{{ selectedNetworkInfo?.type || '-' }}</el-descriptions-item>
</el-descriptions>
</div>
<template #footer>
<el-button @click="netAddVisible = false">取消</el-button>
<el-button type="primary" :loading="actionLoading" @click="submitAddNetwork" :disabled="!netAddSelectedId">添加</el-button>
<el-button @click="netCreateVisible = false">取消</el-button>
<el-button type="primary" :loading="actionLoading" @click="submitCreateNetwork">创建</el-button>
</template>
</el-dialog>
<!-- 挂载数据卷弹窗(从已有列表选择) -->
<el-dialog v-model="volAddVisible" title="挂载数据卷到虚拟机" width="600px" destroy-on-close>
<el-form label-width="100px">
<el-form-item label="选择宿主机">
<el-select v-model="volAddHostId" placeholder="选择宿主机以加载数据卷列表" filterable style="width: 100%" @change="loadAvailableVolumes">
<el-option v-for="h in hostOptions" :key="h.id" :label="`${h.name} (${h.ip || h.id})`" :value="h.id" />
</el-select>
<!-- 添加已有网络弹窗 -->
<NetworkSelectorPopup v-model="netAddVisible" :service-id="serviceId" :host-id="vmHostId" @confirm="handleNetworkSelectorConfirm" />
<!-- 创建数据卷弹窗 -->
<el-dialog v-model="volCreateVisible" title="创建数据卷" width="550px" destroy-on-close>
<el-form ref="volCreateFormRef" :model="volCreateForm" :rules="volCreateRules" label-width="120px">
<el-form-item label="名称" prop="name">
<el-input v-model="volCreateForm.name" placeholder="数据卷名称" />
</el-form-item>
<el-form-item label="选择数据卷">
<el-select v-model="volAddSelectedId" placeholder="请先选择宿主机" filterable style="width: 100%" :loading="volOptionsLoading" :disabled="!volAddHostId">
<el-option v-for="v in availableVolumes" :key="v.id" :label="`${v.name} (${v.size || 0} GB, ID: ${v.id})`" :value="v.id" />
</el-select>
<el-form-item label="大小(GB)" prop="size">
<el-input-number v-model="volCreateForm.size" :min="1" :step="10" style="width: 100%" />
</el-form-item>
<el-form-item label="是否系统镜像">
<el-switch v-model="volCreateForm.is_system" />
</el-form-item>
<el-form-item label="挂载设备名">
<el-input v-model="volCreateForm.target_device" placeholder="不填自动生成" />
</el-form-item>
<el-form-item label="所属镜像ID">
<el-input-number v-model="volCreateForm.image_id" :min="0" style="width: 100%" placeholder="镜像ID" />
</el-form-item>
</el-form>
<div v-if="volAddSelectedId" style="margin-top: 8px">
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="名称">{{ selectedVolumeInfo?.name || '-' }}</el-descriptions-item>
<el-descriptions-item label="大小">{{ selectedVolumeInfo?.size ? selectedVolumeInfo.size + ' GB' : '-' }}</el-descriptions-item>
<el-descriptions-item label="状态">{{ selectedVolumeInfo?.status || '-' }}</el-descriptions-item>
<el-descriptions-item label="路径">{{ selectedVolumeInfo?.path || '-' }}</el-descriptions-item>
</el-descriptions>
</div>
<template #footer>
<el-button @click="volAddVisible = false">取消</el-button>
<el-button type="primary" :loading="actionLoading" @click="submitAddVolume" :disabled="!volAddSelectedId">挂载</el-button>
<el-button @click="volCreateVisible = false">取消</el-button>
<el-button type="primary" :loading="actionLoading" @click="submitCreateVolume">创建</el-button>
</template>
</el-dialog>
<!-- 挂载已有数据卷弹窗 -->
<VolumeSelectorPopup v-model="volAddVisible" :service-id="serviceId" :host-id="vmHostId" @confirm="handleVolumeSelectorConfirm" />
<!-- 迁移卷弹窗 -->
<el-dialog v-model="volTransferVisible" title="迁移数据卷" width="480px" destroy-on-close>
<el-form :model="volTransferForm" label-width="120px">
@@ -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 }
}