fix: 虚拟机模块
This commit is contained in:
@@ -70,17 +70,24 @@
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<div class="config-cell"><span class="config-label">用户名</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">{{ isWindows ? (vm.ssh_port || 3389) : (vm.ssh_port || 22) }}</span></div>
|
||||
<div class="config-cell"><span class="config-label">远程端口</span><span class="config-value">{{ isWindows ? (vm.ssh_port && vm.ssh_port !== 22 ? vm.ssh_port : 3389) : (vm.ssh_port || 22) }}</span></div>
|
||||
<div class="config-cell">
|
||||
<span class="config-label">外网IP</span>
|
||||
<span class="config-value" style="color:#165dff;font-weight:500" v-if="vmPublicIpList.length">
|
||||
{{ vmPublicIpList[0] }}
|
||||
<el-popover v-if="vmPublicIpList.length > 1" trigger="hover" placement="bottom-start" :width="280">
|
||||
<span class="config-value ip-value" v-if="vmPublicIpList.length">
|
||||
<span class="ip-main" style="color:#165dff">{{ vmPublicIpList[0] }}</span>
|
||||
<el-popover v-if="vmPublicIpList.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">+{{ vmPublicIpList.length - 1 }}</el-tag>
|
||||
<el-tag size="small" type="primary" class="ip-more-tag">+{{ vmPublicIpList.length - 1 }}</el-tag>
|
||||
</template>
|
||||
<div class="ip-popover-header">
|
||||
<span>全部外网IP({{ vmPublicIpList.length }})</span>
|
||||
<el-button link type="primary" size="small" @click="copyAllIps(vmPublicIpList)">复制全部</el-button>
|
||||
</div>
|
||||
<div class="ip-popover-list">
|
||||
<div v-for="(ip, idx) in vmPublicIpList" :key="idx" class="ip-popover-item">{{ ip }}</div>
|
||||
<div v-for="(ip, idx) in vmPublicIpList" :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>
|
||||
@@ -88,14 +95,21 @@
|
||||
</div>
|
||||
<div class="config-cell">
|
||||
<span class="config-label">内网IP</span>
|
||||
<span class="config-value" style="color:#67c23a;font-weight:500" v-if="vmPrivateIpList.length">
|
||||
{{ vmPrivateIpList[0] }}
|
||||
<el-popover v-if="vmPrivateIpList.length > 1" trigger="hover" placement="bottom-start" :width="280">
|
||||
<span class="config-value ip-value" v-if="vmPrivateIpList.length">
|
||||
<span class="ip-main" style="color:#67c23a">{{ vmPrivateIpList[0] }}</span>
|
||||
<el-popover v-if="vmPrivateIpList.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">+{{ vmPrivateIpList.length - 1 }}</el-tag>
|
||||
<el-tag size="small" type="success" class="ip-more-tag">+{{ vmPrivateIpList.length - 1 }}</el-tag>
|
||||
</template>
|
||||
<div class="ip-popover-header">
|
||||
<span>全部内网IP({{ vmPrivateIpList.length }})</span>
|
||||
<el-button link type="primary" size="small" @click="copyAllIps(vmPrivateIpList)">复制全部</el-button>
|
||||
</div>
|
||||
<div class="ip-popover-list">
|
||||
<div v-for="(ip, idx) in vmPrivateIpList" :key="idx" class="ip-popover-item">{{ ip }}</div>
|
||||
<div v-for="(ip, idx) in vmPrivateIpList" :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>
|
||||
@@ -172,16 +186,26 @@
|
||||
<el-table :data="vmVolumes" stripe size="small">
|
||||
<el-table-column prop="id" label="ID" width="70" />
|
||||
<el-table-column prop="name" label="名称" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="大小" width="80"><template #default="{ row }">{{ row.size }} GB</template></el-table-column>
|
||||
<el-table-column label="类型" width="80"><template #default="{ row }"><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="volumeStatusType(row.status)" size="small">{{ volumeStatusLabel(row.status) }}</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="100">
|
||||
<template #default="{ row }">{{ row.size ? (row.size + ' GB') : '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" width="80">
|
||||
<template #default="{ row }"><el-tag :type="row.is_system ? 'danger' : ''" size="small">{{ row.is_system ? '系统盘' : '数据盘' }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<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="200" show-overflow-tooltip><template #default="{ row }"><span style="font-family:monospace;font-size:12px">{{ row.path || '-' }}</span></template></el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="handleResizeVolume(row)">扩容</el-button>
|
||||
<el-button link type="success" size="small" @click="handleMountVolume(row)" v-if="!row.is_mount">挂载</el-button>
|
||||
<el-button link type="warning" size="small" @click="handleUnmountVolume(row)" v-if="row.is_mount">卸载</el-button>
|
||||
<el-button link type="success" size="small" @click="handleMountVolume(row)" v-if="!isVolumeMounted(row)">挂载</el-button>
|
||||
<el-button link type="warning" size="small" @click="handleUnmountVolume(row)" v-if="isVolumeMounted(row)">卸载</el-button>
|
||||
<el-button link type="danger" size="small" @click="handleDeleteVolume(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -1034,6 +1058,30 @@ const copyPassword = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const clipCopy = async (text) => {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(text)
|
||||
} else {
|
||||
const ta = document.createElement('textarea')
|
||||
ta.value = text
|
||||
ta.style.cssText = 'position:fixed;left:-9999px;top:-9999px;opacity:0'
|
||||
document.body.appendChild(ta)
|
||||
ta.focus(); ta.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(ta)
|
||||
}
|
||||
}
|
||||
|
||||
const copyText = async (text) => {
|
||||
if (!text) return
|
||||
try { await clipCopy(text); ElMessage.success('已复制') } catch { ElMessage.error('复制失败') }
|
||||
}
|
||||
|
||||
const copyAllIps = async (ipList) => {
|
||||
if (!ipList?.length) return
|
||||
try { await clipCopy(ipList.join('\n')); ElMessage.success(`已复制 ${ipList.length} 个IP`) } catch { ElMessage.error('复制失败') }
|
||||
}
|
||||
|
||||
const isWindows = computed(() => vmImage.value?.os_type === 'windows')
|
||||
|
||||
const vmPublicIpList = computed(() => {
|
||||
@@ -1202,6 +1250,10 @@ const handleMoreCmd = (cmd) => {
|
||||
}
|
||||
|
||||
// ---- 数据卷 ----
|
||||
const isVolumeMounted = (row) => {
|
||||
if (row.is_mount !== undefined) return !!row.is_mount
|
||||
return row.status === 'ready'
|
||||
}
|
||||
const showVolumeSelector = ref(false)
|
||||
const volumes = ref([])
|
||||
const volumeLoading = ref(false)
|
||||
@@ -2183,7 +2235,14 @@ onBeforeUnmount(() => { disposeCharts() })
|
||||
.metric-summary-label { font-size: 12px; color: #86909c; margin-bottom: 8px; }
|
||||
.metric-summary-value { font-size: 22px; font-weight: 600; color: #1d2129; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.metric-summary-sub { font-size: 12px; color: #86909c; margin-top: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.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-value { display: inline-flex; align-items: center; gap: 4px; }
|
||||
.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; }
|
||||
.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>
|
||||
|
||||
Reference in New Issue
Block a user