fex: 样式修改
This commit is contained in:
@@ -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 }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user