diff --git a/src/utils/tool.js b/src/utils/tool.js index a0a8131..40f0fb7 100644 --- a/src/utils/tool.js +++ b/src/utils/tool.js @@ -182,10 +182,16 @@ export function vmStatusType(status) { // ========== 磁盘状态映射 ========== const VOLUME_STATUS_MAP = { - pending: { label: '等待中', type: 'info' }, - ready: { label: '就绪', type: 'success' }, - error: { label: '错误', type: 'danger' }, - unknown: { label: '未知', type: 'info' } + pending: { label: '等待中', type: 'info' }, + creating: { label: '创建中', type: 'warning' }, + ready: { label: '就绪', type: 'success' }, + in_use: { label: '使用中', type: 'success' }, + attaching: { label: '挂载中', type: 'warning' }, + detaching: { label: '卸载中', type: 'warning' }, + resizing: { label: '扩容中', type: 'warning' }, + deleting: { label: '删除中', type: 'danger' }, + error: { label: '错误', type: 'danger' }, + unknown: { label: '未知', type: 'info' } } /** diff --git a/src/views/product/ProductGroup.vue b/src/views/product/ProductGroup.vue index 0097e70..934f5dd 100644 --- a/src/views/product/ProductGroup.vue +++ b/src/views/product/ProductGroup.vue @@ -595,6 +595,9 @@
all: 所有参数 / plan: 套餐 / customize: 自定义
+ + + - - + + + + @@ -297,8 +327,8 @@ @@ -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; } diff --git a/src/views/virtualization/VmManage.vue b/src/views/virtualization/VmManage.vue index 5caea91..7b9e879 100644 --- a/src/views/virtualization/VmManage.vue +++ b/src/views/virtualization/VmManage.vue @@ -184,6 +184,9 @@ 0 表示不创建 + + +
@@ -775,6 +778,11 @@ const submitCreate = () => { createFormRef.value?.validate(async (valid) => { if (!valid) return + if (createForm.system_size > 0 && createForm.system_size < 30) { + try { + await ElMessageBox.confirm('系统盘小于 30 GB,可能无法正常重装 Windows 系统,是否继续?', '系统盘容量提示', { type: 'warning', confirmButtonText: '继续创建', cancelButtonText: '返回修改' }) + } catch { return } + } submitLoading.value = true try { const fd = new FormData()