feat: 添加用户虚拟机商品管理
This commit is contained in:
@@ -0,0 +1,517 @@
|
||||
<template>
|
||||
<div class="user-vm-list">
|
||||
<div class="toolbar">
|
||||
<div class="toolbar-left">
|
||||
<el-button type="primary" :icon="Plus" @click="handleCreate">新建虚拟机</el-button>
|
||||
<el-button :icon="Refresh" @click="loadList">刷新</el-button>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<el-input v-model="query.key" placeholder="搜索商品名称" clearable style="width:200px" @keyup.enter="handleSearch" @clear="handleSearch">
|
||||
<template #prefix><el-icon><Search /></el-icon></template>
|
||||
</el-input>
|
||||
<el-select v-model="query.bound" placeholder="绑定状态" clearable style="width:120px" @change="handleSearch">
|
||||
<el-option label="已绑定" :value="true" />
|
||||
<el-option label="未绑定" :value="false" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table :data="list" v-loading="loading" stripe style="width:100%">
|
||||
<el-table-column label="用户商品ID" width="100">
|
||||
<template #default="{ row }">{{ row.id }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="虚拟机ID" width="90">
|
||||
<template #default="{ row }">{{ row.itemId || row.item_id || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="用户" min-width="130">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.user?.UserName || '-' }}</span>
|
||||
<span style="color:#909399;font-size:12px"> ({{ row.userId || row.user_id }})</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="商品" min-width="140" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.good?.name || '-' }}</span>
|
||||
<el-tag v-if="row.tag" size="small" type="info" style="margin-left:4px">{{ row.tag }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="绑定状态" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.itemId || row.item_id ? 'success' : 'info'" size="small">
|
||||
{{ row.itemId || row.item_id ? '已绑定' : '未绑定' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="到期时间" width="160">
|
||||
<template #default="{ row }">{{ formatExpireTime(row.expireTime || row.expire_time) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="续费价" width="90">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.renewPrice">¥{{ (row.renewPrice).toFixed(2) }}</span>
|
||||
<span v-else style="color:#c0c4cc">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="基础价" width="90">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.basePrice">¥{{ (row.basePrice ).toFixed(2) }}</span>
|
||||
<span v-else style="color:#c0c4cc">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="note" label="备注" min-width="100" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.note || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="goDetail(row)">详情</el-button>
|
||||
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination-wrapper">
|
||||
<el-pagination v-model:current-page="query.page" v-model:page-size="query.count"
|
||||
:page-sizes="[10,20,50]" :total="total" layout="total,sizes,prev,pager,next"
|
||||
@size-change="s => { query.count = s; query.page = 1; loadList() }"
|
||||
@current-change="p => { query.page = p; loadList() }" />
|
||||
</div>
|
||||
|
||||
<!-- 新建虚拟机弹窗 -->
|
||||
<el-dialog v-model="createVisible" title="新建用户虚拟机" width="940px" destroy-on-close class="scrollable-dialog">
|
||||
<el-form ref="createFormRef" :model="createForm" :rules="createRules" label-width="110px" v-loading="createLoading">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="商品" prop="good_id">
|
||||
<div class="selector-row">
|
||||
<el-input :model-value="createForm._goodName || (createForm.good_id ? `商品 #${createForm.good_id}` : '')" readonly placeholder="请选择商品" style="flex:1" />
|
||||
<el-button type="primary" @click="showProductSelector = true" style="margin-left:8px">选择</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="用户" prop="user_id">
|
||||
<div class="selector-row">
|
||||
<el-input :model-value="createForm._userName || (createForm.user_id ? `用户 #${createForm.user_id}` : '')" readonly placeholder="请选择用户" style="flex:1" />
|
||||
<el-button type="primary" @click="showUserSelector = true" style="margin-left:8px">选择</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="虚拟机名称" prop="name">
|
||||
<el-input v-model="createForm.name" placeholder="虚拟机名称" />
|
||||
</el-form-item>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="内存(MB)" prop="memory">
|
||||
<el-input-number v-model="createForm._memoryMB" :min="0" controls-position="right" style="width:100%" placeholder="MB" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="vCPU" prop="vcpu">
|
||||
<el-input-number v-model="createForm.vcpu" :min="0" 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="系统盘(GB)" prop="system_size">
|
||||
<el-input-number v-model="createForm.system_size" :min="0" controls-position="right" style="width:100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="镜像ID" prop="image_id">
|
||||
<div style="display:flex;flex-direction:column;gap:6px;width:100%">
|
||||
<div class="selector-row">
|
||||
<el-input :model-value="createForm._serviceName || (createForm._serviceId ? `主控 #${createForm._serviceId}` : '')"
|
||||
readonly placeholder="1. 选择主控服务" style="flex:1" />
|
||||
<el-button type="primary" @click="showServiceSelector = true" style="margin-left:8px">选择</el-button>
|
||||
<el-button v-if="createForm._serviceId" @click="createForm._serviceId = 0; createForm._serviceName = ''; createForm.image_id = 0; createForm._imageName = ''" style="margin-left:4px">清除</el-button>
|
||||
</div>
|
||||
<div class="selector-row">
|
||||
<el-input :model-value="createForm._imageName || (createForm.image_id ? `镜像 #${createForm.image_id}` : '')"
|
||||
readonly placeholder="2. 选择镜像" style="flex:1" />
|
||||
<el-button type="primary" @click="showImageSelector = true" :disabled="!createForm._serviceId" style="margin-left:8px">选择</el-button>
|
||||
<el-button v-if="createForm.image_id" @click="createForm.image_id = 0; createForm._imageName = ''" style="margin-left:4px">清除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="下行带宽(Mbps)" width="220px">
|
||||
<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)" width="220px">
|
||||
<el-input-number v-model="createForm.tx_bandwidth" :min="0" 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="IPv4数量" label-width="90px">
|
||||
<el-input-number v-model="createForm.ipv4_num" :min="0" controls-position="right" style="width:100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="IPv6数量" label-width="90px">
|
||||
<el-input-number v-model="createForm.ipv6_num" :min="0" 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="快照上限">
|
||||
<el-input-number v-model="createForm.snapshot_num" :min="0" controls-position="right" style="width:100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="备份上限">
|
||||
<el-input-number v-model="createForm.backup_num" :min="0" 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="续费价格(元)">
|
||||
<el-input-number v-model="createForm._renewPriceYuan" :min="0" :precision="2" controls-position="right" style="width:100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="基础价格(元)">
|
||||
<el-input-number v-model="createForm._basePriceYuan" :min="0" :precision="2" 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="订单">
|
||||
<div class="selector-row">
|
||||
<el-input :model-value="createForm._orderName || (createForm.order_id ? `订单 #${createForm.order_id}` : '')" readonly placeholder="可选" style="flex:1" />
|
||||
<el-button type="primary" @click="showOrderSelector = true" style="margin-left:8px">选择</el-button>
|
||||
<el-button v-if="createForm.order_id" @click="createForm.order_id = 0; createForm._orderName = ''" style="margin-left:4px">清除</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="到期时间">
|
||||
<el-date-picker v-model="createForm.expire_time" type="datetime" format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss" style="width:100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="createForm.note" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="createVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="createLoading" @click="submitCreate">确定创建</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 编辑虚拟机弹窗(对接 /user_vm/update) -->
|
||||
<el-dialog v-model="editVisible" title="编辑虚拟机配置" width="560px" destroy-on-close class="scrollable-dialog">
|
||||
<el-form :model="editForm" label-width="130px" v-loading="editLoading">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="下行带宽(Mbps)">
|
||||
<el-input-number v-model="editForm.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="editForm.tx_bandwidth" :min="0" controls-position="right" style="width:100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="Root密码">
|
||||
<el-input v-model="editForm.root_password" placeholder="留空则不修改" show-password />
|
||||
</el-form-item>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="SSH端口">
|
||||
<el-input-number v-model="editForm.ssh_port" :min="1" :max="65535" controls-position="right" style="width:100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="快照上限">
|
||||
<el-input-number v-model="editForm.snapshot_num" :min="0" 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="备份上限">
|
||||
<el-input-number v-model="editForm.backup_num" :min="0" controls-position="right" style="width:100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="安全组">
|
||||
<div class="selector-row">
|
||||
<el-input :model-value="editForm._sgName || (editForm.port_group_id ? `安全组 #${editForm.port_group_id}` : '')"
|
||||
readonly placeholder="可选" style="flex:1" />
|
||||
<el-button type="primary" @click="showSgSelector = true" :disabled="!editForm.id" style="margin-left:8px">选择</el-button>
|
||||
<el-button v-if="editForm.port_group_id" @click="editForm.port_group_id = 0; editForm._sgName = ''" style="margin-left:4px">清除</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="公网网络">
|
||||
<div class="selector-row">
|
||||
<el-input :model-value="editForm._networkName || (editForm.internet_network_id ? `网络 #${editForm.internet_network_id}` : '')"
|
||||
readonly placeholder="可选,仅网桥类型" style="flex:1" />
|
||||
<el-button type="primary" @click="showNetworkSelector = true" :disabled="!editForm.id" style="margin-left:8px">选择</el-button>
|
||||
<el-button v-if="editForm.internet_network_id" @click="editForm.internet_network_id = 0; editForm._networkName = ''" style="margin-left:4px">清除</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="editVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="editLoading" @click="submitEdit">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 商品选择器 -->
|
||||
<ProductSelector v-model="showProductSelector" @confirm="p => { createForm.good_id = p.id; createForm._goodName = p.name }" />
|
||||
<!-- 用户选择器 -->
|
||||
<UserSelector v-model:visible="showUserSelector" @select="u => { createForm.user_id = u.user_id; createForm._userName = u.user_name }" />
|
||||
<!-- 订单选择器 -->
|
||||
<OrderSelector v-model="showOrderSelector" @confirm="o => { createForm.order_id = o.id; createForm._orderName = o.name }" />
|
||||
<!-- 主控服务选择器(镜像用) -->
|
||||
<KvmServiceSelector v-model="showServiceSelector" @confirm="s => { createForm._serviceId = s.id; createForm._serviceName = s.name; createForm.image_id = 0; createForm._imageName = '' }" />
|
||||
<!-- 镜像选择器 -->
|
||||
<ImageSelectorPopup v-model="showImageSelector" :service-id="createForm._serviceId || 0" @confirm="img => { createForm.image_id = img.id; createForm._imageName = img.name }" />
|
||||
<!-- 编辑用安全组选择器 -->
|
||||
<UserVmSecurityGroupSelector v-model="showSgSelector" :user-goods-id="editForm.id"
|
||||
@confirm="sg => { editForm.port_group_id = sg.id; editForm._sgName = sg.name }" />
|
||||
<!-- 编辑用公网网络选择器 -->
|
||||
<UserVmNetworkSelector v-model="showNetworkSelector" :user-goods-id="editForm.id"
|
||||
@confirm="net => { editForm.internet_network_id = net.id; editForm._networkName = net.name }" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Refresh, Search } from '@element-plus/icons-vue'
|
||||
import { getUserVmList, getUserVmDetail, createUserVm, updateUserVm, deleteUserVm } from '@/api/admin/userVm'
|
||||
import { extractApiError } from '@/utils/kvmErrorUtil'
|
||||
import { formatToApiTime } from '@/utils/tool'
|
||||
import ProductSelector from '@/components/admin/ProductSelector.vue'
|
||||
import UserSelector from '@/components/UserSelector/index.vue'
|
||||
import OrderSelector from '@/components/admin/OrderSelector.vue'
|
||||
import KvmServiceSelector from '@/components/admin/KvmServiceSelector.vue'
|
||||
import ImageSelectorPopup from '@/components/admin/ImageSelectorPopup.vue'
|
||||
import UserVmSecurityGroupSelector from '@/components/admin/UserVmSecurityGroupSelector.vue'
|
||||
import UserVmNetworkSelector from '@/components/admin/UserVmNetworkSelector.vue'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
const query = reactive({ page: 1, count: 10, key: '', bound: null })
|
||||
|
||||
const formatTime = (t) => t ? dayjs(t).format('YYYY-MM-DD HH:mm:ss') : '-'
|
||||
const formatExpireTime = (t) => {
|
||||
if (!t) return '-'
|
||||
const d = dayjs(t)
|
||||
if (d.year() < 2000) return '永久'
|
||||
return d.format('YYYY-MM-DD HH:mm')
|
||||
}
|
||||
|
||||
const loadList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = { page: query.page, count: query.count }
|
||||
if (query.key) params.key = query.key
|
||||
if (query.bound !== null && query.bound !== undefined && query.bound !== '') params.bound = query.bound
|
||||
const res = await getUserVmList(params)
|
||||
if (res?.data?.code === 200 && res?.data?.data) {
|
||||
const d = res.data.data
|
||||
list.value = d.data || (Array.isArray(d) ? d : [])
|
||||
total.value = d.all_count ?? d.total ?? list.value.length
|
||||
} else { list.value = []; total.value = 0 }
|
||||
} catch { list.value = []; total.value = 0 } finally { loading.value = false }
|
||||
}
|
||||
|
||||
const handleSearch = () => { query.page = 1; loadList() }
|
||||
|
||||
const goDetail = (row) => {
|
||||
router.push({ path: '/user-goods/vm-detail', query: { id: row.id } })
|
||||
}
|
||||
|
||||
// ---- 新建 ----
|
||||
const createVisible = ref(false)
|
||||
const createLoading = ref(false)
|
||||
const createFormRef = ref(null)
|
||||
const showProductSelector = ref(false)
|
||||
const showUserSelector = ref(false)
|
||||
const showOrderSelector = ref(false)
|
||||
const showServiceSelector = ref(false)
|
||||
const showImageSelector = ref(false)
|
||||
const createForm = reactive({
|
||||
good_id: 0, _goodName: '', user_id: 0, _userName: '',
|
||||
order_id: 0, _orderName: '',
|
||||
name: '',
|
||||
_memoryMB: 0,
|
||||
vcpu: 0, system_size: 0,
|
||||
rx_bandwidth: 0, tx_bandwidth: 0,
|
||||
_serviceId: 0, _serviceName: '', image_id: 0, _imageName: '',
|
||||
ipv4_num: 0, ipv6_num: 0, snapshot_num: 0, backup_num: 0,
|
||||
_renewPriceYuan: 0, _basePriceYuan: 0,
|
||||
note: '', expire_time: ''
|
||||
})
|
||||
const createRules = {
|
||||
good_id: [{ required: true, validator: (r, v, cb) => v > 0 ? cb() : cb(new Error('请选择商品')), trigger: 'change' }],
|
||||
user_id: [{ required: true, validator: (r, v, cb) => v > 0 ? cb() : cb(new Error('请选择用户')), trigger: 'change' }],
|
||||
vcpu: [{ required: true, message: '请填写vCPU', trigger: 'blur' }],
|
||||
system_size: [{ required: true, message: '请填写系统盘大小', trigger: 'blur' }],
|
||||
image_id: [{ required: true, validator: (r, v, cb) => v > 0 ? cb() : cb(new Error('请填写镜像ID')), trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const handleCreate = () => {
|
||||
Object.assign(createForm, { good_id: 0, _goodName: '', user_id: 0, _userName: '', order_id: 0, _orderName: '', name: '', _memoryMB: 0, vcpu: 0, system_size: 0, rx_bandwidth: 0, tx_bandwidth: 0, _serviceId: 0, _serviceName: '', image_id: 0, _imageName: '', ipv4_num: 0, ipv6_num: 0, snapshot_num: 0, backup_num: 0, _renewPriceYuan: 0, _basePriceYuan: 0, note: '', expire_time: '' })
|
||||
createVisible.value = true
|
||||
}
|
||||
|
||||
const submitCreate = () => {
|
||||
createFormRef.value?.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
createLoading.value = true
|
||||
try {
|
||||
const payload = {
|
||||
good_id: createForm.good_id,
|
||||
user_id: createForm.user_id,
|
||||
name: createForm.name,
|
||||
memory: Math.round((createForm._memoryMB || 0) * 1024), // MB → KB
|
||||
vcpu: createForm.vcpu,
|
||||
system_size: createForm.system_size,
|
||||
rx_bandwidth: createForm.rx_bandwidth,
|
||||
tx_bandwidth: createForm.tx_bandwidth,
|
||||
image_id: createForm.image_id,
|
||||
ipv4_num: createForm.ipv4_num,
|
||||
ipv6_num: createForm.ipv6_num,
|
||||
snapshot_num: createForm.snapshot_num,
|
||||
backup_num: createForm.backup_num,
|
||||
renew_price: Math.round(createForm._renewPriceYuan || 0 ),
|
||||
base_price: Math.round(createForm._basePriceYuan || 0 ),
|
||||
note: createForm.note
|
||||
}
|
||||
if (createForm.order_id) payload.order_id = createForm.order_id
|
||||
if (createForm.expire_time) payload.expire_time = formatToApiTime(createForm.expire_time)
|
||||
const res = await createUserVm(payload)
|
||||
if (res?.data?.code === 200) { ElMessage.success('创建成功'); createVisible.value = false; loadList() }
|
||||
else ElMessage.error(extractApiError(res?.data, '创建失败'))
|
||||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '创建失败')) } finally { createLoading.value = false }
|
||||
})
|
||||
}
|
||||
|
||||
// ---- 编辑 ----
|
||||
const editVisible = ref(false)
|
||||
const editLoading = ref(false)
|
||||
const showSgSelector = ref(false)
|
||||
const showNetworkSelector = ref(false)
|
||||
const editForm = reactive({
|
||||
id: 0,
|
||||
rx_bandwidth: 0, tx_bandwidth: 0,
|
||||
root_password: '',
|
||||
ssh_port: 22,
|
||||
port_group_id: 0, _sgName: '',
|
||||
snapshot_num: 0, backup_num: 0,
|
||||
internet_network_id: 0, _networkName: ''
|
||||
})
|
||||
|
||||
const handleEdit = async (row) => {
|
||||
// 先重置
|
||||
Object.assign(editForm, {
|
||||
id: row.id,
|
||||
rx_bandwidth: 0, tx_bandwidth: 0,
|
||||
root_password: '',
|
||||
ssh_port: 22,
|
||||
port_group_id: 0, _sgName: '',
|
||||
snapshot_num: 0, backup_num: 0,
|
||||
internet_network_id: 0, _networkName: ''
|
||||
})
|
||||
editVisible.value = true
|
||||
editLoading.value = true
|
||||
try {
|
||||
const res = await getUserVmDetail({ user_goods_id: row.id })
|
||||
if (res?.data?.code === 200 && res?.data?.data) {
|
||||
const d = res.data.data
|
||||
const vm = d.vm?.data ?? d.vm
|
||||
if (vm) {
|
||||
editForm.rx_bandwidth = vm.rx_bandwidth || 0
|
||||
editForm.tx_bandwidth = vm.tx_bandwidth || 0
|
||||
editForm.ssh_port = vm.ssh_port || 22
|
||||
editForm.snapshot_num = vm.snapshot_num || 0
|
||||
editForm.backup_num = vm.backup_num || 0
|
||||
}
|
||||
// 回填入站安全组
|
||||
const inSg = d.vm?.in_port_group
|
||||
if (inSg) {
|
||||
editForm.port_group_id = inSg.id
|
||||
editForm._sgName = inSg.name
|
||||
}
|
||||
// 回填公网网络(取第一个 bridge 类型)
|
||||
const bridgeNet = (d.vm?.networks || []).find(n => n.type === 'bridge')
|
||||
if (bridgeNet) {
|
||||
editForm.internet_network_id = bridgeNet.id
|
||||
editForm._networkName = bridgeNet.name || bridgeNet.address
|
||||
}
|
||||
}
|
||||
} catch { /* 回填失败不影响编辑 */ } finally { editLoading.value = false }
|
||||
}
|
||||
|
||||
const submitEdit = async () => {
|
||||
editLoading.value = true
|
||||
try {
|
||||
const payload = { user_goods_id: editForm.id }
|
||||
if (editForm.rx_bandwidth) payload.rx_bandwidth = editForm.rx_bandwidth
|
||||
if (editForm.tx_bandwidth) payload.tx_bandwidth = editForm.tx_bandwidth
|
||||
if (editForm.root_password) payload.root_password = editForm.root_password
|
||||
if (editForm.ssh_port && editForm.ssh_port !== 22) payload.ssh_port = editForm.ssh_port
|
||||
if (editForm.port_group_id) payload.port_group_id = editForm.port_group_id
|
||||
if (editForm.snapshot_num) payload.snapshot_num = editForm.snapshot_num
|
||||
if (editForm.backup_num) payload.backup_num = editForm.backup_num
|
||||
if (editForm.internet_network_id) payload.internet_network_id = editForm.internet_network_id
|
||||
const res = await updateUserVm(payload)
|
||||
if (res?.data?.code === 200) { ElMessage.success('保存成功'); editVisible.value = false; loadList() }
|
||||
else ElMessage.error(extractApiError(res?.data, '保存失败'))
|
||||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '保存失败')) } finally { editLoading.value = false }
|
||||
}
|
||||
|
||||
// ---- 删除 ----
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm(`确定删除该用户虚拟机吗?此操作会同时删除远程VM和用户商品记录!`, '删除确认', { type: 'error' })
|
||||
.then(async () => {
|
||||
try {
|
||||
const res = await deleteUserVm({ user_goods_id: row.id })
|
||||
if (res?.data?.code === 200) { ElMessage.success('删除成功'); loadList() }
|
||||
else ElMessage.error(extractApiError(res?.data, '删除失败'))
|
||||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '删除失败')) }
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
onMounted(loadList)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.user-vm-list { padding: 20px; }
|
||||
.toolbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; flex-wrap: wrap; gap: 8px; }
|
||||
.toolbar-left, .toolbar-right { display: flex; gap: 8px; align-items: center; }
|
||||
.pagination-wrapper { display: flex; justify-content: flex-end; margin-top: 16px; }
|
||||
.selector-row { display: flex; align-items: center; width: 100%; }
|
||||
|
||||
:global(.scrollable-dialog .el-dialog__body) {
|
||||
max-height: 65vh;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
:global(.scrollable-dialog .el-dialog__body::-webkit-scrollbar) {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user