@@ -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 = "vm SecurityGroups.length" :data="pagedSecurityGroups" size="small" stripe >
< el-table v-if = "filtered SecurityGroups.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 = "9 0" >
< el-table-column label = "白名单" width = "8 0" >
< 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 = "vm SecurityGroups.length > 0" >
< div class = "pagination-wrapper" v-if = "filtered SecurityGroups.length > 0" >
< el-pagination v -model :current-page = "securityPage" v -model :page-size = "securityPageSize"
: page - sizes = "[10, 20, 50]" : total = "vm SecurityGroups.length" layout = "total, sizes, prev, pager, next" small
: page - sizes = "[10, 20, 50]" : total = "filtered SecurityGroups.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="60 0px" destroy -on -close >
< el-dialog v-model = "refactorDialogVisible" title="重构虚拟机" width="65 0px" 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" co ntrols-position="right" style=" width: 100%" / >
< el-form-item label = "内存" >
< div style = "display: flex; align-items: ce nter; 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 = "netAdd Visible" 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 = "netCreate Visible" 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 ="netAdd Visible = false" > 取消 < / el -button >
< el-button type = "primary" :loading = "actionLoading" @click ="submitAdd Network" :disabled = "!netAddSelectedId" > 添加 < / el-button >
< el-button @click ="netCreate Visible = false" > 取消 < / el -button >
< el-button type = "primary" :loading = "actionLoading" @click ="submitCreate Network" > 创建 < / el -button >
< / template >
< / el-dialog >
<!-- 挂载数据卷弹窗 ( 从已有列表选择 ) -- >
< el-dialog v-model = "vol AddVisible" 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 = "net AddVisible" :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 ="volAdd Visible = false" > 取消 < / el -button >
< el-button type = "primary" :loading = "actionLoading" @click ="submitAddVolume" :disabled = "!volAddSelectedId "> 挂载 < / el-button >
< el-button @click ="volCreate Visible = 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 }
}