|
|
@@ -2,12 +2,17 @@
|
|
|
<div class="vm-list">
|
|
|
<!-- 操作栏 -->
|
|
|
<div class="toolbar">
|
|
|
+ <el-select v-model="selectedHost" placeholder="选择主机" clearable @change="onHostChange" style="width: 200px; margin-right: 8px;">
|
|
|
+ <el-option v-for="h in hosts" :key="h.id" :label="h.name" :value="h.id" />
|
|
|
+ <el-option label="所有主机虚拟机" value="all" />
|
|
|
+ </el-select>
|
|
|
<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>
|
|
|
+ <span v-if="vmTotal" style="margin-left: 16px; color: #7a8fa3;">共 {{ vmTotal }} 台虚拟机</span>
|
|
|
</div>
|
|
|
|
|
|
<!-- 虚拟机列表 -->
|
|
|
@@ -36,7 +41,12 @@
|
|
|
</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>
|
|
|
+ <el-link type="primary" @click="goToDetail(row)">{{ row.name }}</el-link>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="宿主机" width="140">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag type="info" size="small">{{ row.host_name || row.host_id }}</el-tag>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column label="状态" width="100" align="center">
|
|
|
@@ -50,6 +60,16 @@
|
|
|
<el-table-column label="内存" width="110" align="center">
|
|
|
<template #default="{ row }">{{ formatMem(row.memory_mb) }}</template>
|
|
|
</el-table-column>
|
|
|
+ <el-table-column label="IP 地址" min-width="140">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <template v-if="row.interfaces?.length">
|
|
|
+ <span v-for="i in row.interfaces" :key="i.mac || i.dev">
|
|
|
+ <el-tag v-if="i.ip" type="success" size="small" style="margin: 2px;">{{ i.ip }}</el-tag>
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ <span v-else style="color: #5a6a7a;">-</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
<el-table-column label="自动启动" width="90" align="center">
|
|
|
<template #default="{ row }">
|
|
|
<el-tag :type="row.autostart ? 'success' : 'info'" size="small">
|
|
|
@@ -71,13 +91,13 @@
|
|
|
<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>
|
|
|
+ @click="doAction(row.name, 'pause', row.host_id)">暂停</el-button>
|
|
|
<el-button v-if="row.state === 'paused'" type="success" size="small"
|
|
|
- @click="doAction(row.name, 'resume')">恢复</el-button>
|
|
|
+ @click="doAction(row.name, 'resume', row.host_id)">恢复</el-button>
|
|
|
<el-button v-if="row.state === 'running'" type="danger" size="small"
|
|
|
- @click="doAction(row.name, 'force_stop')">强制关</el-button>
|
|
|
+ @click="doAction(row.name, 'force_stop', row.host_id)">强制关</el-button>
|
|
|
<el-button type="primary" size="small"
|
|
|
- @click="$router.push(`/vm/${row.name}?host_id=${hostId()}`)">详情</el-button>
|
|
|
+ @click="$router.push(`/vm/${row.name}?host_id=${row.host_id}`)">详情</el-button>
|
|
|
<el-button type="danger" size="small"
|
|
|
@click="deleteVM(row)">删除</el-button>
|
|
|
</el-button-group>
|
|
|
@@ -88,6 +108,12 @@
|
|
|
<!-- 创建虚拟机对话框 -->
|
|
|
<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-select v-model="createForm.host_id" placeholder="选择创建到哪台主机">
|
|
|
+ <el-option label="本机 (local)" value="local" />
|
|
|
+ <el-option v-for="h in hosts" :key="h.id" :label="h.name" :value="h.id" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
<el-form-item label="名称">
|
|
|
<el-input v-model="createForm.name" placeholder="虚拟机名称" />
|
|
|
</el-form-item>
|
|
|
@@ -133,23 +159,28 @@
|
|
|
|
|
|
<script setup>
|
|
|
import { ref, onMounted } from 'vue'
|
|
|
-import { useRoute } from 'vue-router'
|
|
|
+import { useRoute, useRouter } 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 router = useRouter()
|
|
|
const hostId = () => route.query.host_id || 'local'
|
|
|
|
|
|
const loading = ref(false)
|
|
|
const creating = ref(false)
|
|
|
const vms = ref([])
|
|
|
+const hosts = ref([])
|
|
|
+const selectedHost = ref('all')
|
|
|
+const vmTotal = ref(0)
|
|
|
const showCreateDialog = ref(false)
|
|
|
const poolOptions = ref([])
|
|
|
const networkOptions = ref([])
|
|
|
const isoOptions = ref([])
|
|
|
|
|
|
const createForm = ref({
|
|
|
+ host_id: 'local',
|
|
|
name: '',
|
|
|
vcpus: 2,
|
|
|
memory_mb: 2048,
|
|
|
@@ -177,18 +208,42 @@ function formatMem(mb) {
|
|
|
async function loadData() {
|
|
|
loading.value = true
|
|
|
try {
|
|
|
- const data = await api.get('/vm/list', { params: { host_id: hostId() } })
|
|
|
+ let data
|
|
|
+ if (selectedHost.value === 'all') {
|
|
|
+ // 聚合所有主机(包含 IP)
|
|
|
+ data = await api.get('/vm/list-all', { params: { include_ip: true } })
|
|
|
+ } else {
|
|
|
+ data = await api.get('/vm/list', { params: { host_id: selectedHost.value, include_ip: true } })
|
|
|
+ }
|
|
|
vms.value = data.vms || []
|
|
|
- } catch (e) {}
|
|
|
+ vmTotal.value = data.total || 0
|
|
|
+ } catch (e) {
|
|
|
+ console.error('加载失败:', e)
|
|
|
+ }
|
|
|
loading.value = false
|
|
|
}
|
|
|
|
|
|
+async function loadHosts() {
|
|
|
+ try {
|
|
|
+ const data = await api.get('/hosts/list')
|
|
|
+ hosts.value = data.hosts || []
|
|
|
+ } catch (e) {
|
|
|
+ console.error('加载主机列表失败:', e)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function onHostChange() {
|
|
|
+ await loadData()
|
|
|
+ loadOptions()
|
|
|
+}
|
|
|
+
|
|
|
async function loadOptions() {
|
|
|
+ const hid = createForm.value.host_id || 'local'
|
|
|
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() } }),
|
|
|
+ api.get('/storage/pools', { params: { host_id: hid } }),
|
|
|
+ api.get('/network/list', { params: { host_id: hid } }),
|
|
|
+ api.get('/storage/isos', { params: { host_id: hid } }),
|
|
|
])
|
|
|
poolOptions.value = (pools.pools || []).map(p => p.name)
|
|
|
networkOptions.value = (nets.networks || []).map(n => n.name)
|
|
|
@@ -196,11 +251,12 @@ async function loadOptions() {
|
|
|
} catch (e) {}
|
|
|
}
|
|
|
|
|
|
-async function doAction(name, action) {
|
|
|
+async function doAction(name, action, hid = null) {
|
|
|
+ const host = hid ? hid : (selectedHost.value === 'all' ? 'local' : selectedHost.value)
|
|
|
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() } })
|
|
|
+ await api.post(`/vm/action/${name}`, { action }, { params: { host_id: host } })
|
|
|
ElMessage.success(`${labels[action]}操作已发送`)
|
|
|
setTimeout(loadData, 2000)
|
|
|
} catch (e) {
|
|
|
@@ -208,6 +264,10 @@ async function doAction(name, action) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+function goToDetail(row) {
|
|
|
+ router.push({ path: '/vm/' + row.name, query: { host_id: row.host_id } })
|
|
|
+}
|
|
|
+
|
|
|
async function createVM() {
|
|
|
if (!createForm.value.name) {
|
|
|
ElMessage.warning('请输入虚拟机名称')
|
|
|
@@ -215,7 +275,7 @@ async function createVM() {
|
|
|
}
|
|
|
creating.value = true
|
|
|
try {
|
|
|
- await api.post('/vm/create', createForm.value, { params: { host_id: hostId() } })
|
|
|
+ await api.post('/vm/create', createForm.value, { params: { host_id: createForm.value.host_id } })
|
|
|
ElMessage.success('虚拟机创建成功')
|
|
|
showCreateDialog.value = false
|
|
|
loadData()
|
|
|
@@ -232,7 +292,7 @@ async function deleteVM(row) {
|
|
|
'危险操作',
|
|
|
{ type: 'error', confirmButtonText: '确定删除', confirmButtonClass: 'el-button--danger' }
|
|
|
)
|
|
|
- await api.delete(`/vm/delete/${row.name}`, { params: { force: row.state === 'running', host_id: hostId() } })
|
|
|
+ await api.delete(`/vm/delete/${row.name}`, { params: { force: row.state === 'running', host_id: row.host_id || 'local' } })
|
|
|
ElMessage.success('虚拟机已删除')
|
|
|
loadData()
|
|
|
} catch (e) {
|
|
|
@@ -240,7 +300,8 @@ async function deleteVM(row) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-onMounted(() => {
|
|
|
+onMounted(async () => {
|
|
|
+ await loadHosts()
|
|
|
loadData()
|
|
|
loadOptions()
|
|
|
})
|