8ccccf8f52
主要功能: - 多主机管理: 支持TCP/SSH方式纳管远程KVM主机 - 用户认证: JWT token认证, 默认admin/admin123 - noVNC控制台: 前端集成noVNC, WebSocket代理VNC连接 - 深色主题: 全局Element Plus深色主题覆盖 - 虚拟机操作: 克隆、迁移、XML编辑、快照管理 - 资源监控: CPU/内存/磁盘IO/网络流量实时监控 Bug修复: - libvirt getInfo()内存单位修正(MiB非KiB) - 远程主机VNC 0.0.0.0监听地址连接策略修复 - Dashboard定时器内存泄漏修复 - bcrypt版本兼容性修复
270 lines
9.5 KiB
Vue
270 lines
9.5 KiB
Vue
<template>
|
|
<div class="vm-list">
|
|
<!-- 操作栏 -->
|
|
<div class="toolbar">
|
|
<el-button type="primary" @click="showCreateDialog = true">
|
|
<el-icon><Plus /></el-icon> 创建虚拟机
|
|
</el-button>
|
|
<el-button @click="loadData">
|
|
<el-icon><Refresh /></el-icon> 刷新
|
|
</el-button>
|
|
</div>
|
|
|
|
<!-- 虚拟机列表 -->
|
|
<el-table :data="vms" stripe style="width: 100%" v-loading="loading">
|
|
<el-table-column type="expand">
|
|
<template #default="{ row }">
|
|
<div class="expand-content">
|
|
<p><strong>UUID:</strong> {{ row.uuid }}</p>
|
|
<p><strong>CPU模式:</strong> {{ row.cpu_mode }}</p>
|
|
<p><strong>OS类型:</strong> {{ row.os_type }}</p>
|
|
<p v-if="row.disks?.length">
|
|
<strong>磁盘:</strong>
|
|
<span v-for="d in row.disks" :key="d.dev" style="margin-right: 12px;">
|
|
{{ d.dev }} - {{ d.file }} ({{ d.format }})
|
|
</span>
|
|
</p>
|
|
<p v-if="row.interfaces?.length">
|
|
<strong>网络:</strong>
|
|
<span v-for="i in row.interfaces" :key="i.mac || i.dev" style="margin-right: 12px;">
|
|
{{ i.type }} {{ i.network }} {{ i.mac }}
|
|
<el-tag v-if="i.ip" type="success" size="small">{{ i.ip }}</el-tag>
|
|
</span>
|
|
</p>
|
|
</div>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="name" label="名称" min-width="150">
|
|
<template #default="{ row }">
|
|
<el-link type="primary" @click="$router.push(`/vm/${row.name}?host_id=${hostId()}`)">{{ row.name }}</el-link>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="状态" width="100" align="center">
|
|
<template #default="{ row }">
|
|
<el-tag :type="stateType(row.state)" size="small" effect="dark">
|
|
{{ stateLabel(row.state) }}
|
|
</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="vcpus" label="CPU" width="80" align="center" />
|
|
<el-table-column label="内存" width="110" align="center">
|
|
<template #default="{ row }">{{ formatMem(row.memory_mb) }}</template>
|
|
</el-table-column>
|
|
<el-table-column label="自动启动" width="90" align="center">
|
|
<template #default="{ row }">
|
|
<el-tag :type="row.autostart ? 'success' : 'info'" size="small">
|
|
{{ row.autostart ? '是' : '否' }}
|
|
</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="VNC" width="80" align="center">
|
|
<template #default="{ row }">
|
|
<span v-if="row.vnc_port > 0">{{ row.vnc_port }}</span>
|
|
<span v-else>-</span>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="操作" width="280" align="center" fixed="right">
|
|
<template #default="{ row }">
|
|
<el-button-group>
|
|
<el-button v-if="row.state === 'shutoff'" type="success" size="small"
|
|
@click="doAction(row.name, 'start')">启动</el-button>
|
|
<el-button v-if="row.state === 'running'" type="warning" size="small"
|
|
@click="doAction(row.name, 'stop')">关机</el-button>
|
|
<el-button v-if="row.state === 'running'" type="info" size="small"
|
|
@click="doAction(row.name, 'pause')">暂停</el-button>
|
|
<el-button v-if="row.state === 'paused'" type="success" size="small"
|
|
@click="doAction(row.name, 'resume')">恢复</el-button>
|
|
<el-button v-if="row.state === 'running'" type="danger" size="small"
|
|
@click="doAction(row.name, 'force_stop')">强制关</el-button>
|
|
<el-button type="primary" size="small"
|
|
@click="$router.push(`/vm/${row.name}?host_id=${hostId()}`)">详情</el-button>
|
|
<el-button type="danger" size="small"
|
|
@click="deleteVM(row)">删除</el-button>
|
|
</el-button-group>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
|
|
<!-- 创建虚拟机对话框 -->
|
|
<el-dialog v-model="showCreateDialog" title="创建虚拟机" width="600px" :close-on-click-modal="false">
|
|
<el-form :model="createForm" label-width="100px">
|
|
<el-form-item label="名称">
|
|
<el-input v-model="createForm.name" placeholder="虚拟机名称" />
|
|
</el-form-item>
|
|
<el-row :gutter="16">
|
|
<el-col :span="12">
|
|
<el-form-item label="CPU核心">
|
|
<el-input-number v-model="createForm.vcpus" :min="1" :max="64" />
|
|
</el-form-item>
|
|
</el-col>
|
|
<el-col :span="12">
|
|
<el-form-item label="内存(MB)">
|
|
<el-input-number v-model="createForm.memory_mb" :min="512" :step="512" />
|
|
</el-form-item>
|
|
</el-col>
|
|
</el-row>
|
|
<el-form-item label="磁盘大小">
|
|
<el-input-number v-model="createForm.disk_gb" :min="5" :max="2000" />
|
|
<span style="margin-left: 8px; color: #7a8fa3;">GB</span>
|
|
</el-form-item>
|
|
<el-form-item label="存储池">
|
|
<el-select v-model="createForm.pool_name">
|
|
<el-option v-for="p in poolOptions" :key="p" :label="p" :value="p" />
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item label="网络">
|
|
<el-select v-model="createForm.network">
|
|
<el-option v-for="n in networkOptions" :key="n" :label="n" :value="n" />
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item label="ISO镜像">
|
|
<el-select v-model="createForm.iso_path" clearable placeholder="可选,用于安装系统">
|
|
<el-option v-for="iso in isoOptions" :key="iso.path" :label="`${iso.name} (${iso.size_gb}GB)`" :value="iso.path" />
|
|
</el-select>
|
|
</el-form-item>
|
|
</el-form>
|
|
<template #footer>
|
|
<el-button @click="showCreateDialog = false">取消</el-button>
|
|
<el-button type="primary" @click="createVM" :loading="creating">创建</el-button>
|
|
</template>
|
|
</el-dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted } from 'vue'
|
|
import { useRoute } from 'vue-router'
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
import { Plus, Refresh } from '@element-plus/icons-vue'
|
|
import api from '../api'
|
|
|
|
const route = useRoute()
|
|
const hostId = () => route.query.host_id || 'local'
|
|
|
|
const loading = ref(false)
|
|
const creating = ref(false)
|
|
const vms = ref([])
|
|
const showCreateDialog = ref(false)
|
|
const poolOptions = ref([])
|
|
const networkOptions = ref([])
|
|
const isoOptions = ref([])
|
|
|
|
const createForm = ref({
|
|
name: '',
|
|
vcpus: 2,
|
|
memory_mb: 2048,
|
|
disk_gb: 20,
|
|
pool_name: 'default',
|
|
network: 'default',
|
|
iso_path: null,
|
|
})
|
|
|
|
function stateType(s) {
|
|
const map = { running: 'success', shutoff: 'info', paused: 'warning', crashed: 'danger' }
|
|
return map[s] || 'info'
|
|
}
|
|
|
|
function stateLabel(s) {
|
|
const map = { running: '运行中', shutoff: '已关闭', paused: '已暂停', blocked: '阻塞', crashed: '崩溃' }
|
|
return map[s] || s
|
|
}
|
|
|
|
function formatMem(mb) {
|
|
if (mb >= 1024) return (mb / 1024).toFixed(1) + ' GB'
|
|
return mb + ' MB'
|
|
}
|
|
|
|
async function loadData() {
|
|
loading.value = true
|
|
try {
|
|
const data = await api.get('/vm/list', { params: { host_id: hostId() } })
|
|
vms.value = data.vms || []
|
|
} catch (e) {}
|
|
loading.value = false
|
|
}
|
|
|
|
async function loadOptions() {
|
|
try {
|
|
const [pools, nets, isos] = await Promise.all([
|
|
api.get('/storage/pools', { params: { host_id: hostId() } }),
|
|
api.get('/network/list', { params: { host_id: hostId() } }),
|
|
api.get('/storage/isos', { params: { host_id: hostId() } }),
|
|
])
|
|
poolOptions.value = (pools.pools || []).map(p => p.name)
|
|
networkOptions.value = (nets.networks || []).map(n => n.name)
|
|
isoOptions.value = isos.isos || []
|
|
} catch (e) {}
|
|
}
|
|
|
|
async function doAction(name, action) {
|
|
const labels = { start: '启动', stop: '关机', force_stop: '强制关机', pause: '暂停', resume: '恢复' }
|
|
try {
|
|
await ElMessageBox.confirm(`确定要${labels[action]}虚拟机 ${name} 吗?`, '确认', { type: 'info' })
|
|
await api.post(`/vm/action/${name}`, { action }, { params: { host_id: hostId() } })
|
|
ElMessage.success(`${labels[action]}操作已发送`)
|
|
setTimeout(loadData, 2000)
|
|
} catch (e) {
|
|
if (e !== 'cancel') ElMessage.error(e.response?.data?.detail || '操作失败')
|
|
}
|
|
}
|
|
|
|
async function createVM() {
|
|
if (!createForm.value.name) {
|
|
ElMessage.warning('请输入虚拟机名称')
|
|
return
|
|
}
|
|
creating.value = true
|
|
try {
|
|
await api.post('/vm/create', createForm.value, { params: { host_id: hostId() } })
|
|
ElMessage.success('虚拟机创建成功')
|
|
showCreateDialog.value = false
|
|
loadData()
|
|
} catch (e) {
|
|
ElMessage.error(e.response?.data?.detail || '创建失败')
|
|
}
|
|
creating.value = false
|
|
}
|
|
|
|
async function deleteVM(row) {
|
|
try {
|
|
await ElMessageBox.confirm(
|
|
`确定要删除虚拟机 ${row.name} 吗?此操作不可恢复!`,
|
|
'危险操作',
|
|
{ type: 'error', confirmButtonText: '确定删除', confirmButtonClass: 'el-button--danger' }
|
|
)
|
|
await api.delete(`/vm/delete/${row.name}`, { params: { force: row.state === 'running', host_id: hostId() } })
|
|
ElMessage.success('虚拟机已删除')
|
|
loadData()
|
|
} catch (e) {
|
|
if (e !== 'cancel') ElMessage.error(e.response?.data?.detail || '删除失败')
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
loadData()
|
|
loadOptions()
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.toolbar {
|
|
margin-bottom: 16px;
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
|
|
.expand-content {
|
|
padding: 12px 20px;
|
|
color: #7a8fa3;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.expand-content p {
|
|
margin: 4px 0;
|
|
}
|
|
|
|
.expand-content strong {
|
|
color: #c0ccda;
|
|
}
|
|
</style>
|