@@ -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 " typ e="primary " @click=" handleAddVolume ">添加数据卷</el-button >
<div s tyl e=" display : flex ; gap : 8 px " >
<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 " typ e="primary " @click=" handleBindSgFromTab ">绑定安全组</el-button >
<div s tyl e=" display : flex ; gap : 8 px ; align - items : center " >
<el-select v-model=" sgDirectionFilter " placeholder=" 方向 " clearable style=" width : 100 px " 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=" 600 px " destroy-on-close>
<el-dialog v-model=" refactorDialogVisible " title=" 重构虚拟机 " width=" 650 px " destroy-on-close>
<el-alert title=" 重构会修改虚拟机的底层配置参数 , 请谨慎操作 ! " type=" warning " :closable=" false " style=" margin - bottom : 16 px " />
<el-form ref=" refactorFormRef " :model=" refactorForm " label-width=" 120 px ">
<el-form ref=" refactorFormRef " :model=" refactorForm " label-width=" 130 px ">
<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 : 6 px ; 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 : 80 px " @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 : 12 px ">( {{ 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 : 6 px ; flex - wrap : wrap ; margin - bottom : 6 px " 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=" 560 px " destroy-on-close>
<el-form label-width=" 100 px ">
@@ -629,22 +684,8 @@
</el-dialog>
<!-- 绑定/解绑安全组弹窗 -->
<el-dialog v-model=" sgDialogVisible " :title=" sgDialogType === 'bind' ? '绑定安全组' : '解绑安全组' " width=" 480 px " destroy-on-close >
<el-form label-width=" 100 px " >
<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=" 560 px " destroy-on-close>
@@ -695,62 +736,78 @@
</template>
</el-dialog>
<!-- 添加 网络弹窗(从已有列表选择) -->
<el-dialog v-model=" netAddVisible " title=" 添加网络到虚拟机 " width=" 600 px " destroy-on-close>
<el-form label-width=" 100 px ">
<el-form-item label=" 选择宿主机 ">
<el-selec t 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=" 600 px " destroy-on-close>
<el-form ref=" netCreateFormRef " :model=" netCreateForm " :rules=" netCreateRules " label-width="120 px ">
<el-form-item label=" 名称 " prop=" name ">
<el-inpu t 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-selec t 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-inpu t 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 : 8 px ">
<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 ="600 px " destroy-on-close >
<el-form label-width=" 100 px ">
<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=" 550 px " destroy-on-close >
<el-form ref=" volCreateFormRef " :model=" volCreateForm " :rules=" volCreateRules " label-width=" 120 px " >
<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 : 8 px ">
<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=" 480 px " destroy-on-close>
<el-form :model=" volTransferForm " label-width=" 120 px ">
@@ -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_p ort || 0 ,
vnc_port: 0, vnc_password: '',
port_group_id: vmPortGroup.value?.id || null
root_password: d.root_passw ord || '' ,
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 vm SecurityGroups.value.slice(start, start + securityPageSize.value)
return filtered SecurityGroups.value.slice(start, start + securityPageSize.value)
})
const loadVmSecurityGroups = async () => {
@@ -1350,9 +1454,7 @@ const loadVmSecurityGroups = async () => {
}
// ---- 绑定/解绑安全组 ----
const sgDialog Visible = ref(false)
const sgDialogType = ref('bind')
const sgSelectedId = ref(null)
const sgBindSelector Visible = 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 handleSg BindConfirm = 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 }
}