fix: 虚拟机模块
Build and Deploy Vue3 / build (push) Successful in 1m26s
Build and Deploy Vue3 / deploy (push) Successful in 3m5s

This commit is contained in:
2026-04-16 13:22:39 +08:00
parent f53f63e679
commit 985412c3bc
5 changed files with 206 additions and 57 deletions
+101 -30
View File
@@ -56,17 +56,33 @@
<div class="status-item">
<span class="status-label">IP地址</span>
<span class="status-value" v-if="publicIpList.length || privateIpList.length">
{{ (publicIpList[0] || privateIpList[0]) }}
<el-popover v-if="publicIpList.length + privateIpList.length > 1" trigger="hover" placement="bottom-start" :width="300">
<span class="ip-main">{{ (publicIpList[0] || privateIpList[0]) }}</span>
<el-popover v-if="publicIpList.length + privateIpList.length > 1" trigger="hover" placement="bottom" :width="360" popper-class="ip-popover-panel">
<template #reference>
<el-tag size="small" type="info" style="margin-left:4px;cursor:pointer;vertical-align:middle">+{{ publicIpList.length + privateIpList.length - 1 }}</el-tag>
<el-tag size="small" type="info" class="ip-more-tag">+{{ publicIpList.length + privateIpList.length - 1 }}</el-tag>
</template>
<div class="ip-popover-list">
<div v-if="publicIpList.length" style="font-size:12px;color:#909399;margin-bottom:4px">公网IP</div>
<div v-for="(ip, idx) in publicIpList" :key="'pub'+idx" class="ip-popover-item">{{ ip }}</div>
<div v-if="privateIpList.length" style="font-size:12px;color:#909399;margin:8px 0 4px">内网IP</div>
<div v-for="(ip, idx) in privateIpList" :key="'pri'+idx" class="ip-popover-item">{{ ip }}</div>
<div v-if="publicIpList.length" class="ip-popover-header">
<span>公网IP{{ publicIpList.length }}</span>
<el-button link type="primary" size="small" @click="copyAllIps(publicIpList)">复制全部</el-button>
</div>
<div class="ip-popover-list">
<div v-for="(ip, idx) in publicIpList" :key="'pub'+idx" class="ip-popover-item">
<span class="ip-text">{{ ip }}</span>
<el-button link type="primary" size="small" class="ip-copy-btn" @click="copyText(ip)">复制</el-button>
</div>
</div>
<template v-if="privateIpList.length">
<div class="ip-popover-header" style="margin-top:8px">
<span>内网IP{{ privateIpList.length }}</span>
<el-button link type="primary" size="small" @click="copyAllIps(privateIpList)">复制全部</el-button>
</div>
<div class="ip-popover-list">
<div v-for="(ip, idx) in privateIpList" :key="'pri'+idx" class="ip-popover-item">
<span class="ip-text">{{ ip }}</span>
<el-button link type="primary" size="small" class="ip-copy-btn" @click="copyText(ip)">复制</el-button>
</div>
</div>
</template>
</el-popover>
</span>
<span class="status-value" v-else>{{ detail.ips || '-' }}</span>
@@ -127,13 +143,20 @@
<div class="config-cell">
<span class="config-label">公网IP</span>
<span class="config-value ip-value" v-if="publicIpList.length">
{{ publicIpList[0] }}
<el-popover v-if="publicIpList.length > 1" trigger="hover" placement="bottom-start" :width="300">
<span class="ip-main" style="color:#165dff">{{ publicIpList[0] }}</span>
<el-popover v-if="publicIpList.length > 1" trigger="hover" placement="bottom" :width="360" popper-class="ip-popover-panel">
<template #reference>
<el-tag size="small" type="primary" style="margin-left:4px;cursor:pointer;vertical-align:middle">+{{ publicIpList.length - 1 }}</el-tag>
<el-tag size="small" type="primary" class="ip-more-tag">+{{ publicIpList.length - 1 }}</el-tag>
</template>
<div class="ip-popover-header">
<span>全部公网IP{{ publicIpList.length }}</span>
<el-button link type="primary" size="small" @click="copyAllIps(publicIpList)">复制全部</el-button>
</div>
<div class="ip-popover-list">
<div v-for="(ip, idx) in publicIpList" :key="idx" class="ip-popover-item">{{ ip }}</div>
<div v-for="(ip, idx) in publicIpList" :key="idx" class="ip-popover-item">
<span class="ip-text">{{ ip }}</span>
<el-button link type="primary" size="small" class="ip-copy-btn" @click="copyText(ip)">复制</el-button>
</div>
</div>
</el-popover>
</span>
@@ -142,13 +165,20 @@
<div class="config-cell">
<span class="config-label">内网IP</span>
<span class="config-value ip-value" v-if="privateIpList.length">
{{ privateIpList[0] }}
<el-popover v-if="privateIpList.length > 1" trigger="hover" placement="bottom-start" :width="300">
<span class="ip-main" style="color:#67c23a">{{ privateIpList[0] }}</span>
<el-popover v-if="privateIpList.length > 1" trigger="hover" placement="bottom" :width="360" popper-class="ip-popover-panel">
<template #reference>
<el-tag size="small" type="success" style="margin-left:4px;cursor:pointer;vertical-align:middle">+{{ privateIpList.length - 1 }}</el-tag>
<el-tag size="small" type="success" class="ip-more-tag">+{{ privateIpList.length - 1 }}</el-tag>
</template>
<div class="ip-popover-header">
<span>全部内网IP{{ privateIpList.length }}</span>
<el-button link type="primary" size="small" @click="copyAllIps(privateIpList)">复制全部</el-button>
</div>
<div class="ip-popover-list">
<div v-for="(ip, idx) in privateIpList" :key="idx" class="ip-popover-item">{{ ip }}</div>
<div v-for="(ip, idx) in privateIpList" :key="idx" class="ip-popover-item">
<span class="ip-text">{{ ip }}</span>
<el-button link type="primary" size="small" class="ip-copy-btn" @click="copyText(ip)">复制</el-button>
</div>
</div>
</el-popover>
</span>
@@ -174,11 +204,11 @@
<div class="config-row">
<div class="config-cell">
<span class="config-label">用户名</span>
<span class="config-value">root</span>
<span class="config-value" style="font-weight:500">{{ isWindows ? 'Administrator' : 'root' }}</span>
</div>
<div class="config-cell">
<span class="config-label">远程端口</span>
<span class="config-value">{{ detail.ssh_port || 22 }}</span>
<span class="config-value">{{ isWindows ? (detail.ssh_port && detail.ssh_port !== 22 ? detail.ssh_port : 3389) : (detail.ssh_port || 22) }}</span>
</div>
<div class="config-cell">
<span class="config-label">密码</span>
@@ -192,7 +222,7 @@
<div class="config-row">
<div class="config-cell">
<span class="config-label">流量上限</span>
<span class="config-value">{{ detail.traffic_max != null ? `${(detail.traffic_max / 1024).toFixed(2)} GB` : '-' }}</span>
<span class="config-value">{{ formatTrafficMax(detail.traffic_max) }}</span>
</div>
<div class="config-cell">
<span class="config-label">快照配额</span>
@@ -280,16 +310,16 @@
<el-tag :type="row.is_system ? 'danger' : ''" size="small">{{ row.is_system ? '系统盘' : '数据盘' }}</el-tag>
</template>
</el-table-column>
<!-- <el-table-column label="挂载" width="80">
<template #default="{ row }">
<el-tag :type="row.is_mount ? 'success' : 'info'" size="small">{{ row.is_mount ? '已挂载' : '未挂载' }}</el-tag>
</template>
</el-table-column> -->
<el-table-column label="状态" width="80">
<el-table-column label="状态" width="90">
<template #default="{ row }">
<el-tag :type="volumeStatusType(row.status)" size="small">{{ volumeStatusLabel(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="挂载" width="90">
<template #default="{ row }">
<el-tag :type="isVolumeMounted(row) ? 'success' : 'info'" size="small">{{ isVolumeMounted(row) ? '已挂载' : '未挂载' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="path" label="路径" min-width="160" show-overflow-tooltip>
<template #default="{ row }"><span class="mono-text">{{ row.path || '-' }}</span></template>
</el-table-column>
@@ -297,8 +327,8 @@
<template #default="{ row }">
<el-button link type="primary" size="small" @click="handleVolDetail(row)">详情</el-button>
<el-button link type="primary" size="small" @click="handleVolResize(row)">调整大小</el-button>
<!-- <el-button link type="success" size="small" @click="handleVolMount(row)" v-if="!row.is_mount">挂载</el-button> -->
<el-button link type="warning" size="small" @click="handleVolUnmount(row)" v-if="row.is_mount">卸载</el-button>
<el-button link type="success" size="small" @click="handleVolMount(row)" v-if="!isVolumeMounted(row)">挂载</el-button>
<el-button link type="warning" size="small" @click="handleVolUnmount(row)" v-if="isVolumeMounted(row)">卸载</el-button>
<el-button link type="info" size="small" @click="handleVolTransfer(row)">迁移</el-button>
<el-button link type="danger" size="small" @click="handleVolDelete(row)">删除</el-button>
</template>
@@ -1517,12 +1547,36 @@ const showImageSelector = ref(false)
const activeTab = ref('info')
const showPassword = ref(false)
const isWindows = computed(() => vmImage.value?.os_type === 'windows')
const extractIp = (addr) => addr ? addr.split('/')[0] : ''
const publicIpList = computed(() => vmNetworks.value.filter(n => n.type === 'bridge').map(n => extractIp(n.address)).filter(Boolean))
const privateIpList = computed(() => vmNetworks.value.filter(n => n.type === 'nat').map(n => extractIp(n.address)).filter(Boolean))
const publicIps = computed(() => publicIpList.value.join(', '))
const privateIps = computed(() => privateIpList.value.join(', '))
const isVolumeMounted = (row) => {
if (row.is_mount !== undefined) return !!row.is_mount
return row.status === 'ready'
}
const formatTrafficMax = (val) => {
if (val == null) return '-'
const gb = val / 1024
if (gb >= 1024) return `${(gb / 1024).toFixed(2)} TB`
return `${gb.toFixed(2)} GB`
}
const copyAllIps = (ipList) => {
if (!ipList?.length) return
const text = ipList.join('\n')
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(text).then(() => ElMessage.success(`已复制 ${ipList.length} 个IP`)).catch(() => fallbackCopy(text))
} else {
fallbackCopy(text)
}
}
const networkPage = ref(1)
const networkPageSize = ref(10)
const pagedNetworks = computed(() => {
@@ -1606,7 +1660,18 @@ const loadDetail = async () => {
detail.value = d.data ?? d.vm ?? d
vmNetworks.value = d.networks || []
vmVolumes.value = d.volumes || []
vmImage.value = d.image || null
if (d.image) {
vmImage.value = d.image
} else {
const sysVol = (d.volumes || []).find(v => v.is_system)
if (sysVol) {
const name = sysVol.name || ''
const osType = /windows/i.test(name) ? 'windows' : 'linux'
vmImage.value = { id: 0, name, os_type: osType, status: sysVol.status }
} else {
vmImage.value = null
}
}
vmPortGroup.value = d.in_port_group || null
vmOutPortGroup.value = d.out_port_group || null
vmHostId.value = detail.value?.host_id || vmVolumes.value[0]?.host_id || vmNetworks.value[0]?.host_id || 0
@@ -3517,7 +3582,13 @@ onMounted(() => { isPageActive = true; initPage() })
.rules-section { margin-top: 8px; }
.rules-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
.rules-header h4 { margin: 0; font-size: 15px; font-weight: 600; color: #303133; }
.ip-popover-list { max-height: 200px; overflow-y: auto; }
.ip-popover-item { padding: 4px 0; font-size: 13px; color: #303133; border-bottom: 1px dashed #ebeef5; word-break: break-all; }
.ip-main { font-weight: 500; font-family: 'SF Mono', Consolas, monospace; font-size: 13px; }
.ip-more-tag { cursor: pointer; vertical-align: middle; flex-shrink: 0; margin-left: 4px; }
.ip-popover-header { display: flex; justify-content: space-between; align-items: center; padding-bottom: 8px; border-bottom: 1px solid #ebeef5; margin-bottom: 4px; font-size: 13px; color: #606266; }
.ip-popover-list { max-height: 240px; overflow-y: auto; scrollbar-width: none; -ms-overflow-style: none; }
.ip-popover-list::-webkit-scrollbar { display: none; }
.ip-popover-item { display: flex; justify-content: space-between; align-items: center; padding: 6px 0; border-bottom: 1px solid #f2f3f5; }
.ip-popover-item:last-child { border-bottom: none; }
.ip-popover-item .ip-text { font-family: 'SF Mono', Consolas, monospace; font-size: 13px; color: #303133; word-break: break-all; }
.ip-popover-item .ip-copy-btn { flex-shrink: 0; margin-left: 8px; }
</style>