feat: 对接虚拟化平台管理
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
<el-button @click="goBack" :icon="ArrowLeft">返回</el-button>
|
||||
<div class="header-info">
|
||||
<h3>虚拟机管理</h3>
|
||||
<span class="sub-info" v-if="serviceName">主控服务:{{ serviceName }} | 宿主机:{{ selectedHostName || '请选择' }}</span>
|
||||
<span class="sub-info" v-if="serviceName">主控服务:{{ serviceName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
@@ -23,9 +23,6 @@
|
||||
<el-input v-model="keyword" placeholder="搜索虚拟机" clearable style="width: 220px" @keyup.enter="handleSearch" @clear="handleSearch">
|
||||
<template #prefix><el-icon><Search /></el-icon></template>
|
||||
</el-input>
|
||||
<el-select v-model="hostIdInput" placeholder="选择宿主机" clearable filterable style="width: 220px" @change="handleSearch">
|
||||
<el-option v-for="h in hostOptions" :key="h.id" :label="`${h.name} (${h.ip || h.id})`" :value="h.id" />
|
||||
</el-select>
|
||||
<el-select v-model="filterStatus" placeholder="状态" clearable style="width: 130px" @change="handleSearch">
|
||||
<el-option v-for="s in vmStatuses" :key="s.value" :label="s.label" :value="s.value" />
|
||||
</el-select>
|
||||
@@ -77,14 +74,9 @@
|
||||
</div>
|
||||
|
||||
<!-- 创建弹窗 -->
|
||||
<el-dialog v-model="createDialogVisible" title="创建虚拟机" width="640px" destroy-on-close>
|
||||
<el-dialog v-model="createDialogVisible" title="创建虚拟机" width="800px" destroy-on-close>
|
||||
<el-form ref="createFormRef" :model="createForm" :rules="createRules" label-width="120px">
|
||||
<el-form-item label="名称"><el-input v-model="createForm.name" placeholder="不填随机生成" /></el-form-item>
|
||||
<el-form-item label="宿主机" prop="host_id">
|
||||
<el-select v-model="createForm.host_id" placeholder="选择宿主机" filterable style="width: 100%">
|
||||
<el-option v-for="h in hostOptions" :key="h.id" :label="`${h.name} (${h.ip || h.id})`" :value="h.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="镜像" prop="image_id">
|
||||
<div class="bind-selector-row">
|
||||
<el-input :model-value="createForm.image_id ? `镜像 #${createForm.image_id}${createForm._imageName ? ' - ' + createForm._imageName : ''}` : '未选择'" disabled style="flex: 1" />
|
||||
@@ -92,46 +84,82 @@
|
||||
<el-button v-if="createForm.image_id" @click="createForm.image_id = 0; createForm._imageName = ''" style="margin-left: 4px">清除</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-divider content-position="left">资源配置</el-divider>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="CPU(核)" prop="vcpu">
|
||||
<el-input-number v-model="createForm.vcpu" :min="1" controls-position="right" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="内存(KB)" prop="memory">
|
||||
<el-input-number v-model="createForm.memory" :min="65536" :step="65536" controls-position="right" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="系统盘(MB)" prop="system_size">
|
||||
<el-input-number v-model="createForm.system_size" :min="1024" :step="1024" controls-position="right" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="下行带宽(Mbps)">
|
||||
<el-input-number v-model="createForm.rx_bandwidth" :min="0" controls-position="right" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="上行带宽(Mbps)">
|
||||
<el-input-number v-model="createForm.tx_bandwidth" :min="0" controls-position="right" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-divider content-position="left">可选配置</el-divider>
|
||||
<el-form-item label="宿主机组">
|
||||
<el-form-item label="用户" prop="user_id">
|
||||
<div class="bind-selector-row">
|
||||
<el-input :model-value="createForm.host_group_id ? `宿主机组 #${createForm.host_group_id}${createForm._groupName ? ' - ' + createForm._groupName : ''}` : '未选择'" disabled style="flex: 1" />
|
||||
<el-input :model-value="createForm.user_id ? `${createForm._userName || ''} (ID: ${createForm.user_id})` : '未选择'" disabled style="flex: 1" />
|
||||
<el-button type="primary" @click="showUserSelector = true" style="margin-left: 8px">选择</el-button>
|
||||
<el-button v-if="createForm.user_id" @click="createForm.user_id = 0; createForm._userName = ''" style="margin-left: 4px">清除</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">宿主机配置(二选一)</el-divider>
|
||||
<el-form-item label="分配方式">
|
||||
<el-radio-group v-model="hostMode">
|
||||
<el-radio value="host">指定宿主机</el-radio>
|
||||
<el-radio value="group">指定宿主机组</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="宿主机" v-if="hostMode === 'host'">
|
||||
<el-select v-model="createForm.host_id" placeholder="选择宿主机" filterable style="width: 100%" @change="(v) => loadNetworkOptions(v)">
|
||||
<el-option v-for="h in hostOptions" :key="h.id" :label="`${h.name} (${h.ip || h.id})`" :value="h.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="宿主机组" v-if="hostMode === 'group'">
|
||||
<div class="bind-selector-row">
|
||||
<el-input :model-value="createForm.host_group_id ? `${createForm._groupName || ''} (ID: ${createForm.host_group_id})` : '未选择'" disabled style="flex: 1" />
|
||||
<el-button type="primary" @click="showHostGroupSelector = true" style="margin-left: 8px">选择</el-button>
|
||||
<el-button v-if="createForm.host_group_id" @click="createForm.host_group_id = 0; createForm._groupName = ''" style="margin-left: 4px">清除</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="用户ID"><el-input-number v-model="createForm.user_id" :min="0" controls-position="right" style="width: 100%" /></el-form-item>
|
||||
<el-form-item label="IP 数量"><el-input-number v-model="createForm.ip_num" :min="0" controls-position="right" style="width: 100%" /></el-form-item>
|
||||
|
||||
<el-divider content-position="left">资源配置</el-divider>
|
||||
<div class="resource-row">
|
||||
<div class="resource-item">
|
||||
<span class="resource-label">* 内存</span>
|
||||
<el-select v-model="memoryUnit" class="resource-unit-select">
|
||||
<el-option v-for="u in memoryUnitOptions" :key="u.label" :label="u.label" :value="u.label" />
|
||||
</el-select>
|
||||
<el-input-number v-model="memoryDisplay" :min="1" controls-position="right" class="resource-input" />
|
||||
</div>
|
||||
<div class="resource-item">
|
||||
<span class="resource-label">* 系统盘</span>
|
||||
<el-select v-model="diskUnit" class="resource-unit-select">
|
||||
<el-option v-for="u in diskUnitOptions" :key="u.label" :label="u.label" :value="u.label" />
|
||||
</el-select>
|
||||
<el-input-number v-model="diskDisplay" :min="1" controls-position="right" class="resource-input" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource-row">
|
||||
<div class="resource-item">
|
||||
<span class="resource-label">* CPU(核)</span>
|
||||
<el-input-number v-model="createForm.vcpu" :min="1" controls-position="right" class="resource-input" />
|
||||
</div>
|
||||
<div class="resource-item">
|
||||
<span class="resource-label">下行带宽(Mbps)</span>
|
||||
<el-input-number v-model="createForm.rx_bandwidth" :min="0" controls-position="right" class="resource-input" />
|
||||
</div>
|
||||
<div class="resource-item">
|
||||
<span class="resource-label">上行带宽(Mbps)</span>
|
||||
<el-input-number v-model="createForm.tx_bandwidth" :min="0" controls-position="right" class="resource-input" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider content-position="left">网络配置(二选一)</el-divider>
|
||||
<el-form-item label="IP分配方式">
|
||||
<el-radio-group v-model="ipMode">
|
||||
<el-radio value="num">按IP数量分配</el-radio>
|
||||
<el-radio value="ids">选择网络IP</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="IP数量" v-if="ipMode === 'num'">
|
||||
<el-input-number v-model="createForm.ip_num" :min="1" controls-position="right" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="网络IP列表" v-if="ipMode === 'ids'">
|
||||
<el-select v-model="createForm.network_ids" multiple filterable placeholder="选择网络IP" style="width: 100%">
|
||||
<el-option v-for="n in networkOptions" :key="n.id" :label="`${n.name || ''} - ${n.address || n.ip || ''} (ID: ${n.id})`" :value="n.id" />
|
||||
</el-select>
|
||||
<div class="form-tip" v-if="!networkOptions.length">请先选择宿主机以加载可用网络</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="createDialogVisible = false">取消</el-button>
|
||||
@@ -228,22 +256,17 @@
|
||||
<template v-if="vmMetricsData">
|
||||
<h4 style="margin: 16px 0 8px">实时指标</h4>
|
||||
<el-descriptions :column="2" border size="small">
|
||||
<template v-if="vmMetricsData.cpu">
|
||||
<el-descriptions-item label="CPU使用率">{{ (vmMetricsData.cpu.cpu_usage_percent ?? 0).toFixed(1) }}%</el-descriptions-item>
|
||||
</template>
|
||||
<template v-if="vmMetricsData.memory">
|
||||
<el-descriptions-item label="内存总量">{{ formatBytesRaw(vmMetricsData.memory.total) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="内存使用">{{ formatBytesRaw(vmMetricsData.memory.used) }} ({{ vmMetricsData.memory.percent || 0 }}%)</el-descriptions-item>
|
||||
</template>
|
||||
<template v-if="vmMetricsData.disk">
|
||||
<el-descriptions-item v-for="(info, path) in vmMetricsData.disk" :key="path" :label="'磁盘 ' + path">
|
||||
{{ formatBytesRaw(info.used) }} / {{ formatBytesRaw(info.total) }} ({{ info.percent || 0 }}%)
|
||||
<el-descriptions-item label="虚拟机">{{ vmMetricsData.vm_name || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="CPU使用率">
|
||||
<span :style="{ color: vmMetricsData.cpu_usage_percent > 90 ? '#F56C6C' : vmMetricsData.cpu_usage_percent > 60 ? '#E6A23C' : '#67C23A' }">
|
||||
{{ (vmMetricsData.cpu_usage_percent ?? 0).toFixed(1) }}%
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
<template v-if="vmMetricsData.internet_speed && Object.keys(vmMetricsData.internet_speed).length">
|
||||
<el-descriptions-item label="网络速率" :span="2">
|
||||
<div v-for="(val, key) in vmMetricsData.internet_speed" :key="key">{{ key }}: {{ val }}</div>
|
||||
</el-descriptions-item>
|
||||
</template>
|
||||
<template v-if="vmMetricsData.network">
|
||||
<el-descriptions-item label="网络接收">{{ formatBytesRaw(vmMetricsData.network.rx_bytes) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="网络发送">{{ formatBytesRaw(vmMetricsData.network.tx_bytes) }}</el-descriptions-item>
|
||||
</template>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
</div>
|
||||
@@ -256,6 +279,8 @@
|
||||
<ImageSelectorPopup v-model="showRebuildImageSelector" :service-id="serviceId" :current-id="rebuildImageId" @confirm="handleRebuildImageSelected" />
|
||||
<!-- 宿主机组选择器 -->
|
||||
<HostGroupSelectorPopup v-model="showHostGroupSelector" :service-id="serviceId" :current-id="createForm.host_group_id" @confirm="handleHostGroupSelected" />
|
||||
<!-- 用户选择器 -->
|
||||
<UserListSelector v-model="showUserSelector" :current-user-id="createForm.user_id" @confirm="handleUserSelected" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -267,10 +292,12 @@ import { Plus, Refresh, Search, ArrowLeft, ArrowDown } from '@element-plus/icons
|
||||
import {
|
||||
getRemoteHostList, getVmList, getVmDetail, getVmStatus, getVmMetrics,
|
||||
createVm, rebuildVm, startVm, stopVm, rebootVm, suspendVm,
|
||||
resumeVm, rescueVm, exitRescueVm, deleteVm
|
||||
resumeVm, rescueVm, exitRescueVm, deleteVm, getNetworkList
|
||||
} from '@/api/admin/kvmService'
|
||||
import { extractApiError } from '@/utils/kvmErrorUtil'
|
||||
import ImageSelectorPopup from '@/components/admin/ImageSelectorPopup.vue'
|
||||
import HostGroupSelectorPopup from '@/components/admin/HostGroupSelectorPopup.vue'
|
||||
import UserListSelector from '@/components/admin/UserListSelector.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -288,7 +315,6 @@ const vmList = ref([])
|
||||
const total = ref(0)
|
||||
const keyword = ref('')
|
||||
const filterStatus = ref('')
|
||||
const hostIdInput = ref(0)
|
||||
const hostOptions = ref([])
|
||||
const queryParams = reactive({ page: 1, count: 10 })
|
||||
|
||||
@@ -296,12 +322,50 @@ const queryParams = reactive({ page: 1, count: 10 })
|
||||
const showCreateImageSelector = ref(false)
|
||||
const showRebuildImageSelector = ref(false)
|
||||
const showHostGroupSelector = ref(false)
|
||||
const showUserSelector = ref(false)
|
||||
|
||||
const selectedHostName = computed(() => {
|
||||
const h = hostOptions.value.find(x => x.id === hostIdInput.value)
|
||||
return h ? `${h.name} (${h.ip || h.id})` : (hostIdInput.value || '')
|
||||
// 创建表单模式切换
|
||||
const hostMode = ref('host')
|
||||
const ipMode = ref('num')
|
||||
const networkOptions = ref([])
|
||||
|
||||
// 内存单位: API传输单位为 KB
|
||||
const memoryUnitOptions = [
|
||||
{ label: 'KB', factor: 1 },
|
||||
{ label: 'MB', factor: 1024 },
|
||||
{ label: 'GB', factor: 1048576 }
|
||||
]
|
||||
const memoryUnit = ref('KB')
|
||||
const getMemFactor = () => memoryUnitOptions.find(u => u.label === memoryUnit.value)?.factor || 1
|
||||
const memoryDisplay = computed({
|
||||
get: () => Math.round(createForm.memory / getMemFactor()),
|
||||
set: (v) => { createForm.memory = Math.round(v * getMemFactor()) }
|
||||
})
|
||||
|
||||
// 系统盘单位: API传输单位为 MB
|
||||
const diskUnitOptions = [
|
||||
{ label: 'MB', factor: 1 },
|
||||
{ label: 'GB', factor: 1024 }
|
||||
]
|
||||
const diskUnit = ref('GB')
|
||||
const getDiskFactor = () => diskUnitOptions.find(u => u.label === diskUnit.value)?.factor || 1
|
||||
const diskDisplay = computed({
|
||||
get: () => Math.round(createForm.system_size / getDiskFactor()),
|
||||
set: (v) => { createForm.system_size = Math.round(v * getDiskFactor()) }
|
||||
})
|
||||
|
||||
const loadNetworkOptions = async (hostId) => {
|
||||
if (!hostId) return
|
||||
try {
|
||||
const res = await getNetworkList({ service_id: serviceId.value, host_id: hostId, page: 1, page_size: 200 })
|
||||
const body = res?.data
|
||||
if (body?.code === 200 && body?.data) {
|
||||
const inner = body.data
|
||||
networkOptions.value = inner.networks || inner.data || (Array.isArray(inner) ? inner : [])
|
||||
}
|
||||
} catch { networkOptions.value = [] }
|
||||
}
|
||||
|
||||
const getHostLabel = (hid) => {
|
||||
const h = hostOptions.value.find(x => x.id === hid)
|
||||
return h ? `${h.name}` : (hid || '-')
|
||||
@@ -340,16 +404,16 @@ const vmMetricsData = ref(null)
|
||||
const createForm = reactive({
|
||||
name: '', host_id: 0, image_id: 0, vcpu: 1, memory: 1048576,
|
||||
system_size: 10240, rx_bandwidth: 0, tx_bandwidth: 0,
|
||||
host_group_id: 0, user_id: 0, ip_num: 0,
|
||||
_imageName: '', _groupName: ''
|
||||
host_group_id: 0, user_id: 0, ip_num: 0, network_ids: [],
|
||||
_imageName: '', _groupName: '', _userName: ''
|
||||
})
|
||||
|
||||
const createRules = {
|
||||
host_id: [{ required: true, message: '请选择宿主机', trigger: 'change' }],
|
||||
image_id: [{ required: true, message: '请选择镜像', trigger: 'blur', type: 'number', min: 1 }],
|
||||
vcpu: [{ required: true, message: '请输入CPU核数', trigger: 'blur' }],
|
||||
memory: [{ required: true, message: '请输入内存(KB)', trigger: 'blur' }],
|
||||
system_size: [{ required: true, message: '请输入系统盘(MB)', trigger: 'blur' }]
|
||||
system_size: [{ required: true, message: '请输入系统盘大小', trigger: 'blur' }],
|
||||
user_id: [{ required: true, message: '请选择用户', trigger: 'change', type: 'number', min: 1 }]
|
||||
}
|
||||
|
||||
const vmStatusType = (s) => ({
|
||||
@@ -397,14 +461,13 @@ const formatBytesRaw = (val) => {
|
||||
const handleCreateImageSelected = (img) => { createForm.image_id = img.id; createForm._imageName = img.name }
|
||||
const handleRebuildImageSelected = (img) => { rebuildImageId.value = img.id; rebuildImageName.value = img.name }
|
||||
const handleHostGroupSelected = (group) => { createForm.host_group_id = group.id; createForm._groupName = group.name || '' }
|
||||
const handleUserSelected = (user) => { createForm.user_id = user.user_id || user.id; createForm._userName = user.user_name || user.name || '' }
|
||||
|
||||
const loadList = async () => {
|
||||
if (!serviceId.value) return
|
||||
const hid = hostIdInput.value || hostId.value
|
||||
if (!hid) { ElMessage.warning('请先选择宿主机'); return }
|
||||
loading.value = true
|
||||
try {
|
||||
const params = { service_id: serviceId.value, host_id: hid, page: queryParams.page, count: queryParams.count }
|
||||
const params = { service_id: serviceId.value, page: queryParams.page, count: queryParams.count }
|
||||
if (keyword.value) params.key = keyword.value
|
||||
if (filterStatus.value) params.status = filterStatus.value
|
||||
const res = await getVmList(params)
|
||||
@@ -414,41 +477,51 @@ const loadList = async () => {
|
||||
vmList.value = inner.data || inner.vms || (Array.isArray(inner) ? inner : [])
|
||||
total.value = inner.meta?.count ?? inner.all_count ?? inner.total ?? vmList.value.length
|
||||
} else { vmList.value = []; total.value = 0 }
|
||||
} catch (e) { ElMessage.error('获取虚拟机列表失败') } finally { loading.value = false }
|
||||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '获取虚拟机列表失败')) } finally { loading.value = false }
|
||||
}
|
||||
|
||||
const handleSearch = () => { queryParams.page = 1; loadList() }
|
||||
|
||||
const handleAdd = () => {
|
||||
Object.assign(createForm, {
|
||||
name: '', host_id: hostIdInput.value || hostId.value || 0, image_id: 0,
|
||||
name: '', host_id: hostId.value || 0, image_id: 0,
|
||||
vcpu: 1, memory: 1048576, system_size: 10240,
|
||||
rx_bandwidth: 0, tx_bandwidth: 0, host_group_id: 0, user_id: 0, ip_num: 0,
|
||||
_imageName: '', _groupName: ''
|
||||
rx_bandwidth: 0, tx_bandwidth: 0, host_group_id: 0, user_id: 0, ip_num: 0, network_ids: [],
|
||||
_imageName: '', _groupName: '', _userName: ''
|
||||
})
|
||||
hostMode.value = 'host'
|
||||
ipMode.value = 'num'
|
||||
if (createForm.host_id) loadNetworkOptions(createForm.host_id)
|
||||
createDialogVisible.value = true
|
||||
}
|
||||
|
||||
const submitCreate = () => {
|
||||
if (hostMode.value === 'host' && !createForm.host_id) { ElMessage.warning('请选择宿主机'); return }
|
||||
if (hostMode.value === 'group' && !createForm.host_group_id) { ElMessage.warning('请选择宿主机组'); return }
|
||||
if (ipMode.value === 'ids' && !createForm.network_ids.length) { ElMessage.warning('请选择网络IP'); return }
|
||||
if (ipMode.value === 'num' && !createForm.ip_num) { ElMessage.warning('请输入IP数量'); return }
|
||||
|
||||
createFormRef.value?.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const payload = {
|
||||
service_id: serviceId.value,
|
||||
host_id: createForm.host_id, image_id: createForm.image_id,
|
||||
vcpu: createForm.vcpu, memory: createForm.memory, system_size: createForm.system_size
|
||||
image_id: createForm.image_id,
|
||||
vcpu: createForm.vcpu, memory: createForm.memory, system_size: createForm.system_size,
|
||||
user_id: createForm.user_id
|
||||
}
|
||||
if (createForm.name) payload.name = createForm.name
|
||||
if (createForm.rx_bandwidth) payload.rx_bandwidth = createForm.rx_bandwidth
|
||||
if (createForm.tx_bandwidth) payload.tx_bandwidth = createForm.tx_bandwidth
|
||||
if (createForm.host_group_id) payload.host_group_id = createForm.host_group_id
|
||||
if (createForm.user_id) payload.user_id = createForm.user_id
|
||||
if (createForm.ip_num) payload.ip_num = createForm.ip_num
|
||||
if (hostMode.value === 'host') payload.host_id = createForm.host_id
|
||||
else payload.host_group_id = createForm.host_group_id
|
||||
if (ipMode.value === 'num') payload.ip_num = createForm.ip_num
|
||||
else payload.network_ids = createForm.network_ids
|
||||
const res = await createVm(payload)
|
||||
if (res?.data?.code === 200) { ElMessage.success('创建成功'); createDialogVisible.value = false; loadList() }
|
||||
else ElMessage.error(res?.data?.message || '创建失败')
|
||||
} catch (e) { ElMessage.error('创建失败: ' + (e?.response?.data?.message || e.message)) }
|
||||
else ElMessage.error(extractApiError(res?.data, '创建失败'))
|
||||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '创建失败')) }
|
||||
finally { submitLoading.value = false }
|
||||
})
|
||||
}
|
||||
@@ -473,8 +546,8 @@ const handlePower = (row, action) => {
|
||||
res = await apis[action](payload)
|
||||
}
|
||||
if (res?.data?.code === 200) { ElMessage.success(`${labels[action]}成功`); loadList() }
|
||||
else ElMessage.error(res?.data?.message || `${labels[action]}失败`)
|
||||
} catch (e) { ElMessage.error(`${labels[action]}失败`) }
|
||||
else ElMessage.error(extractApiError(res?.data, `${labels[action]}失败`))
|
||||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, `${labels[action]}失败`)) }
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
@@ -497,8 +570,8 @@ const submitRebuild = async () => {
|
||||
try {
|
||||
const res = await rebuildVm({ service_id: serviceId.value, vm_id: rebuildTarget.value.id, image_id: rebuildImageId.value })
|
||||
if (res?.data?.code === 200) { ElMessage.success('重建成功'); rebuildDialogVisible.value = false; loadList() }
|
||||
else ElMessage.error(res?.data?.message || '重建失败')
|
||||
} catch (e) { ElMessage.error('重建失败') } finally { submitLoading.value = false }
|
||||
else ElMessage.error(extractApiError(res?.data, '重建失败'))
|
||||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '重建失败')) } finally { submitLoading.value = false }
|
||||
}
|
||||
|
||||
const handleRescue = (row) => {
|
||||
@@ -511,8 +584,8 @@ const handleRescue = (row) => {
|
||||
fd.append('vm_id', row.id)
|
||||
const res = await rescueVm(fd)
|
||||
if (res?.data?.code === 200) { ElMessage.success('已进入救援模式'); loadList() }
|
||||
else ElMessage.error(res?.data?.message || '操作失败')
|
||||
} catch (e) { ElMessage.error('操作失败') }
|
||||
else ElMessage.error(extractApiError(res?.data, '操作失败'))
|
||||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '操作失败')) }
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
@@ -526,8 +599,8 @@ const handleExitRescue = (row) => {
|
||||
fd.append('vm_id', row.id)
|
||||
const res = await exitRescueVm(fd)
|
||||
if (res?.data?.code === 200) { ElMessage.success('已退出救援模式'); loadList() }
|
||||
else ElMessage.error(res?.data?.message || '操作失败')
|
||||
} catch (e) { ElMessage.error('操作失败') }
|
||||
else ElMessage.error(extractApiError(res?.data, '操作失败'))
|
||||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '操作失败')) }
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
@@ -554,7 +627,7 @@ const fetchVmStatus = async (vm) => {
|
||||
currentDetail.value = { ...currentDetail.value, status: statusData.status ?? statusData }
|
||||
ElMessage.success('状态已刷新: ' + vmStatusLabel(currentDetail.value.status))
|
||||
}
|
||||
} catch { ElMessage.error('获取状态失败') }
|
||||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '获取状态失败')) }
|
||||
}
|
||||
|
||||
const fetchVmMetrics = async (vm) => {
|
||||
@@ -562,7 +635,7 @@ const fetchVmMetrics = async (vm) => {
|
||||
const res = await getVmMetrics({ service_id: serviceId.value, vm_name: vm.name })
|
||||
if (res?.data?.code === 200) vmMetricsData.value = res.data.data?.data ?? res.data.data
|
||||
else ElMessage.warning('暂无指标数据')
|
||||
} catch { ElMessage.error('获取指标失败') }
|
||||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '获取指标失败')) }
|
||||
}
|
||||
|
||||
const handleGoDetail = (row) => {
|
||||
@@ -574,13 +647,10 @@ const handleDelete = (row) => {
|
||||
confirmButtonText: '确定删除', cancelButtonText: '取消', type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const fd = new FormData()
|
||||
fd.append('service_id', serviceId.value)
|
||||
fd.append('vm_id', row.id)
|
||||
const res = await deleteVm(fd)
|
||||
const res = await deleteVm({ service_id: serviceId.value, vm_id: row.id })
|
||||
if (res?.data?.code === 200) { ElMessage.success('删除成功'); loadList() }
|
||||
else ElMessage.error(res?.data?.message || '删除失败')
|
||||
} catch (e) { ElMessage.error('删除失败') }
|
||||
else ElMessage.error(extractApiError(res?.data, '删除失败'))
|
||||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '删除失败')) }
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
@@ -589,12 +659,7 @@ const goBack = () => { router.push('/virtualization/kvm-service') }
|
||||
onMounted(async () => {
|
||||
if (serviceId.value) {
|
||||
await loadHostOptions()
|
||||
if (hostId.value) {
|
||||
hostIdInput.value = hostId.value
|
||||
} else if (hostOptions.value.length > 0) {
|
||||
hostIdInput.value = hostOptions.value[0].id
|
||||
}
|
||||
if (hostIdInput.value) loadList()
|
||||
loadList()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -615,4 +680,9 @@ onMounted(async () => {
|
||||
.bind-selector-row { display: flex; align-items: center; width: 100%; }
|
||||
:deep(.el-table) { --el-table-header-bg-color: #fafafa; }
|
||||
:deep(.el-table th) { font-weight: 600; color: #303133; font-size: 13px; }
|
||||
.resource-row { display: flex; gap: 20px; margin-bottom: 18px; }
|
||||
.resource-item { display: flex; align-items: center; gap: 6px; flex: 1; min-width: 0; }
|
||||
.resource-label { white-space: nowrap; font-size: 14px; color: #606266; flex-shrink: 0; }
|
||||
.resource-unit-select { width: 72px; flex-shrink: 0; }
|
||||
.resource-input { flex: 1; min-width: 0; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user