fix(structure): 结构优化

This commit is contained in:
it民工
2025-11-26 11:49:36 +08:00
parent 3bf90583ca
commit bb2c5645ef
6 changed files with 1464 additions and 1374 deletions

Binary file not shown.

View File

@@ -0,0 +1,50 @@
<template>
<div class="vm-config-tab">
<a-card :bordered="false">
<a-empty v-if="!configEntries.length" description="暂无配置数据" />
<a-descriptions v-else :column="1" bordered>
<a-descriptions-item
v-for="item in configEntries"
:key="item.label"
:label="item.label"
>
<pre class="config-pre">{{ item.value }}</pre>
</a-descriptions-item>
</a-descriptions>
</a-card>
</div>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
vm: {
type: Object,
required: true
}
})
const configEntries = computed(() => {
const config = props.vm?.pve_config || {}
return Object.keys(config).map(key => ({
label: key,
value: typeof config[key] === 'object' ? JSON.stringify(config[key], null, 2) : String(config[key])
}))
})
</script>
<style scoped>
.vm-config-tab {
width: 100%;
}
.config-pre {
margin: 0;
white-space: pre-wrap;
font-family: var(--font-family-code);
font-size: 12px;
line-height: 1.4;
}
</style>

View File

@@ -0,0 +1,278 @@
<template>
<div class="vm-console-tab">
<a-card :bordered="false">
<a-alert type="info" show-icon style="margin-bottom: 12px;">
<template #title>提示</template>
控制台通过本系统代理访问 PVE无需手动登录 PVE但首次加载可能需要几秒钟
</a-alert>
<div class="pve-console-wrapper">
<div v-if="consoleLoading" class="novnc-placeholder">
<a-spin />
<p style="margin-top: 12px;">正在建立控制台会话...</p>
</div>
<div v-else-if="consoleError" class="novnc-placeholder">
<p>{{ consoleError }}</p>
<a-button type="text" @click="initConsole">重试</a-button>
</div>
<!-- noVNC 容器始终存在通过样式控制显示 -->
<div
ref="novncContainer"
:id="`noVNC_container_${vmId}`"
class="novnc-container"
:style="{ display: consoleLoading || consoleError ? 'none' : 'flex' }"
></div>
</div>
</a-card>
</div>
</template>
<script setup>
import { ref, watch, nextTick, onBeforeUnmount } from 'vue'
import { createVMConsoleSession } from '@/api/pve'
import RFB from '@novnc/novnc/core/rfb'
const props = defineProps({
vm: {
type: Object,
required: true
},
vmId: {
type: [Number, String],
required: true
},
active: {
type: Boolean,
default: false
}
})
const novncContainer = ref(null)
const consoleLoading = ref(false)
const consoleError = ref('')
const rfb = ref(null)
const API_BASE = (import.meta.env.VITE_HOST || '').replace(/\/$/, '')
const buildBackendUrl = (path) => {
if (!path) return ''
if (path.startsWith('http://') || path.startsWith('https://')) {
return path
}
const base = API_BASE || window.location.origin
if (path.startsWith('/')) {
return `${base}${path}`
}
return `${base}/${path}`
}
const initConsole = async () => {
if (!props.vm) return
// 清理之前的连接
if (rfb.value) {
try {
rfb.value.disconnect()
rfb.value = null
} catch (e) {
console.warn('清理旧连接时出错:', e)
}
}
consoleLoading.value = true
consoleError.value = ''
try {
// 创建控制台会话
const session = await createVMConsoleSession(props.vmId, { type: 'novnc' })
if (!session?.session_token) {
throw new Error('未获取到控制台会话信息')
}
// 优先使用 proxy_url完整的 WebSocket URL如果没有则使用 proxy_path 构建
let wsUrl = ''
if (session.proxy_url) {
wsUrl = session.proxy_url
} else if (session.proxy_path) {
const baseUrl = buildBackendUrl('')
const wsProtocol = baseUrl.startsWith('https') ? 'wss' : 'ws'
const wsHost = baseUrl.replace(/^https?:\/\//, '').replace(/\/$/, '')
wsUrl = `${wsProtocol}://${wsHost}${session.proxy_path.startsWith('/') ? session.proxy_path : '/' + session.proxy_path}`
} else {
throw new Error('未获取到 WebSocket 代理路径')
}
const password = session.password || ''
// 等待 DOM 更新
await nextTick()
const container = novncContainer.value || document.getElementById(`noVNC_container_${props.vmId}`)
if (!container) {
throw new Error('找不到 noVNC 容器元素,请刷新页面重试')
}
// 创建 noVNC 连接
rfb.value = new RFB(container, wsUrl, {
credentials: {
password: password
},
shared: true,
repeaterID: ''
})
// 配置 RFB
rfb.value.scaleViewport = true
rfb.value.resizeSession = false
rfb.value.background = '#000000'
rfb.value.qualityLevel = 6
rfb.value.compressionLevel = 2
// 事件监听
rfb.value.addEventListener('connect', () => {
consoleLoading.value = false
consoleError.value = ''
console.log('noVNC 连接成功')
setTimeout(() => {
if (rfb.value && container) {
const resizeEvent = new Event('resize', { bubbles: true })
container.dispatchEvent(resizeEvent)
}
}, 200)
})
rfb.value.addEventListener('disconnect', (e) => {
consoleLoading.value = false
const reason = e?.detail?.clean === false && e?.detail?.reason
? e.detail.reason
: '连接已断开'
consoleError.value = reason
console.log('noVNC 断开连接:', reason, e?.detail)
})
rfb.value.addEventListener('credentialsrequired', () => {
consoleError.value = '需要密码验证,但密码可能不正确'
consoleLoading.value = false
console.warn('noVNC 需要密码验证')
})
rfb.value.addEventListener('securityfailure', (e) => {
const reason = e.detail?.reason || '未知错误'
consoleError.value = '安全验证失败: ' + reason
consoleLoading.value = false
console.error('noVNC 安全验证失败:', e.detail)
})
rfb.value.addEventListener('serverinit', () => {
console.log('noVNC 服务器初始化完成')
})
rfb.value.addEventListener('capabilities', (e) => {
console.log('noVNC 服务器能力:', e.detail)
})
} catch (error) {
consoleError.value = error.message || '初始化控制台失败'
consoleLoading.value = false
console.error('初始化控制台失败:', error)
}
}
const cleanupConsole = () => {
if (rfb.value) {
try {
rfb.value.disconnect()
rfb.value = null
} catch (e) {
console.warn('清理控制台连接时出错:', e)
}
}
}
// 监听 active 变化,当切换到控制台 tab 时初始化
watch(() => props.active, (active) => {
if (active) {
nextTick(() => {
setTimeout(() => {
initConsole()
}, 50)
})
} else {
cleanupConsole()
}
}, { immediate: true })
onBeforeUnmount(() => {
cleanupConsole()
})
</script>
<style scoped>
.vm-console-tab {
width: 100%;
}
.pve-console-wrapper {
width: 100%;
min-height: 480px;
border: 1px solid var(--color-border-2);
border-radius: 8px;
background: #000;
overflow: hidden;
position: relative;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
}
.novnc-container {
width: 80%;
max-width: 1200px;
height: 600px;
background: #000;
position: relative;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
padding: 0;
text-align: center;
}
/* noVNC 创建的 _screen div - 确保居中显示 */
.novnc-container > div {
display: flex !important;
align-items: center !important;
justify-content: center !important;
width: 80% !important;
height: 100% !important;
margin: 0 auto !important;
padding: 0 !important;
position: relative !important;
overflow: auto !important;
}
/* noVNC 创建的 canvas - 当 scaleViewport=true 时canvas 会按比例缩放,需要居中显示 */
.novnc-container canvas {
display: block !important;
margin: 0 auto !important;
/* 不强制宽高,让 noVNC 的 scaleViewport 自动处理缩放,保持宽高比 */
max-width: 80% !important;
max-height: 100% !important;
/* 确保 canvas 在容器中水平和垂直居中 */
position: relative !important;
}
.novnc-placeholder {
width: 80%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-text-3);
text-align: center;
flex-direction: column;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,73 @@
<template>
<div class="vm-overview-tab">
<a-row :gutter="16">
<a-col :span="12">
<a-card title="基本信息" :bordered="false">
<a-descriptions :column="1" bordered :data="overviewData.basic" />
</a-card>
</a-col>
<a-col :span="12">
<a-card title="资源" :bordered="false">
<a-descriptions :column="1" bordered :data="overviewData.resource" />
</a-card>
</a-col>
</a-row>
<a-card title="时间与状态" :bordered="false" style="margin-top: 16px;">
<a-descriptions :column="2" bordered :data="overviewData.meta" />
</a-card>
</div>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
vm: {
type: Object,
required: true
}
})
const getStatusText = (status) => {
const textMap = {
running: '运行中',
stopped: '已停止',
paused: '已暂停',
unknown: '未知'
}
return textMap[status] || '未知'
}
const overviewData = computed(() => {
const vm = props.vm
if (!vm) {
return { basic: [], resource: [], meta: [] }
}
return {
basic: [
{ label: '虚拟机ID', value: vm.vmid || '-' },
{ label: '节点', value: vm.node || '-' },
{ label: '所属服务器', value: vm.server_name || '-' },
{ label: '描述', value: vm.description || '无' }
],
resource: [
{ label: 'CPU核心数', value: vm.cpu_cores ? `${vm.cpu_cores} vCPU` : '-' },
{ label: '内存', value: vm.memory_mb ? `${vm.memory_mb} MB` : '-' },
{ label: '磁盘', value: vm.disk_gb ? `${vm.disk_gb} GB` : '-' },
{ label: 'IP地址', value: vm.ip_address || '未分配' }
],
meta: [
{ label: '创建时间', value: vm.created_at || '-' },
{ label: '更新时间', value: vm.updated_at || '-' },
{ label: '状态', value: getStatusText(vm.status) }
]
}
})
</script>
<style scoped>
.vm-overview-tab {
width: 100%;
}
</style>