feat: 添加用户虚拟机商品管理
Build and Deploy Vue3 / build (push) Successful in 1m40s
Build and Deploy Vue3 / deploy (push) Successful in 1m8s

This commit is contained in:
2026-03-31 15:13:04 +08:00
parent 71d3605f4f
commit c07e09c151
28 changed files with 7143 additions and 621 deletions
+62 -29
View File
@@ -321,6 +321,26 @@
<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" />
<!-- 电源操作确认弹窗 -->
<el-dialog v-model="powerDialogVisible" :title="`${powerLabels[powerAction] || ''}虚拟机`" width="400px" destroy-on-close>
<div style="display: flex; align-items: flex-start; gap: 12px; padding: 8px 0">
<el-icon :size="22" :style="{ color: powerAction === 'stop' ? '#F56C6C' : powerAction === 'reboot' ? '#E6A23C' : '#409EFF', flexShrink: 0, marginTop: '2px' }">
<WarningFilled />
</el-icon>
<div>
<div style="font-size: 15px; font-weight: 500; color: #303133; margin-bottom: 12px">
确定要{{ powerLabels[powerAction] }}虚拟机「{{ powerRow?.name }}」吗?
</div>
<el-checkbox v-model="powerForce" style="margin-bottom: 4px">强制执行</el-checkbox>
<div style="font-size: 12px; color: #909399; padding-left: 24px">勾选后将强制{{ powerLabels[powerAction] }},可能导致数据丢失</div>
</div>
</div>
<template #footer>
<el-button @click="powerDialogVisible = false">取消</el-button>
<el-button :type="powerAction === 'stop' ? 'danger' : 'primary'" @click="submitPower">确定{{ powerLabels[powerAction] }}</el-button>
</template>
</el-dialog>
</div>
</template>
@@ -328,7 +348,7 @@
import { ref, reactive, computed, inject, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Refresh, Search, ArrowLeft, ArrowDown } from '@element-plus/icons-vue'
import { Plus, Refresh, Search, ArrowLeft, ArrowDown, WarningFilled } from '@element-plus/icons-vue'
import {
getRemoteHostList, getVmList, getVmDetail, getVmStatus, getVmMetrics,
createVm, rebuildVm, startVm, stopVm, rebootVm, suspendVm,
@@ -438,6 +458,7 @@ const vmStatuses = [
{ label: '等待中', value: 'pending' }, { label: '创建中', value: 'creating' },
{ label: '就绪', value: 'ready' }, { label: '运行中', value: 'running' },
{ label: '已停止', value: 'stopped' }, { label: '已停止', value: 'stop' },
{ label: '已关闭', value: 'shutoff' },
{ label: '错误', value: 'error' }, { label: '已暂停', value: 'paused' },
{ label: '重启中', value: 'reboot' }, { label: '已关机', value: 'poweroff' },
{ label: '未知', value: 'unknown' }
@@ -470,13 +491,13 @@ const createRules = {
const vmStatusType = (s) => ({
running: 'success', ready: 'success', creating: 'warning', pending: 'info',
stopped: 'danger', stop: 'danger', error: 'danger', paused: 'warning',
stopped: 'danger', stop: 'danger', shutoff: 'danger', error: 'danger', paused: 'warning',
reboot: 'warning', poweroff: 'info', unknown: 'info'
}[s] || 'info')
const vmStatusLabel = (s) => ({
running: '运行中', ready: '就绪', creating: '创建中', pending: '等待中',
stopped: '已停止', stop: '已停止', error: '错误', paused: '已暂停',
stopped: '已停止', stop: '已停止', shutoff: '已关闭', error: '错误', paused: '已暂停',
reboot: '重启中', poweroff: '已关机', unknown: '未知'
}[s] || s || '-')
@@ -589,29 +610,34 @@ const submitCreate = () => {
})
}
const powerDialogVisible = ref(false)
const powerAction = ref('')
const powerRow = ref(null)
const powerForce = ref(false)
const powerLabels = { start: '启动', stop: '停止', reboot: '重启', suspend: '暂停', resume: '恢复' }
const handlePower = (row, action) => {
const labels = { start: '启动', stop: '停止', reboot: '重启', suspend: '暂停', resume: '恢复' }
ElMessageBox.confirm(`确定要${labels[action]}虚拟机「${row.name}」吗?`, `${labels[action]}确认`, {
confirmButtonText: '确定', cancelButtonText: '取消',
type: action === 'stop' ? 'warning' : 'info'
}).then(async () => {
try {
const apis = { start: startVm, stop: stopVm, reboot: rebootVm, suspend: suspendVm, resume: resumeVm }
const payload = { service_id: serviceId.value, vm_id: row.id }
// resume uses FormData
let res
if (action === 'resume') {
const fd = new FormData()
fd.append('service_id', serviceId.value)
fd.append('vm_id', row.id)
res = await resumeVm(fd)
} else {
res = await apis[action](payload)
}
if (res?.data?.code === 200) { ElMessage.success(`${labels[action]}成功`); loadList() }
else ElMessage.error(extractApiError(res?.data, `${labels[action]}失败`))
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, `${labels[action]}失败`)) }
}).catch(() => {})
powerRow.value = row
powerAction.value = action
powerForce.value = false
powerDialogVisible.value = true
}
const submitPower = async () => {
const action = powerAction.value
const row = powerRow.value
const label = powerLabels[action]
powerDialogVisible.value = false
try {
const apis = { start: startVm, stop: stopVm, reboot: rebootVm, suspend: suspendVm, resume: resumeVm }
const fd = new FormData()
fd.append('service_id', serviceId.value)
fd.append('vm_id', row.id)
if (powerForce.value) fd.append('force', true)
const res = await apis[action](fd)
if (res?.data?.code === 200) { ElMessage.success(`${label}成功`); loadList() }
else ElMessage.error(extractApiError(res?.data, `${label}失败`))
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, `${label}失败`)) }
}
const handleMoreAction = (row, command) => {
@@ -635,7 +661,11 @@ const submitRebuild = async () => {
if (!rebuildImageId.value) { ElMessage.warning('请选择镜像'); return }
submitLoading.value = true
try {
const res = await rebuildVm({ service_id: serviceId.value, vm_id: rebuildTarget.value.id, image_id: rebuildImageId.value })
const fd = new FormData()
fd.append('service_id', serviceId.value)
fd.append('vm_id', rebuildTarget.value.id)
fd.append('image_id', rebuildImageId.value)
const res = await rebuildVm(fd)
if (res?.data?.code === 200) { ElMessage.success('重装成功'); rebuildDialogVisible.value = false; loadList() }
else ElMessage.error(extractApiError(res?.data, '重装失败'))
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '重装失败')) } finally { submitLoading.value = false }
@@ -695,9 +725,12 @@ const fetchVmStatus = async (vm) => {
try {
const res = await getVmStatus({ service_id: serviceId.value, vm_id: vm.id })
if (res?.data?.code === 200 && res?.data?.data) {
const statusData = res.data.data
currentDetail.value = { ...currentDetail.value, status: statusData.status ?? statusData }
ElMessage.success('状态已刷新: ' + vmStatusLabel(currentDetail.value.status))
const outer = res.data.data
const inner = outer.data ?? outer
const state = inner.state ?? inner.status ?? inner
const desc = inner.desc || ''
currentDetail.value = { ...currentDetail.value, status: state }
ElMessage.success(`状态已刷新: ${desc || vmStatusLabel(state)}`)
}
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '获取状态失败')) }
}