1814 lines
87 KiB
Vue
1814 lines
87 KiB
Vue
<template>
|
||
<div class="user-vm-list">
|
||
<el-card class="main-container" shadow="never">
|
||
<div class="filter-section">
|
||
<div class="filter-content">
|
||
<el-form :inline="true" class="search-form">
|
||
<el-form-item label="用户ID">
|
||
<el-input :model-value="filterUserName || (query.user_id ? `${query.user_id}` : '')"
|
||
readonly placeholder="筛选用户" clearable style="width:140px;cursor:pointer"
|
||
@click="showFilterUserSelector = true"
|
||
@clear="query.user_id = ''; filterUserName = ''; handleSearch()" />
|
||
</el-form-item>
|
||
<el-form-item label="商品ID">
|
||
<el-input :model-value="filterGoodName || (query.good_id ? `${query.good_id}` : '')"
|
||
readonly placeholder="筛选商品" clearable style="width:140px;cursor:pointer"
|
||
@click="showFilterProductSelector = true"
|
||
@clear="query.good_id = ''; filterGoodName = ''; handleSearch()" />
|
||
</el-form-item>
|
||
<el-form-item label="关键词">
|
||
<el-input v-model="query.key" placeholder="搜索关键词" clearable style="width:180px"
|
||
@keyup.enter="handleSearch" @clear="handleSearch">
|
||
<template #prefix><el-icon><Search /></el-icon></template>
|
||
</el-input>
|
||
</el-form-item>
|
||
<el-form-item label="绑定状态">
|
||
<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-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" @click="handleSearch">
|
||
<el-icon><Search /></el-icon>查询
|
||
</el-button>
|
||
<el-button @click="query.user_id = ''; query.good_id = ''; query.key = ''; query.bound = null; filterUserName = ''; filterGoodName = ''; handleSearch()">重置</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
<div class="action-bar">
|
||
<el-button type="primary" @click="handleCreate()">
|
||
<el-icon><Plus /></el-icon>新建用户虚拟机
|
||
</el-button>
|
||
<el-button type="success" @click="loadList">
|
||
<el-icon><Refresh /></el-icon>刷新
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="table-section">
|
||
<el-alert v-if="listError" :title="listError" type="warning" :closable="false" show-icon style="margin:12px 20px 0" />
|
||
<el-table :data="list" v-loading="loading" stripe style="width:100%"
|
||
:header-cell-style="{ background: '#f8f9fa', color: '#2c3e50', fontWeight: 600, fontSize: '13px' }">
|
||
<el-table-column label="用户商品ID" width="110">
|
||
<template #default="{ row }">{{ row.id }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="商品名称" min-width="150" show-overflow-tooltip>
|
||
<template #default="{ row }">
|
||
{{ row.good?.name ? `${row.good.name}(ID:${row.good.id})` : '-' }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="用户" min-width="140" show-overflow-tooltip>
|
||
<template #default="{ row }">{{ row.user?.UserName || '-' }} <span style="color:#909399;font-size:12px">(ID:{{ row.userId || row.user_id || '-' }})</span></template>
|
||
</el-table-column>
|
||
<el-table-column label="绑定状态" width="90">
|
||
<template #default="{ row }">
|
||
<el-tag :type="row.itemId ? 'success' : 'info'" size="small">
|
||
{{ row.itemId ? '已绑定' : '未绑定' }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="续费价格" width="100">
|
||
<template #default="{ row }">
|
||
<span v-if="row.renewPrice">¥{{ (row.renewPrice / 100).toFixed(2) }}</span>
|
||
<span v-else>-</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="基础价格" width="100">
|
||
<template #default="{ row }">
|
||
<span v-if="row.basePrice">¥{{ (row.basePrice / 100).toFixed(2) }}</span>
|
||
<span v-else>-</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="到期时间" width="150">
|
||
<template #default="{ row }">{{ formatExpireTime(row.expireTime) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="200" fixed="right">
|
||
<template #default="{ row }">
|
||
<el-button v-if="row.itemId && row.itemId !== 0" link type="primary" size="small" @click="goDetail(row)">详情</el-button>
|
||
<el-button v-if="row.itemId && row.itemId !== 0" link type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
|
||
<el-button v-if="!row.itemId || row.itemId === 0" link type="success" size="small" @click="handleBindVm(row)">绑定虚拟机</el-button>
|
||
<el-dropdown trigger="click" @command="cmd => handleMoreCmd(cmd, row)" style="margin-left:8px">
|
||
<el-button link type="primary" size="small" style="transform: translateY(4px);">更多<el-icon style="margin-left:2px;"><ArrowDown /></el-icon></el-button>
|
||
<template #dropdown>
|
||
<el-dropdown-menu>
|
||
<el-dropdown-item command="remind">提醒记录</el-dropdown-item>
|
||
<el-dropdown-item command="send">发送提醒</el-dropdown-item>
|
||
<el-dropdown-item command="delete" divided style="color:#F56C6C">删除商品</el-dropdown-item>
|
||
</el-dropdown-menu>
|
||
</template>
|
||
</el-dropdown>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<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, jumper"
|
||
@size-change="s => { query.count = s; query.page = 1; loadList() }"
|
||
@current-change="p => { query.page = p; loadList() }"
|
||
background
|
||
class="pagination"
|
||
/>
|
||
</div>
|
||
</el-card>
|
||
|
||
<!-- 新建虚拟机 / 绑定虚拟机弹窗 -->
|
||
<el-dialog v-model="createVisible" title="创建虚拟机/绑定已有虚拟机" width="900px" destroy-on-close class="scrollable-dialog" @opened="onCreateDialogOpened">
|
||
<el-tabs v-model="createDialogTab" style="margin-top:-10px">
|
||
<el-tab-pane label="创建用户虚拟机" name="create">
|
||
<el-form ref="createFormRef" :model="createForm" :rules="createRules" label-width="120px" v-loading="createLoading">
|
||
<!-- 行1: 商品 + 用户 -->
|
||
<el-row :gutter="20">
|
||
<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="请选择商品" />
|
||
<el-button type="primary" @click="showProductSelector = true">选择</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="请选择用户" />
|
||
<el-button type="primary" @click="showUserSelector = true">选择</el-button>
|
||
</div>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<!-- 行2: 虚拟机名称 -->
|
||
<el-form-item label="虚拟机名称" prop="name">
|
||
<el-input v-model="createForm.name" placeholder="虚拟机名称" />
|
||
</el-form-item>
|
||
<!-- 行3: 内存 + vCPU -->
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="内存" prop="memory">
|
||
<div class="unit-input-row">
|
||
<el-input-number v-model="createForm._memoryValue" :min="0" controls-position="right" style="flex:1" />
|
||
<el-select v-model="createForm._memoryUnit" class="unit-select">
|
||
<el-option label="MB" value="MB" /><el-option label="GB" value="GB" />
|
||
</el-select>
|
||
</div>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="vCPU" prop="vcpu">
|
||
<div class="unit-input-row">
|
||
<el-input-number v-model="createForm.vcpu" :min="0" controls-position="right" style="flex:1" />
|
||
<span class="unit-text">核</span>
|
||
</div>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<!-- 行4: 系统盘 + 主控服务 -->
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="系统盘" prop="system_size">
|
||
<div class="unit-input-row">
|
||
<el-input-number v-model="createForm.system_size" :min="0" controls-position="right" style="flex:1" />
|
||
<el-select v-model="createForm._systemSizeUnit" class="unit-select">
|
||
<el-option label="GB" value="GB" /><el-option label="TB" value="TB" />
|
||
</el-select>
|
||
</div>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="主控服务">
|
||
<div class="selector-row">
|
||
<el-input :model-value="createForm._serviceName || (createForm._serviceId ? `主控 #${createForm._serviceId}` : '')" readonly placeholder="选择主控服务" />
|
||
<el-button type="primary" @click="showServiceSelector = true">选择</el-button>
|
||
<el-button v-if="createForm._serviceId" @click="createForm._serviceId = 0; createForm._serviceName = ''; createForm.image_id = 0; createForm._imageName = ''">清除</el-button>
|
||
</div>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<!-- 行5: 下行带宽 + 镜像 -->
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="下行带宽">
|
||
<div class="unit-input-row">
|
||
<el-input-number v-model="createForm.rx_bandwidth" :min="0" controls-position="right" style="flex:1" />
|
||
<el-select v-model="createForm._rxUnit" class="unit-select">
|
||
<el-option label="Mbps" value="Mbps" /><el-option label="Gbps" value="Gbps" />
|
||
</el-select>
|
||
</div>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="镜像" prop="image_id">
|
||
<div class="selector-row">
|
||
<el-input :model-value="createForm._imageName || (createForm.image_id ? `镜像 #${createForm.image_id}` : '')" readonly placeholder="请先选择商品" />
|
||
<el-button type="primary" @click="showImageSelector = true" :disabled="!createForm.good_id">选择</el-button>
|
||
<el-button v-if="createForm.image_id" @click="createForm.image_id = 0; createForm._imageName = ''">清除</el-button>
|
||
</div>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<!-- 行6: 上行带宽 + 额外数据卷 -->
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="上行带宽">
|
||
<div class="unit-input-row">
|
||
<el-input-number v-model="createForm.tx_bandwidth" :min="0" controls-position="right" style="flex:1" />
|
||
<el-select v-model="createForm._txUnit" class="unit-select">
|
||
<el-option label="Mbps" value="Mbps" /><el-option label="Gbps" value="Gbps" />
|
||
</el-select>
|
||
</div>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="数据卷">
|
||
<div class="unit-input-row">
|
||
<el-input-number v-model="createForm.data_volume_size" :min="0" controls-position="right" style="flex:1" />
|
||
<span class="unit-text">GB</span>
|
||
</div>
|
||
<div style="font-size:12px;color:#909399;margin-top:2px">额外数据卷大小,0 表示不创建</div>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<!-- 行7: IPv4 + IPv6 -->
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="IPv4数量">
|
||
<div class="unit-input-row">
|
||
<el-input-number v-model="createForm.ipv4_num" :min="0" controls-position="right" style="flex:1" />
|
||
<span class="unit-text">个</span>
|
||
</div>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="IPv6数量">
|
||
<div class="unit-input-row">
|
||
<el-input-number v-model="createForm.ipv6_num" :min="0" controls-position="right" style="flex:1" />
|
||
<span class="unit-text">个</span>
|
||
</div>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="快照上限">
|
||
<div class="unit-input-row">
|
||
<el-input-number v-model="createForm.snapshot_num" :min="0" controls-position="right" style="flex:1" />
|
||
<span class="unit-text">个</span>
|
||
</div>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="备份上限">
|
||
<div class="unit-input-row">
|
||
<el-input-number v-model="createForm.backup_num" :min="0" controls-position="right" style="flex:1" />
|
||
<span class="unit-text">个</span>
|
||
</div>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<!-- 行9: 续费价格 + 基础价格 -->
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="续费价格">
|
||
<div class="unit-input-row">
|
||
<el-input-number v-model="createForm._renewPriceYuan" :min="0" :precision="2" controls-position="right" style="flex:1" />
|
||
<span class="unit-text">元</span>
|
||
</div>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="基础价格">
|
||
<div class="unit-input-row">
|
||
<el-input-number v-model="createForm._basePriceYuan" :min="0" :precision="2" controls-position="right" style="flex:1" />
|
||
<span class="unit-text">元</span>
|
||
</div>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<!-- 行9: 基础价格 + 订单 -->
|
||
<!-- 行10: 订单 + 到期时间 -->
|
||
<el-row :gutter="20">
|
||
<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="可选" />
|
||
<el-button type="primary" @click="showOrderSelector = true">选择</el-button>
|
||
<el-button v-if="createForm.order_id" @click="createForm.order_id = 0; createForm._orderName = ''">清除</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>
|
||
<!-- 行11: 宿主机 + 宿主机组 -->
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="指定宿主机">
|
||
<div class="selector-row">
|
||
<el-input :model-value="createForm._hostName || (createForm.host_id ? `宿主机 #${createForm.host_id}` : '')" readonly placeholder="可选,不选则自动分配" />
|
||
<el-button type="primary" @click="showHostSelector = true" :disabled="!createForm._serviceId">选择</el-button>
|
||
<el-button v-if="createForm.host_id" @click="createForm.host_id = 0; createForm._hostName = ''">清除</el-button>
|
||
</div>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="指定宿主机组">
|
||
<div class="selector-row">
|
||
<el-input :model-value="createForm._hostGroupName || (createForm.host_group_id ? `主机组 #${createForm.host_group_id}` : '')" readonly placeholder="可选,不选则用商品绑定的" />
|
||
<el-button type="primary" @click="showHostGroupSelector = true" :disabled="!createForm._serviceId">选择</el-button>
|
||
<el-button v-if="createForm.host_group_id" @click="createForm.host_group_id = 0; createForm._hostGroupName = ''">清除</el-button>
|
||
</div>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="网络">
|
||
<div class="selector-row">
|
||
<el-input :model-value="createForm._networkNames || ''" readonly placeholder="可选,可多选" />
|
||
<el-button type="primary" @click="showNetworkSelector = true" :disabled="!createForm._serviceId || !createForm.host_id">选择</el-button>
|
||
<el-button v-if="createForm.network_ids.length" @click="createForm.network_ids = []; createForm._networkNames = ''">清除</el-button>
|
||
</div>
|
||
<div v-if="!createForm.host_id && createForm._serviceId" style="font-size:12px;color:#c0c4cc;margin-top:2px">请先选择宿主机</div>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<!-- 行12: 备注 -->
|
||
<el-form-item label="备注">
|
||
<el-input v-model="createForm.note" type="textarea" :rows="2" />
|
||
</el-form-item>
|
||
</el-form>
|
||
</el-tab-pane>
|
||
<el-tab-pane label="绑定虚拟机" name="bind">
|
||
<el-form ref="bindGoodsFormRef" :model="bindGoodsForm" :rules="bindGoodsFormRules" label-width="110px">
|
||
<el-form-item label="商品" prop="good_id">
|
||
<div class="selector-row">
|
||
<el-input :model-value="bindGoodsForm._goodName || (bindGoodsForm.good_id ? `商品 #${bindGoodsForm.good_id}` : '')"
|
||
readonly placeholder="请选择商品" style="flex:1" />
|
||
<el-button type="primary" @click="showBindGoodsProductSelector = true" style="margin-left:8px">选择</el-button>
|
||
<el-button v-if="bindGoodsForm.good_id" @click="bindGoodsForm.good_id = 0; bindGoodsForm._goodName = ''; bindGoodsForm._goodTag = ''; bindGoodsForm.item_id = 0; bindGoodsForm._itemName = ''" style="margin-left:4px">清除</el-button>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="用户" prop="user_id">
|
||
<div class="selector-row">
|
||
<el-input :model-value="bindGoodsForm._userName || (bindGoodsForm.user_id ? `用户 #${bindGoodsForm.user_id}` : '')"
|
||
readonly placeholder="请选择用户" style="flex:1" />
|
||
<el-button type="primary" @click="showBindGoodsUserSel = true" style="margin-left:8px">选择</el-button>
|
||
<el-button v-if="bindGoodsForm.user_id" @click="bindGoodsForm.user_id = 0; bindGoodsForm._userName = ''" style="margin-left:4px">清除</el-button>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="订单">
|
||
<div class="selector-row">
|
||
<el-input :model-value="bindGoodsForm._orderName || (bindGoodsForm.order_id ? `订单 #${bindGoodsForm.order_id}` : '')"
|
||
readonly placeholder="可选" style="flex:1" />
|
||
<el-button type="primary" @click="showBindGoodsOrderSelector = true" style="margin-left:8px">选择</el-button>
|
||
<el-button v-if="bindGoodsForm.order_id" @click="bindGoodsForm.order_id = 0; bindGoodsForm._orderName = ''" style="margin-left:4px">清除</el-button>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="套餐">
|
||
<div class="selector-row">
|
||
<el-input :model-value="bindGoodsForm._planName || (bindGoodsForm.good_plan_id ? `套餐 #${bindGoodsForm.good_plan_id}` : '')"
|
||
readonly placeholder="可选,选择后自动填入参数" style="flex:1" />
|
||
<el-button type="primary" @click="showBindGoodsPlanSelector = true" :disabled="!bindGoodsForm.good_id" style="margin-left:8px">选择</el-button>
|
||
<el-button v-if="bindGoodsForm.good_plan_id" @click="bindGoodsForm.good_plan_id = 0; bindGoodsForm._planName = ''" style="margin-left:4px">清除</el-button>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="归属项">
|
||
<div class="selector-row">
|
||
<el-input :model-value="bindGoodsForm._itemName || (bindGoodsForm.item_id ? `#${bindGoodsForm.item_id}` : '')"
|
||
readonly placeholder="可选" style="flex:1" />
|
||
<el-button type="primary" @click="handleBindGoodsItemSelect" :disabled="!bindGoodsForm.good_id" style="margin-left:8px">
|
||
{{ bindGoodsForm._goodTag === '云服务器' ? '选择虚拟机' : '使用商品ID' }}
|
||
</el-button>
|
||
<el-button v-if="bindGoodsForm.item_id" @click="bindGoodsForm.item_id = 0; bindGoodsForm._itemName = ''" style="margin-left:4px">清除</el-button>
|
||
</div>
|
||
<div v-if="!bindGoodsForm.good_id" class="form-hint disabled">请先选择商品</div>
|
||
<div v-else-if="bindGoodsForm._goodTag === '云服务器'" class="form-hint">云服务器商品,点击选择用户虚拟机作为归属项</div>
|
||
<div v-else class="form-hint">普通商品,点击将商品ID赋值为归属项</div>
|
||
</el-form-item>
|
||
<el-form-item label="续费价格">
|
||
<div class="unit-input-row">
|
||
<el-input-number v-model="bindGoodsForm._renewYuan" :min="0" :precision="2" controls-position="right" style="flex:1" />
|
||
<span class="unit-text">元</span>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="基础价格">
|
||
<div class="unit-input-row">
|
||
<el-input-number v-model="bindGoodsForm._baseYuan" :min="0" :precision="2" controls-position="right" style="flex:1" />
|
||
<span class="unit-text">元</span>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="到期时间">
|
||
<el-date-picker v-model="bindGoodsForm.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-form-item label="备注">
|
||
<el-input v-model="bindGoodsForm.note" type="textarea" :rows="2" />
|
||
</el-form-item>
|
||
</el-form>
|
||
</el-tab-pane>
|
||
</el-tabs>
|
||
<template #footer>
|
||
<el-button @click="createVisible = false">取消</el-button>
|
||
<el-button v-if="createDialogTab === 'create'" type="primary" :loading="createLoading" @click="submitCreate">确定创建</el-button>
|
||
<el-button v-if="createDialogTab === 'bind'" type="primary" :loading="bindGoodsFormLoading" @click="submitBindGoodsForm">确定</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 编辑用户商品弹窗 -->
|
||
<el-dialog v-model="editVisible" title="编辑用户商品" width="600px" destroy-on-close class="scrollable-dialog">
|
||
<el-form ref="editFormRef" :model="editForm" :rules="editRules" label-width="110px">
|
||
<el-form-item label="归属项">
|
||
<div class="selector-row">
|
||
<el-input :model-value="editForm._itemName || (editForm.item_id ? `虚拟机 #${editForm.item_id}` : '')" readonly placeholder="可选" style="flex:1" />
|
||
<el-button type="primary" @click="handleEditItemSelect" :disabled="!editForm._goodId" style="margin-left:8px">{{ editForm._goodTag === '云服务器' ? '选择用户虚拟机' : '使用商品ID' }}</el-button>
|
||
<el-button v-if="editForm.item_id" @click="editForm.item_id = 0; editForm._itemName = ''" style="margin-left:4px">清除</el-button>
|
||
</div>
|
||
<div v-if="editForm._goodTag === '云服务器'" class="form-hint">云服务器商品,点击选择用户虚拟机作为归属项</div>
|
||
<div v-else class="form-hint">普通商品,点击分配商品ID作为归属项</div>
|
||
</el-form-item>
|
||
<el-form-item label="续费价格">
|
||
<div class="unit-input-row">
|
||
<el-input-number v-model="editForm._renewYuan" :min="0" :precision="2" controls-position="right" style="flex:1" />
|
||
<span class="unit-text">元</span>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="基础价格">
|
||
<div class="unit-input-row">
|
||
<el-input-number v-model="editForm._baseYuan" :min="0" :precision="2" controls-position="right" style="flex:1" />
|
||
<span class="unit-text">元</span>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="到期时间">
|
||
<el-date-picker v-model="editForm.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-form-item label="备注">
|
||
<el-input v-model="editForm.note" type="textarea" :rows="2" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<div class="dialog-footer">
|
||
<el-button @click="editVisible = false">取消</el-button>
|
||
<el-button type="primary" :loading="editLoading" @click="submitEdit">保存</el-button>
|
||
</div>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 用户虚拟机选择弹窗 -->
|
||
<el-dialog v-model="showVmListDialog" title="选择用户虚拟机" width="800px" append-to-body destroy-on-close>
|
||
<div style="margin-bottom:12px">
|
||
<el-form :inline="true" size="default">
|
||
<el-form-item label="关键词">
|
||
<el-input v-model="vmListQuery.key" placeholder="搜索" clearable style="width:180px"
|
||
@keyup.enter="loadVmListForItem" @clear="loadVmListForItem" />
|
||
</el-form-item>
|
||
<el-form-item label="用户ID">
|
||
<div class="selector-row">
|
||
<el-input :model-value="vmListQuery._userName || (vmListQuery.user_id ? `用户 #${vmListQuery.user_id}` : '')"
|
||
readonly placeholder="按用户筛选" style="flex:1" @click="showVmUserSelector = true" />
|
||
<el-button v-if="vmListQuery.user_id" @click="vmListQuery.user_id = ''; vmListQuery._userName = ''; loadVmListForItem()" style="margin-left:4px">清除</el-button>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="状态">
|
||
<el-select v-model="vmListQuery.status" placeholder="筛选状态" clearable style="width:120px" @change="loadVmListForItem">
|
||
<el-option label="运行中" value="running" />
|
||
<el-option label="已停止" value="stopped" />
|
||
<el-option label="未知" value="unknown" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" @click="loadVmListForItem">搜索</el-button>
|
||
<el-button @click="loadVmListForItem" :icon="Refresh">刷新</el-button>
|
||
<el-button @click="resetVmListFilters">重置</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
<el-table :data="vmListForItem" v-loading="vmListLoading" highlight-current-row
|
||
@current-change="r => vmListSelected = r" :height="350" style="width:100%"
|
||
:header-cell-style="{ background: '#f8f9fa', color: '#2c3e50', fontWeight: 600 }">
|
||
<el-table-column prop="id" label="ID" width="80" />
|
||
<el-table-column label="虚拟机名称" min-width="160" show-overflow-tooltip>
|
||
<template #default="{ row }">{{ row.name || '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="配置" min-width="120">
|
||
<template #default="{ row }">
|
||
<div v-if="row.vcpu && row.memory">
|
||
{{ row.vcpu }}core / {{ formatMemory(row.memory) }}
|
||
</div>
|
||
<span v-else>-</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="状态" width="80">
|
||
<template #default="{ row }">
|
||
<el-tag :type="getStatusType(row.status)" size="small">
|
||
{{ getStatusText(row.status) }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="绑定状态" width="90">
|
||
<template #default="{ row }">
|
||
<el-tag :type="row.bound ? 'success' : 'info'" size="small">
|
||
{{ row.bound ? '已绑定' : '未绑定' }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="IP地址" min-width="180" show-overflow-tooltip>
|
||
<template #default="{ row }">{{ row.ips || '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="用户ID" width="80">
|
||
<template #default="{ row }">{{ row.user_id || '-' }}</template>
|
||
</el-table-column>
|
||
<template #empty>
|
||
<el-empty description="暂无虚拟机数据" :image-size="80" />
|
||
</template>
|
||
</el-table>
|
||
<div style="display:flex;justify-content:flex-end;margin-top:12px">
|
||
<el-pagination v-model:current-page="vmListQuery.page" v-model:page-size="vmListQuery.count"
|
||
:total="vmListTotal" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next" background
|
||
@size-change="s => { vmListQuery.count = s; vmListQuery.page = 1; loadVmListForItem() }"
|
||
@current-change="p => { vmListQuery.page = p; loadVmListForItem() }" />
|
||
</div>
|
||
<template #footer>
|
||
<div class="dialog-footer">
|
||
<el-button @click="showVmListDialog = false">取消</el-button>
|
||
<el-button type="primary" :disabled="!vmListSelected" @click="confirmVmForItem">确定选择</el-button>
|
||
</div>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 订单参数配置弹窗 -->
|
||
<el-dialog v-model="showArgsDialog" title="配置订单参数" width="600px" append-to-body destroy-on-close class="scrollable-dialog">
|
||
<div v-if="argsSpecLoading" v-loading="true" style="min-height:120px"></div>
|
||
<el-empty v-else-if="argsSpecList.length === 0" description="该商品暂无参数配置" :image-size="80" />
|
||
<div v-else>
|
||
<div v-for="spec in argsSpecList" :key="spec.id" class="args-spec-item">
|
||
<div class="args-spec-label">
|
||
{{ spec.name }}
|
||
<el-tag v-if="spec.must" size="small" type="danger" style="margin-left:6px">必填</el-tag>
|
||
</div>
|
||
<template v-if="spec.type === 'select' && spec.attrs && spec.attrs.length > 0">
|
||
<el-radio-group v-model="argsValues[spec.id]" size="small" @change="buildArgsJson">
|
||
<el-radio-button v-for="attr in spec.attrs" :key="attr.id" :value="attr.id">{{ attr.name }}</el-radio-button>
|
||
</el-radio-group>
|
||
</template>
|
||
<template v-else-if="spec.type === 'number'">
|
||
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap">
|
||
<el-input-number
|
||
v-model="argsDisplayValues[spec.id]"
|
||
:min="hasUnit(spec) ? fromBaseUnit(spec.min ?? 0, argsDisplayUnits[spec.id], getArgKey(spec)) : (spec.min ?? 0)"
|
||
:max="hasUnit(spec) ? fromBaseUnit(spec.max ?? 0, argsDisplayUnits[spec.id], getArgKey(spec)) : (spec.max ?? 0)"
|
||
:step="hasUnit(spec) ? (fromBaseUnit(spec.step ?? 1, argsDisplayUnits[spec.id], getArgKey(spec)) || 1) : (spec.step ?? 1)"
|
||
:step-strictly="true"
|
||
@change="onArgsNumberChange(spec)"
|
||
style="width:200px"
|
||
/>
|
||
<el-select v-if="hasUnit(spec)" :model-value="argsDisplayUnits[spec.id]" size="default" style="width:90px" @change="(newUnit) => onArgsUnitChange(spec, newUnit)">
|
||
<el-option v-for="u in getParamUnits(spec)" :key="u" :label="u" :value="u" />
|
||
</el-select>
|
||
<span class="form-hint" style="margin-top:0">范围: {{ spec.min ?? 0 }} ~ {{ spec.max ?? 0 }}
|
||
<template v-if="hasUnit(spec)"> {{ getBaseUnit(getArgKey(spec)) }}</template>,步长: {{ spec.step ?? 1 }}</span>
|
||
</div>
|
||
</template>
|
||
<template v-else>
|
||
<el-input v-model="argsValues[spec.id]" placeholder="请输入值" style="width:200px" @input="buildArgsJson" />
|
||
</template>
|
||
</div>
|
||
<div v-if="editForm.order_args" style="margin-top:16px">
|
||
<div class="form-hint" style="margin-bottom:6px;margin-top:0">生成的参数 JSON:</div>
|
||
<el-input v-model="editForm.order_args" type="textarea" :rows="4" readonly style="font-family:monospace;font-size:12px" />
|
||
</div>
|
||
</div>
|
||
<template #footer>
|
||
<div class="dialog-footer">
|
||
<el-button @click="showArgsDialog = false">取消</el-button>
|
||
<el-button type="primary" @click="showArgsDialog = false">确定</el-button>
|
||
</div>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 到期提醒记录弹窗 -->
|
||
<el-dialog v-model="remindVisible" title="到期提醒记录" width="700px" destroy-on-close append-to-body>
|
||
<div style="margin-bottom:12px;display:flex;justify-content:space-between;align-items:center">
|
||
<span style="font-size:13px;color:#909399">用户商品 ID: {{ remindGoodsId }}</span>
|
||
<el-button type="primary" size="small" :icon="Refresh" @click="loadRemindList">刷新</el-button>
|
||
</div>
|
||
<el-table :data="remindList" v-loading="remindLoading" stripe size="small" :max-height="400">
|
||
<el-table-column prop="id" label="ID" width="60" />
|
||
<el-table-column label="用户" width="100">
|
||
<template #default="{ row }">{{ row.user?.UserName || row.user?.user_name || `#${row.userId ?? row.user_id ?? '-'}` }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="提醒类型" width="80">
|
||
<template #default="{ row }">
|
||
<el-tag size="small" :type="row.type === 'manual' ? 'warning' : 'info'">{{ row.type === 'manual' ? '手动' : '自动' }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="发送方式" width="80">
|
||
<template #default="{ row }">
|
||
<el-tag size="small" :type="row.method === 'sms' ? 'success' : row.method === 'email' ? 'primary' : 'info'">{{ row.method === 'sms' ? '短信' : row.method === 'email' ? '邮件' : (row.method || '-') }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="发送状态" width="80">
|
||
<template #default="{ row }">
|
||
<el-tag size="small" :type="row.success ? 'success' : 'danger'">{{ row.success ? '成功' : '失败' }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="发送时间" min-width="160">
|
||
<template #default="{ row }">{{ formatRemindTime(row) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="备注" min-width="120" show-overflow-tooltip>
|
||
<template #default="{ row }">{{ row.note || '-' }}</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div style="display:flex;justify-content:flex-end;margin-top:12px" v-if="remindTotal > remindQuery.count">
|
||
<el-pagination v-model:current-page="remindQuery.page" :page-size="remindQuery.count" :total="remindTotal" layout="total, prev, pager, next" small background @current-change="loadRemindList" />
|
||
</div>
|
||
</el-dialog>
|
||
|
||
<!-- 选择器组件 -->
|
||
<ProductSelector v-model="showProductSelector" default-tag="云服务器" @confirm="handleProductSelect" />
|
||
<ProductSelector v-model="showProductSelectorEdit" default-tag="云服务器" @confirm="handleProductSelectEdit" />
|
||
<UserSelector v-model:visible="showUserSelector" @select="u => { createForm.user_id = u.user_id; createForm._userName = u.user_name }" />
|
||
<UserSelector v-model:visible="showUserSelectorEdit" @select="u => { editForm.user_id = u.user_id; editForm._userName = u.user_name }" />
|
||
<UserSelector v-model:visible="showVmUserSelector" @select="handleVmUserSelect" />
|
||
<OrderSelector v-model="showOrderSelector" @confirm="o => { createForm.order_id = o.id; createForm._orderName = o.name }" />
|
||
<OrderSelector v-model="showOrderSelectorEdit" @confirm="handleOrderSelectEdit" />
|
||
<PlanSelector v-model="showPlanSelector" :good-id="createForm.good_id || 0" @confirm="handlePlanSelectedForCreate" />
|
||
<PlanSelector v-model="showPlanSelectorEdit" :good-id="editForm.good_id || 0" @confirm="handlePlanSelectEdit" />
|
||
<KvmServiceSelector v-model="showServiceSelector" @confirm="s => { createForm._serviceId = s.id; createForm._serviceName = s.name; createForm.image_id = 0; createForm._imageName = ''; createForm.host_id = 0; createForm._hostName = ''; createForm.host_group_id = 0; createForm._hostGroupName = ''; createForm.network_ids = []; createForm._networkNames = '' }" />
|
||
<ImageSelectorPopup v-model="showImageSelector" :good-id="createForm.good_id || 0" :use-user-vm-api="true" @confirm="img => { createForm.image_id = img.id; createForm._imageName = img.name }" />
|
||
<!-- 宿主机选择器 -->
|
||
<HostSelectorPopup v-model="showHostSelector" :service-id="createForm._serviceId || 0" @confirm="h => { createForm.host_id = h.id; createForm._hostName = h.name || h.ip; createForm.network_ids = []; createForm._networkNames = '' }" />
|
||
<!-- 宿主机组选择器 -->
|
||
<HostGroupSelectorPopup v-model="showHostGroupSelector" :service-id="createForm._serviceId || 0" @confirm="g => { createForm.host_group_id = g.id; createForm._hostGroupName = g.name }" />
|
||
<!-- 网络选择器(多选用弹窗内表格) -->
|
||
<NetworkSelectorPopup v-model="showNetworkSelector" :service-id="createForm._serviceId || 0" :host-id="createForm.host_id || 0" filter-used="false" @confirm="n => addNetwork(n)" />
|
||
<UserVmSecurityGroupSelector v-model="showSgSelector" :user-goods-id="editForm.id" @confirm="sg => { editForm.port_group_id = sg.id; editForm._sgName = sg.name }" />
|
||
<UserVmNetworkSelector v-model="showEditNetworkSelector" :user-goods-id="editForm.id" @confirm="net => { editForm.internet_network_id = net.id; editForm._networkName = net.name }" />
|
||
|
||
<!-- 绑定虚拟机弹窗 -->
|
||
<el-dialog v-model="bindVmDialogVisible" title="绑定虚拟机" width="900px" destroy-on-close append-to-body>
|
||
<div v-if="bindTargetRow" style="margin-bottom:8px;color:#909399;font-size:13px">
|
||
绑定目标用户商品: <b>{{ bindTargetRow.good?.name || '-' }}</b>(ID: {{ bindTargetRow.id }}),用户: {{ bindTargetRow.user?.UserName || '-' }}
|
||
</div>
|
||
<!-- 全局模式:商品选择 + 筛选 -->
|
||
<template v-if="!bindTargetRow">
|
||
<div style="margin-bottom:12px">
|
||
<el-form :inline="true" size="default">
|
||
<el-form-item label="商品">
|
||
<div class="selector-row" style="width:260px">
|
||
<el-input
|
||
:model-value="bindSelectedProduct ? `${bindSelectedProduct.name}(ID: ${bindSelectedProduct.id})` : ''"
|
||
readonly placeholder="请先选择商品" style="flex:1;cursor:pointer"
|
||
@click="showBindProductSelector = true" />
|
||
<el-button v-if="bindSelectedProduct" link @click.stop="bindSelectedProduct = null; bindVmList = []; bindVmTotal = 0">清除</el-button>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="关键词">
|
||
<el-input v-model="bindVmQuery.key" placeholder="搜索" clearable style="width:140px"
|
||
:disabled="!bindSelectedProduct"
|
||
@keyup.enter="loadBindVmList" @clear="loadBindVmList" />
|
||
</el-form-item>
|
||
<el-form-item label="状态">
|
||
<el-select v-model="bindVmQuery.status" placeholder="全部" clearable style="width:110px"
|
||
:disabled="!bindSelectedProduct" @change="loadBindVmList">
|
||
<el-option label="运行中" value="running" />
|
||
<el-option label="已停止" value="stopped" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" :disabled="!bindSelectedProduct" @click="loadBindVmList">搜索</el-button>
|
||
<el-button :disabled="!bindSelectedProduct" @click="bindVmQuery.key = ''; bindVmQuery.status = ''; loadBindVmList()">重置</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
<el-alert v-if="!bindSelectedProduct" title="请先选择商品,选择后将自动加载该商品下的虚拟机列表" type="info" :closable="false" show-icon style="margin-bottom:12px" />
|
||
</template>
|
||
<!-- 行内模式:只有筛选 -->
|
||
<div v-else style="margin-bottom:12px">
|
||
<el-form :inline="true" size="default">
|
||
<el-form-item label="关键词">
|
||
<el-input v-model="bindVmQuery.key" placeholder="搜索" clearable style="width:160px"
|
||
@keyup.enter="loadBindVmList" @clear="loadBindVmList" />
|
||
</el-form-item>
|
||
<el-form-item label="状态">
|
||
<el-select v-model="bindVmQuery.status" placeholder="全部" clearable style="width:120px" @change="loadBindVmList">
|
||
<el-option label="运行中" value="running" />
|
||
<el-option label="已停止" value="stopped" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" @click="loadBindVmList">搜索</el-button>
|
||
<el-button @click="bindVmQuery.key = ''; bindVmQuery.status = ''; loadBindVmList()">重置</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
<el-table v-if="bindTargetRow || bindSelectedProduct" :data="bindVmList" v-loading="bindVmLoading" stripe :height="380" style="width:100%"
|
||
:header-cell-style="{ background: '#f8f9fa', color: '#2c3e50', fontWeight: 600 }">
|
||
<el-table-column prop="id" label="ID" width="80" />
|
||
<el-table-column label="虚拟机名称" min-width="150" show-overflow-tooltip>
|
||
<template #default="{ row }">{{ row.name || '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="配置" min-width="120">
|
||
<template #default="{ row }">
|
||
<span v-if="row.vcpu && row.memory">{{ row.vcpu }}核 / {{ formatMemory(row.memory) }}</span>
|
||
<span v-else>-</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="状态" width="80">
|
||
<template #default="{ row }">
|
||
<el-tag :type="vmStatusType(row.status)" size="small">{{ vmStatusLabel(row.status) }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="绑定状态" width="90">
|
||
<template #default="{ row }">
|
||
<el-tag :type="row.bound ? 'success' : 'info'" size="small">{{ row.bound ? '已绑定' : '未绑定' }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="IP地址" min-width="180" show-overflow-tooltip>
|
||
<template #default="{ row }">{{ row.ips || '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="用户" width="80">
|
||
<template #default="{ row }">{{ row.user_id || '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="80" fixed="right">
|
||
<template #default="{ row }">
|
||
<el-button v-if="!row.bound" link type="primary" size="small"
|
||
:loading="bindSubmitLoading && bindSubmitVmId === row.id"
|
||
@click="onBindVmClick(row)">绑定</el-button>
|
||
<span v-else class="text-muted">-</span>
|
||
</template>
|
||
</el-table-column>
|
||
<template #empty>
|
||
<el-empty description="暂无虚拟机数据" :image-size="80" />
|
||
</template>
|
||
</el-table>
|
||
<div v-if="bindTargetRow || bindSelectedProduct" style="display:flex;justify-content:flex-end;margin-top:12px">
|
||
<el-pagination v-model:current-page="bindVmQuery.page" v-model:page-size="bindVmQuery.count"
|
||
:total="bindVmTotal" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next" background
|
||
@size-change="s => { bindVmQuery.count = s; bindVmQuery.page = 1; loadBindVmList() }"
|
||
@current-change="p => { bindVmQuery.page = p; loadBindVmList() }" />
|
||
</div>
|
||
<template #footer>
|
||
<el-button @click="bindVmDialogVisible = false">关闭</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 选择用户商品弹窗(全局绑定时使用) -->
|
||
<el-dialog v-model="bindGoodsDialogVisible" title="选择要绑定的用户商品" width="750px" destroy-on-close append-to-body>
|
||
<div style="margin-bottom:8px;color:#909399;font-size:13px">
|
||
待绑定虚拟机: <b>{{ bindSelectedVm?.name || '-' }}</b>(ID: {{ bindSelectedVm?.id }})
|
||
</div>
|
||
<div style="margin-bottom:12px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px">
|
||
<el-form :inline="true" size="default" style="margin:0">
|
||
<el-form-item label="用户" style="margin-bottom:0">
|
||
<div class="selector-row" style="width:200px">
|
||
<el-input
|
||
:model-value="bindGoodsQuery._userName || (bindGoodsQuery.user_id ? `用户 #${bindGoodsQuery.user_id}` : '')"
|
||
readonly placeholder="点击选择用户" style="flex:1;cursor:pointer"
|
||
@click="showBindGoodsUserSelector = true" />
|
||
<el-button v-if="bindGoodsQuery.user_id" link @click.stop="bindGoodsQuery.user_id = ''; bindGoodsQuery._userName = ''; loadBindGoodsList()">清除</el-button>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item style="margin-bottom:0">
|
||
<el-button type="primary" @click="loadBindGoodsList">搜索</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
<el-button type="primary" @click="handleCreateFromBind">新建用户商品</el-button>
|
||
</div>
|
||
<el-table :data="bindGoodsList" v-loading="bindGoodsLoading" highlight-current-row
|
||
@current-change="r => bindGoodsSelected = r" :height="300" style="width:100%"
|
||
:header-cell-style="{ background: '#f8f9fa', color: '#2c3e50', fontWeight: 600 }">
|
||
<el-table-column prop="id" label="ID" width="70" />
|
||
<el-table-column label="商品名称" min-width="150" show-overflow-tooltip>
|
||
<template #default="{ row }">{{ row.good?.name || '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="用户" min-width="120">
|
||
<template #default="{ row }">{{ row.user?.UserName || '-' }} ({{ row.userId || row.user_id || '-' }})</template>
|
||
</el-table-column>
|
||
<el-table-column label="绑定状态" width="90">
|
||
<template #default="{ row }">
|
||
<el-tag :type="row.itemId ? 'success' : 'info'" size="small">{{ row.itemId ? '已绑定' : '未绑定' }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="到期时间" width="150">
|
||
<template #default="{ row }">{{ formatExpireTime(row.expireTime) }}</template>
|
||
</el-table-column>
|
||
<template #empty>
|
||
<el-empty description="暂无用户商品数据" :image-size="80" />
|
||
</template>
|
||
</el-table>
|
||
<div style="display:flex;justify-content:flex-end;margin-top:12px">
|
||
<el-pagination v-model:current-page="bindGoodsQuery.page" v-model:page-size="bindGoodsQuery.count"
|
||
:total="bindGoodsTotal" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next" background
|
||
@size-change="s => { bindGoodsQuery.count = s; bindGoodsQuery.page = 1; loadBindGoodsList() }"
|
||
@current-change="p => { bindGoodsQuery.page = p; loadBindGoodsList() }" />
|
||
</div>
|
||
<template #footer>
|
||
<el-button @click="bindGoodsDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" :disabled="!bindGoodsSelected" :loading="bindSubmitLoading" @click="confirmGlobalBind">确定绑定</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
<UserSelector v-model:visible="showBindGoodsUserSelector" @select="handleBindGoodsUserSelect" />
|
||
|
||
<!-- 绑定专用商品选择器 -->
|
||
<ProductSelector v-model="showBindProductSelector" default-tag="云服务器" @confirm="handleBindProductSelected" />
|
||
|
||
<!-- 新建用户商品弹窗(绑定流程中使用) -->
|
||
<el-dialog v-model="bindCreateGoodsVisible" title="新建用户商品" width="520px" destroy-on-close append-to-body>
|
||
<el-form ref="bindCreateGoodsFormRef" :model="bindCreateGoodsForm" :rules="bindCreateGoodsRules" label-width="100px">
|
||
<el-form-item label="商品" prop="good_id">
|
||
<div class="selector-row">
|
||
<el-input :model-value="bindCreateGoodsForm._goodName || (bindCreateGoodsForm.good_id ? `商品 #${bindCreateGoodsForm.good_id}` : '')"
|
||
readonly placeholder="请选择商品" style="flex:1" />
|
||
<el-button type="primary" @click="showBindCreateProductSelector = true" style="margin-left:8px">选择</el-button>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="用户" prop="user_id">
|
||
<div class="selector-row">
|
||
<el-input :model-value="bindCreateGoodsForm._userName || (bindCreateGoodsForm.user_id ? `用户 #${bindCreateGoodsForm.user_id}` : '')"
|
||
readonly placeholder="请选择用户" style="flex:1" />
|
||
<el-button type="primary" @click="showBindCreateUserSelector = true" style="margin-left:8px">选择</el-button>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="续费价格">
|
||
<div class="unit-input-row">
|
||
<el-input-number v-model="bindCreateGoodsForm._renewYuan" :min="0" :precision="2" controls-position="right" style="flex:1" />
|
||
<span class="unit-text">元</span>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="基础价格">
|
||
<div class="unit-input-row">
|
||
<el-input-number v-model="bindCreateGoodsForm._baseYuan" :min="0" :precision="2" controls-position="right" style="flex:1" />
|
||
<span class="unit-text">元</span>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="到期时间">
|
||
<el-date-picker v-model="bindCreateGoodsForm.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-form-item label="备注">
|
||
<el-input v-model="bindCreateGoodsForm.note" type="textarea" :rows="2" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="bindCreateGoodsVisible = false">取消</el-button>
|
||
<el-button type="primary" :loading="bindCreateGoodsLoading" @click="submitBindCreateGoods">确定</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
<ProductSelector v-model="showBindCreateProductSelector" default-tag="云服务器" @confirm="p => { bindCreateGoodsForm.good_id = p.id; bindCreateGoodsForm._goodName = p.name }" />
|
||
<UserSelector v-model:visible="showBindCreateUserSelector" @select="u => { bindCreateGoodsForm.user_id = u.user_id; bindCreateGoodsForm._userName = u.user_name }" />
|
||
|
||
<!-- 绑定tab的选择器 -->
|
||
<ProductSelector v-model="showBindGoodsProductSelector" default-tag="云服务器" @confirm="p => { bindGoodsForm.good_id = p.id; bindGoodsForm._goodName = p.name; bindGoodsForm._goodTag = p.tag || '云服务器' }" />
|
||
<UserSelector v-model:visible="showBindGoodsUserSel" @select="u => { bindGoodsForm.user_id = u.user_id; bindGoodsForm._userName = u.user_name }" />
|
||
<OrderSelector v-model="showBindGoodsOrderSelector" @confirm="o => { bindGoodsForm.order_id = o.id; bindGoodsForm._orderName = o.name }" />
|
||
<PlanSelector v-model="showBindGoodsPlanSelector" :good-id="bindGoodsForm.good_id || 0" @confirm="p => { bindGoodsForm.good_plan_id = p.id; bindGoodsForm._planName = p.name }" />
|
||
|
||
<!-- 筛选区选择器 -->
|
||
<UserSelector v-model:visible="showFilterUserSelector" @select="u => { query.user_id = u.user_id; filterUserName = u.user_name; handleSearch() }" />
|
||
<ProductSelector v-model="showFilterProductSelector" default-tag="云服务器" @confirm="p => { query.good_id = p.id; filterGoodName = p.name; handleSearch() }" />
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, computed, onMounted, nextTick } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import { Plus, Refresh, Search, ArrowDown } from '@element-plus/icons-vue'
|
||
import { getUserVmList, getUserVmDetail, createUserVm, updateUserVm, deleteUserVm, getUserGoodsList, createUserGoods, updateUserGoods, deleteUserGoods, bindUserVm, getExpireRemindList, sendExpireRemind } from '@/api/admin/userVm'
|
||
import { getProductParameterList, getProductPlanDetail } from '@/api/admin/product'
|
||
import { hasUnit, getArgKey, getBaseUnit, getParamUnits, getParamDefaultUnit, toBaseUnit, fromBaseUnit } from '@/utils/dynamicUnit'
|
||
import { extractApiError } from '@/utils/kvmErrorUtil'
|
||
import { formatToApiTime, vmStatusLabel, vmStatusType } 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 PlanSelector from '@/components/admin/PlanSelector.vue'
|
||
import KvmServiceSelector from '@/components/admin/KvmServiceSelector.vue'
|
||
import ImageSelectorPopup from '@/components/admin/ImageSelectorPopup.vue'
|
||
import HostSelectorPopup from '@/components/admin/HostSelectorPopup.vue'
|
||
import HostGroupSelectorPopup from '@/components/admin/HostGroupSelectorPopup.vue'
|
||
import NetworkSelectorPopup from '@/components/admin/NetworkSelectorPopup.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, bound: null, user_id: '', good_id: '', key: '' })
|
||
const filterUserName = ref('')
|
||
const filterGoodName = ref('')
|
||
const showFilterUserSelector = ref(false)
|
||
const showFilterProductSelector = ref(false)
|
||
|
||
const listError = ref('')
|
||
|
||
const formatMemory = (kb) => {
|
||
if (!kb) return '-'
|
||
if (kb >= 1048576) return (kb / 1048576).toFixed(1) + ' GB'
|
||
if (kb >= 1024) return (kb / 1024).toFixed(0) + ' MB'
|
||
return kb + ' KB'
|
||
}
|
||
|
||
const formatExpireTime = (t) => {
|
||
if (!t) return '-'
|
||
const d = dayjs(t)
|
||
if (d.year() < 2000) return '永久'
|
||
return d.format('YYYY-MM-DD HH:mm')
|
||
}
|
||
|
||
const loadGoods = async () => {
|
||
// 不再需要加载商品列表,直接加载用户商品
|
||
loadList()
|
||
}
|
||
|
||
|
||
const loadList = async () => {
|
||
loading.value = true
|
||
listError.value = ''
|
||
try {
|
||
const params = { page: query.page, count: query.count, tag: '云服务器' }
|
||
if (query.bound !== null && query.bound !== undefined && query.bound !== '') params.bound = query.bound
|
||
if (query.user_id) params.user_id = query.user_id
|
||
if (query.good_id) params.good_id = query.good_id
|
||
if (query.key) params.key = query.key
|
||
const res = await getUserGoodsList(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.meta?.count ?? d.all_count ?? d.total ?? list.value.length
|
||
} else {
|
||
list.value = []
|
||
total.value = 0
|
||
if (res?.data?.message) {
|
||
listError.value = res.data.message
|
||
}
|
||
}
|
||
} catch (e) {
|
||
list.value = []
|
||
total.value = 0
|
||
const errData = e?.response?.data
|
||
listError.value = extractApiError(errData, '获取列表失败')
|
||
} finally { loading.value = false }
|
||
}
|
||
|
||
const handleSearch = () => { query.page = 1; loadList() }
|
||
|
||
|
||
const goDetail = (row) => {
|
||
if (!row.id) return
|
||
router.push({ path: '/user-goods/vm-detail', query: { id: row.id } })
|
||
}
|
||
|
||
// ---- 新建 ----
|
||
const createVisible = ref(false)
|
||
const createLoading = ref(false)
|
||
const createDialogTab = ref('create')
|
||
let pendingTab = 'create'
|
||
const onCreateDialogOpened = () => { createDialogTab.value = pendingTab }
|
||
|
||
// 绑定虚拟机 tab(新增用户商品)
|
||
const bindGoodsFormRef = ref(null)
|
||
const bindGoodsFormLoading = ref(false)
|
||
const showBindGoodsProductSelector = ref(false)
|
||
const showBindGoodsUserSel = ref(false)
|
||
const showBindGoodsOrderSelector = ref(false)
|
||
const showBindGoodsPlanSelector = ref(false)
|
||
const bindGoodsForm = reactive({
|
||
good_id: 0, _goodName: '', _goodTag: '', user_id: 0, _userName: '',
|
||
order_id: 0, _orderName: '', good_plan_id: 0, _planName: '',
|
||
item_id: 0, _itemName: '', order_args: '',
|
||
_renewYuan: 0, _baseYuan: 0, expire_time: '', note: ''
|
||
})
|
||
const bindGoodsFormRules = {
|
||
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' }]
|
||
}
|
||
const resetBindGoodsForm = () => {
|
||
Object.assign(bindGoodsForm, {
|
||
good_id: 0, _goodName: '', _goodTag: '', user_id: 0, _userName: '',
|
||
order_id: 0, _orderName: '', good_plan_id: 0, _planName: '',
|
||
item_id: 0, _itemName: '', order_args: '',
|
||
_renewYuan: 0, _baseYuan: 0, expire_time: '', note: ''
|
||
})
|
||
}
|
||
const handleBindGoodsItemSelect = () => {
|
||
if (bindGoodsForm._goodTag === '云服务器') {
|
||
vmItemTarget.value = 'bind'
|
||
showVmListDialog.value = true
|
||
loadVmListForItem()
|
||
} else {
|
||
bindGoodsForm.item_id = bindGoodsForm.good_id
|
||
bindGoodsForm._itemName = `商品 #${bindGoodsForm.good_id}`
|
||
ElMessage.success('已将商品ID赋值为归属项')
|
||
}
|
||
}
|
||
const submitBindGoodsForm = async () => {
|
||
try { await bindGoodsFormRef.value?.validate() } catch { return }
|
||
bindGoodsFormLoading.value = true
|
||
try {
|
||
const payload = {
|
||
good_id: bindGoodsForm.good_id,
|
||
user_id: bindGoodsForm.user_id,
|
||
item_id: bindGoodsForm.item_id || 0,
|
||
renew_price: Math.round((bindGoodsForm._renewYuan || 0) * 100),
|
||
base_price: Math.round((bindGoodsForm._baseYuan || 0) * 100),
|
||
note: bindGoodsForm.note || ''
|
||
}
|
||
if (bindGoodsForm.order_id) payload.order_id = bindGoodsForm.order_id
|
||
if (bindGoodsForm.good_plan_id) payload.good_plan_id = bindGoodsForm.good_plan_id
|
||
if (bindGoodsForm.order_args) payload.order_args = bindGoodsForm.order_args
|
||
if (bindGoodsForm.expire_time) payload.expire_time = formatToApiTime(bindGoodsForm.expire_time)
|
||
const res = await createUserGoods(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 { bindGoodsFormLoading.value = false }
|
||
}
|
||
const createFormRef = ref(null)
|
||
const showProductSelector = ref(false)
|
||
const showUserSelector = ref(false)
|
||
const showOrderSelector = ref(false)
|
||
const showPlanSelector = ref(false)
|
||
const handlePlanSelectedForCreate = (plan) => {
|
||
createForm.good_plan_id = plan.id
|
||
createForm._planName = plan.name || `套餐 #${plan.id}`
|
||
}
|
||
const showServiceSelector = ref(false)
|
||
const showImageSelector = ref(false)
|
||
const showHostSelector = ref(false)
|
||
const showHostGroupSelector = ref(false)
|
||
const showNetworkSelector = ref(false)
|
||
|
||
// 编辑相关变量
|
||
const editFormRef = ref(null)
|
||
const showProductSelectorEdit = ref(false)
|
||
const showUserSelectorEdit = ref(false)
|
||
const showOrderSelectorEdit = ref(false)
|
||
const showPlanSelectorEdit = ref(false)
|
||
|
||
// 订单参数配置
|
||
const argsSpecList = ref([])
|
||
const argsSpecLoading = ref(false)
|
||
const argsValues = reactive({})
|
||
const argsDisplayValues = reactive({})
|
||
const argsDisplayUnits = reactive({})
|
||
const showArgsDialog = ref(false)
|
||
|
||
// ---- item_id 选择 ----
|
||
const vmItemTarget = ref('create')
|
||
const showVmListDialog = ref(false)
|
||
const vmListForItem = ref([])
|
||
const vmListLoading = ref(false)
|
||
const vmListSelected = ref(null)
|
||
const vmListTotal = ref(0)
|
||
const vmListQuery = reactive({ page: 1, count: 10, key: '', user_id: '', status: '', _userName: '' })
|
||
const showVmUserSelector = ref(false)
|
||
|
||
const createForm = reactive({
|
||
good_id: 0, _goodName: '', user_id: 0, _userName: '',
|
||
order_id: 0, _orderName: '', name: '',
|
||
_memoryValue: 0, _memoryUnit: 'MB', vcpu: 0,
|
||
system_size: 0, _systemSizeUnit: 'GB',
|
||
rx_bandwidth: 0, _rxUnit: 'Mbps',
|
||
tx_bandwidth: 0, _txUnit: 'Mbps',
|
||
_serviceId: 0, _serviceName: '', image_id: 0, _imageName: '',
|
||
host_id: 0, _hostName: '', host_group_id: 0, _hostGroupName: '',
|
||
network_ids: [], _networkNames: '',
|
||
ipv4_num: 0, ipv6_num: 0, snapshot_num: 0, backup_num: 0,
|
||
_renewPriceYuan: 0, _basePriceYuan: 0,
|
||
data_volume_size: 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' }],
|
||
memory: [{ required: true, validator: (r, v, cb) => createForm._memoryValue > 0 ? cb() : cb(new Error('请填写内存')), trigger: 'blur' }],
|
||
vcpu: [{ required: true, validator: (r, v, cb) => v > 0 ? cb() : cb(new Error('请填写vCPU')), trigger: 'blur' }],
|
||
system_size: [{ required: true, validator: (r, v, cb) => v > 0 ? cb() : cb(new Error('请填写系统盘大小')), trigger: 'blur' }],
|
||
image_id: [{ required: true, validator: (r, v, cb) => v > 0 ? cb() : cb(new Error('请选择镜像')), trigger: 'blur' }]
|
||
}
|
||
|
||
const editRules = {
|
||
// 简化的验证规则,只验证必要字段
|
||
}
|
||
|
||
const handleCreate = (tab = 'create') => {
|
||
Object.assign(createForm, {
|
||
good_id: 0, _goodName: '', user_id: 0, _userName: '', order_id: 0, _orderName: '', name: '',
|
||
_memoryValue: 0, _memoryUnit: 'MB', vcpu: 0,
|
||
system_size: 0, _systemSizeUnit: 'GB',
|
||
rx_bandwidth: 0, _rxUnit: 'Mbps', tx_bandwidth: 0, _txUnit: 'Mbps',
|
||
_serviceId: 0, _serviceName: '', image_id: 0, _imageName: '',
|
||
host_id: 0, _hostName: '', host_group_id: 0, _hostGroupName: '',
|
||
network_ids: [], _networkNames: '',
|
||
ipv4_num: 0, ipv6_num: 0, snapshot_num: 0, backup_num: 0,
|
||
_renewPriceYuan: 0, _basePriceYuan: 0, data_volume_size: 0, note: '', expire_time: ''
|
||
})
|
||
resetBindGoodsForm()
|
||
pendingTab = tab
|
||
createVisible.value = true
|
||
}
|
||
|
||
// 处理编辑弹窗中的选择虚拟机功能
|
||
const handleSelectVmInEdit = async () => {
|
||
if (!editForm._currentGoodId) {
|
||
ElMessage.warning('当前编辑项没有商品ID')
|
||
return
|
||
}
|
||
|
||
try {
|
||
// 调用用户虚拟机列表API,传递当前编辑项的商品ID
|
||
const res = await getUserVmList({
|
||
page: 1,
|
||
count: 10,
|
||
good_id: editForm._currentGoodId // 传递当前编辑项的商品ID
|
||
})
|
||
if (res.data.code === 200) {
|
||
console.log('用户虚拟机列表:', res.data.data)
|
||
ElMessage.success(`已获取商品ID ${editForm._currentGoodId} 的虚拟机数据`)
|
||
}
|
||
} catch (error) {
|
||
console.error('获取用户虚拟机列表失败:', error)
|
||
ElMessage.error(`获取商品ID ${editForm._currentGoodId} 的虚拟机数据失败`)
|
||
}
|
||
}
|
||
|
||
// 处理商品选择
|
||
const handleProductSelect = async (product) => {
|
||
// 设置商品信息
|
||
createForm.good_id = product.id
|
||
createForm._goodName = product.name
|
||
createForm.image_id = 0
|
||
createForm._imageName = ''
|
||
|
||
// 调用用户虚拟机列表API
|
||
try {
|
||
const res = await getUserVmList({
|
||
page: 1,
|
||
count: 10,
|
||
good_id: product.id // 传递当前选择的商品ID
|
||
})
|
||
if (res.data.code === 200) {
|
||
console.log('用户虚拟机列表:', res.data.data)
|
||
ElMessage.success(`已获取商品 ${product.name} 的虚拟机数据`)
|
||
}
|
||
} catch (error) {
|
||
console.error('获取用户虚拟机列表失败:', error)
|
||
ElMessage.error(`获取商品 ${product.name} 的虚拟机数据失败`)
|
||
}
|
||
}
|
||
|
||
const submitCreate = () => {
|
||
createFormRef.value?.validate(async (valid) => {
|
||
if (!valid) return
|
||
createLoading.value = true
|
||
try {
|
||
const memKB = createForm._memoryUnit === 'GB'
|
||
? (createForm._memoryValue || 0) * 1024 * 1024
|
||
: (createForm._memoryValue || 0) * 1024
|
||
const sysSize = createForm._systemSizeUnit === 'TB'
|
||
? (createForm.system_size || 0) * 1024
|
||
: (createForm.system_size || 0)
|
||
const rxBw = createForm._rxUnit === 'Gbps'
|
||
? (createForm.rx_bandwidth || 0) * 1000
|
||
: (createForm.rx_bandwidth || 0)
|
||
const txBw = createForm._txUnit === 'Gbps'
|
||
? (createForm.tx_bandwidth || 0) * 1000
|
||
: (createForm.tx_bandwidth || 0)
|
||
const payload = {
|
||
good_id: createForm.good_id,
|
||
user_id: createForm.user_id,
|
||
name: createForm.name,
|
||
memory: Math.round(memKB),
|
||
vcpu: createForm.vcpu,
|
||
system_size: Math.round(sysSize),
|
||
rx_bandwidth: Math.round(rxBw),
|
||
tx_bandwidth: Math.round(txBw),
|
||
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) * 100),
|
||
base_price: Math.round((createForm._basePriceYuan || 0) * 100),
|
||
note: createForm.note
|
||
}
|
||
if (createForm.data_volume_size > 0) payload.data_volume_size = createForm.data_volume_size
|
||
if (createForm.order_id) payload.order_id = createForm.order_id
|
||
if (createForm.expire_time) payload.expire_time = formatToApiTime(createForm.expire_time)
|
||
if (createForm.host_id) payload.host_id = createForm.host_id
|
||
if (createForm.host_group_id) payload.host_group_id = createForm.host_group_id
|
||
if (createForm.network_ids.length) payload.network_ids = createForm.network_ids
|
||
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 addNetwork = (n) => {
|
||
if (!createForm.network_ids.includes(n.id)) {
|
||
createForm.network_ids.push(n.id)
|
||
const names = createForm._networkNames ? createForm._networkNames + ', ' + (n.name || n.address || `#${n.id}`) : (n.name || n.address || `#${n.id}`)
|
||
createForm._networkNames = names
|
||
}
|
||
}
|
||
|
||
// ---- 编辑 ----
|
||
const editVisible = ref(false)
|
||
const editLoading = ref(false)
|
||
const showSgSelector = ref(false)
|
||
const showEditNetworkSelector = ref(false)
|
||
const editForm = reactive({
|
||
id: 0,
|
||
note: '',
|
||
_renewYuan: 0,
|
||
_baseYuan: 0,
|
||
expire_time: '',
|
||
item_id: 0,
|
||
_itemName: '',
|
||
_goodId: 0,
|
||
_goodTag: ''
|
||
})
|
||
|
||
const handleEdit = async (row) => {
|
||
if (!row.itemId) { ElMessage.warning('该用户商品未绑定虚拟机,无法编辑'); return }
|
||
|
||
Object.assign(editForm, {
|
||
id: row.id,
|
||
note: row.note || '',
|
||
_renewYuan: ((row.renewPrice || row.renew_price || 0) / 100),
|
||
_baseYuan: ((row.basePrice || row.base_price || 0) / 100),
|
||
expire_time: row.expireTime || row.expire_time || '',
|
||
item_id: row.itemId || row.item_id || 0,
|
||
_itemName: row.itemName || row._itemName || '',
|
||
_goodId: row.goodId || row.good_id || 0,
|
||
_goodTag: row.good?.tag || row._goodTag || ''
|
||
})
|
||
|
||
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
|
||
editForm.root_password = vm.root_password || ''
|
||
}
|
||
const inSg = d.vm?.in_port_group
|
||
if (inSg) { editForm.port_group_id = inSg.id; editForm._sgName = inSg.name }
|
||
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 = {
|
||
id: editForm.id,
|
||
note: editForm.note,
|
||
renew_price: Math.round((editForm._renewYuan || 0) * 100),
|
||
base_price: Math.round((editForm._baseYuan || 0) * 100)
|
||
}
|
||
if (editForm.expire_time) payload.expire_time = formatToApiTime(editForm.expire_time)
|
||
if (editForm.item_id) payload.item_id = editForm.item_id
|
||
|
||
const res = await updateUserGoods(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 handleProductSelectEdit = (p) => {
|
||
editForm.good_id = p.id
|
||
editForm._goodName = p.name
|
||
editForm._goodTag = p.tag || ''
|
||
editForm.item_id = 0
|
||
editForm._itemName = ''
|
||
clearArgsConfig()
|
||
}
|
||
|
||
const handleUserSelectEdit = (u) => {
|
||
editForm.user_id = u.user_id
|
||
editForm._userName = u.user_name
|
||
}
|
||
|
||
const handleOrderSelectEdit = (o) => {
|
||
editForm.order_id = o.id
|
||
editForm._orderName = o.name
|
||
}
|
||
|
||
const handlePlanSelectEdit = async (plan) => {
|
||
editForm.good_plan_id = plan.id
|
||
editForm._planName = plan.name || `套餐 #${plan.id}`
|
||
if (plan.args) {
|
||
editForm.order_args = typeof plan.args === 'string' ? plan.args : JSON.stringify(plan.args)
|
||
}
|
||
if (plan.id && editForm.good_id) {
|
||
try {
|
||
const res = await getProductPlanDetail({ good_id: String(editForm.good_id), plan_id: String(plan.id) })
|
||
if (res?.data?.code === 200 && res.data.data?.args) {
|
||
editForm.order_args = typeof res.data.data.args === 'string' ? res.data.data.args : JSON.stringify(res.data.data.args)
|
||
}
|
||
} catch { /* keep what we have */ }
|
||
}
|
||
}
|
||
|
||
const handleItemSelect = () => {
|
||
if (editForm._goodTag === '云服务器') {
|
||
vmItemTarget.value = 'edit'
|
||
showVmListDialog.value = true
|
||
loadVmListForItem()
|
||
} else {
|
||
editForm.item_id = editForm.good_id
|
||
editForm._itemName = `商品 #${editForm.good_id}`
|
||
ElMessage.success('已将商品ID赋值为归属项')
|
||
}
|
||
}
|
||
|
||
const handleEditItemSelect = () => {
|
||
if (editForm._goodTag === '云服务器') {
|
||
vmItemTarget.value = 'edit'
|
||
showVmListDialog.value = true
|
||
loadVmListForItem()
|
||
} else {
|
||
editForm.item_id = editForm._goodId
|
||
editForm._itemName = `商品 #${editForm._goodId}`
|
||
ElMessage.success('已将商品ID赋值为归属项')
|
||
}
|
||
}
|
||
|
||
const argsCount = computed(() => {
|
||
try { const arr = JSON.parse(editForm.order_args || '[]'); return Array.isArray(arr) ? arr.length : 0 } catch { return 0 }
|
||
})
|
||
|
||
const openArgsDialog = () => {
|
||
if (!editForm.good_id) return
|
||
showArgsDialog.value = true
|
||
if (argsSpecList.value.length === 0) loadArgsSpec(editForm.good_id)
|
||
}
|
||
|
||
const clearArgsConfig = () => {
|
||
argsSpecList.value = []
|
||
for (const k in argsValues) delete argsValues[k]
|
||
editForm.order_args = ''
|
||
}
|
||
|
||
const loadArgsSpec = async (goodId) => {
|
||
if (!goodId) return
|
||
argsSpecLoading.value = true
|
||
try {
|
||
const res = await getProductParameterList({ good_id: goodId })
|
||
if (res?.data?.code === 200) {
|
||
argsSpecList.value = res.data.data || []
|
||
for (const spec of argsSpecList.value) {
|
||
if (spec.type === 'number' && argsValues[spec.id] === undefined) {
|
||
argsValues[spec.id] = spec.min ?? 0
|
||
if (hasUnit(spec)) {
|
||
argsDisplayUnits[spec.id] = getParamDefaultUnit(spec)
|
||
argsDisplayValues[spec.id] = fromBaseUnit(spec.min ?? 0, argsDisplayUnits[spec.id], getArgKey(spec))
|
||
} else {
|
||
argsDisplayValues[spec.id] = spec.min ?? 0
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} catch { argsSpecList.value = [] } finally { argsSpecLoading.value = false }
|
||
}
|
||
|
||
const onArgsNumberChange = (spec) => {
|
||
if (hasUnit(spec)) {
|
||
argsValues[spec.id] = Math.round(toBaseUnit(argsDisplayValues[spec.id] || 0, argsDisplayUnits[spec.id], getArgKey(spec)))
|
||
} else {
|
||
argsValues[spec.id] = argsDisplayValues[spec.id]
|
||
}
|
||
buildArgsJson()
|
||
}
|
||
const onArgsUnitChange = (spec, newUnit) => {
|
||
const argKey = getArgKey(spec)
|
||
const oldUnit = argsDisplayUnits[spec.id]
|
||
const oldDisplay = argsDisplayValues[spec.id] || 0
|
||
const baseValue = oldUnit ? toBaseUnit(oldDisplay, oldUnit, argKey) : oldDisplay
|
||
argsDisplayUnits[spec.id] = newUnit
|
||
argsDisplayValues[spec.id] = fromBaseUnit(baseValue, newUnit, argKey)
|
||
argsValues[spec.id] = Math.round(baseValue)
|
||
buildArgsJson()
|
||
}
|
||
|
||
const buildArgsJson = () => {
|
||
const argsArray = []
|
||
for (const spec of argsSpecList.value) {
|
||
const val = argsValues[spec.id]
|
||
if (val === undefined || val === '') continue
|
||
if (spec.type === 'select') {
|
||
const attr = spec.attrs?.find(a => a.id === val)
|
||
if (attr) argsArray.push({ arg_id: spec.id, name: spec.name, attr_id: attr.id, value: attr.value || '' })
|
||
} else if (spec.type === 'number') {
|
||
argsArray.push({ arg_id: spec.id, name: spec.name, attr_id: 0, number: Number(val) })
|
||
} else {
|
||
argsArray.push({ arg_id: spec.id, name: spec.name, attr_id: 0, value: String(val) })
|
||
}
|
||
}
|
||
editForm.order_args = argsArray.length > 0 ? JSON.stringify(argsArray) : ''
|
||
}
|
||
|
||
const loadVmListForItem = async () => {
|
||
vmListLoading.value = true
|
||
try {
|
||
const params = { page: vmListQuery.page, count: vmListQuery.count }
|
||
if (vmListQuery.key) params.key = vmListQuery.key
|
||
if (vmListQuery.user_id) params.user_id = parseInt(vmListQuery.user_id) || undefined
|
||
if (vmListQuery.status) params.status = vmListQuery.status
|
||
// good_id parameter
|
||
if (vmItemTarget.value === 'edit' && editForm._goodId) {
|
||
params.good_id = editForm._goodId
|
||
} else if (vmItemTarget.value === 'create' && createForm.good_id) {
|
||
params.good_id = createForm.good_id
|
||
} else if (vmItemTarget.value === 'bind' && bindGoodsForm.good_id) {
|
||
params.good_id = bindGoodsForm.good_id
|
||
}
|
||
const res = await getUserVmList(params)
|
||
if (res?.data?.code === 200 && res?.data?.data) {
|
||
const d = res.data.data
|
||
vmListForItem.value = d.data || (Array.isArray(d) ? d : [])
|
||
vmListTotal.value = d.all_count ?? d.total ?? vmListForItem.value.length
|
||
}
|
||
} catch { vmListForItem.value = [] } finally { vmListLoading.value = false }
|
||
}
|
||
|
||
const confirmVmForItem = () => {
|
||
if (!vmListSelected.value) return
|
||
const vm = vmListSelected.value
|
||
if (vmItemTarget.value === 'edit') {
|
||
editForm.item_id = vm.id
|
||
editForm._itemName = `虚拟机 #${vm.id}`
|
||
} else if (vmItemTarget.value === 'bind') {
|
||
bindGoodsForm.item_id = vm.id
|
||
bindGoodsForm._itemName = `虚拟机 #${vm.id}`
|
||
} else {
|
||
createForm.item_id = vm.id
|
||
createForm._itemName = `虚拟机 #${vm.id}`
|
||
}
|
||
showVmListDialog.value = false
|
||
vmListSelected.value = null
|
||
ElMessage.success('虚拟机已选择')
|
||
}
|
||
|
||
const resetVmListFilters = () => {
|
||
vmListQuery.key = ''
|
||
vmListQuery.user_id = ''
|
||
vmListQuery.status = ''
|
||
vmListQuery._userName = ''
|
||
vmListQuery.page = 1
|
||
loadVmListForItem()
|
||
}
|
||
|
||
const handleVmUserSelect = (user) => {
|
||
vmListQuery.user_id = user.user_id
|
||
vmListQuery._userName = user.user_name
|
||
showVmUserSelector.value = false
|
||
loadVmListForItem()
|
||
}
|
||
|
||
const getStatusType = (status) => {
|
||
switch (status) {
|
||
case 'running': return 'success'
|
||
case 'stop': return 'danger'
|
||
case 'stopped': return 'danger'
|
||
default: return 'info'
|
||
}
|
||
}
|
||
|
||
const getStatusText = (status) => {
|
||
switch (status) {
|
||
case 'running': return '运行中'
|
||
case 'stop': return '已停止'
|
||
case 'stopped': return '已停止'
|
||
default: return status || '未知'
|
||
}
|
||
}
|
||
|
||
// ---- 更多操作 ----
|
||
const handleMoreCmd = (cmd, row) => {
|
||
if (cmd === 'remind') openRemindList(row)
|
||
else if (cmd === 'send') handleSendRemind(row)
|
||
else if (cmd === 'delete') handleDeleteGoods(row)
|
||
}
|
||
|
||
// ---- 删除用户商品 ----
|
||
const handleDeleteGoods = (row) => {
|
||
const hasBoundVm = row.itemId && row.itemId !== 0
|
||
const msg = hasBoundVm
|
||
? `该商品(ID: ${row.id})已绑定虚拟机(VM ID: ${row.itemId}),删除后虚拟机将不再关联此商品记录,确定删除?`
|
||
: `确定删除该用户商品(ID: ${row.id})吗?`
|
||
ElMessageBox.confirm(msg, '删除确认', { type: 'error', confirmButtonText: '确认删除' })
|
||
.then(async () => {
|
||
try {
|
||
const res = await deleteUserGoods({ 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(() => {})
|
||
}
|
||
|
||
// ---- 到期提醒 ----
|
||
const remindVisible = ref(false)
|
||
const remindLoading = ref(false)
|
||
const remindList = ref([])
|
||
const remindTotal = ref(0)
|
||
const remindGoodsId = ref(0)
|
||
const remindQuery = reactive({ page: 1, count: 10 })
|
||
|
||
const openRemindList = (row) => {
|
||
remindGoodsId.value = row.id
|
||
remindQuery.page = 1
|
||
remindVisible.value = true
|
||
loadRemindList()
|
||
}
|
||
|
||
const formatRemindTime = (row) => {
|
||
const t = row.CreatedAt || row.created_at || row.send_time
|
||
if (!t || t.startsWith('0001')) return '-'
|
||
try { return dayjs(t).format('YYYY-MM-DD HH:mm:ss') } catch { return t }
|
||
}
|
||
|
||
const loadRemindList = async () => {
|
||
remindLoading.value = true
|
||
try {
|
||
const res = await getExpireRemindList({ user_goods_id: remindGoodsId.value, page: remindQuery.page, count: remindQuery.count })
|
||
if (res?.data?.code === 200 && res?.data?.data) {
|
||
const d = res.data.data
|
||
remindList.value = d.data || d.list || (Array.isArray(d) ? d : [])
|
||
remindTotal.value = d.all_count ?? d.meta?.count ?? d.total ?? remindList.value.length
|
||
} else { remindList.value = []; remindTotal.value = 0 }
|
||
} catch { remindList.value = []; remindTotal.value = 0 }
|
||
finally { remindLoading.value = false }
|
||
}
|
||
|
||
const handleSendRemind = (row) => {
|
||
ElMessageBox.confirm(`确定手动发送到期提醒给该用户商品(ID: ${row.id})吗?`, '发送确认', { type: 'warning', confirmButtonText: '确认发送' })
|
||
.then(async () => {
|
||
try {
|
||
const res = await sendExpireRemind({ user_goods_id: row.id, user_id: row.userId || row.user_id })
|
||
if (res?.data?.code === 200) ElMessage.success('发送成功')
|
||
else ElMessage.error(extractApiError(res?.data, '发送失败'))
|
||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '发送失败')) }
|
||
}).catch(() => {})
|
||
}
|
||
|
||
// ---- 绑定虚拟机 ----
|
||
const bindVmDialogVisible = ref(false)
|
||
const bindTargetRow = ref(null)
|
||
const bindVmList = ref([])
|
||
const bindVmLoading = ref(false)
|
||
const bindVmTotal = ref(0)
|
||
const bindVmQuery = reactive({ page: 1, count: 10, key: '', status: '' })
|
||
const bindSubmitLoading = ref(false)
|
||
const bindSubmitVmId = ref(0)
|
||
const bindSelectedVm = ref(null)
|
||
const bindSelectedProduct = ref(null)
|
||
const showBindProductSelector = ref(false)
|
||
|
||
const bindGoodsDialogVisible = ref(false)
|
||
const bindGoodsList = ref([])
|
||
const bindGoodsLoading = ref(false)
|
||
const bindGoodsTotal = ref(0)
|
||
const bindGoodsSelected = ref(null)
|
||
const bindGoodsQuery = reactive({ page: 1, count: 10, user_id: '', _userName: '' })
|
||
const showBindGoodsUserSelector = ref(false)
|
||
|
||
const handleGlobalBindVm = () => {
|
||
bindTargetRow.value = null
|
||
bindSelectedProduct.value = null
|
||
bindVmList.value = []
|
||
bindVmTotal.value = 0
|
||
bindVmQuery.page = 1
|
||
bindVmQuery.key = ''
|
||
bindVmQuery.status = ''
|
||
bindVmDialogVisible.value = true
|
||
}
|
||
|
||
const handleBindProductSelected = (product) => {
|
||
bindSelectedProduct.value = product
|
||
bindVmQuery.page = 1
|
||
bindVmQuery.key = ''
|
||
bindVmQuery.status = ''
|
||
loadBindVmList()
|
||
}
|
||
|
||
const handleBindVm = (row) => {
|
||
bindTargetRow.value = row
|
||
bindSelectedProduct.value = null
|
||
bindVmQuery.page = 1
|
||
bindVmQuery.key = ''
|
||
bindVmQuery.status = ''
|
||
bindVmDialogVisible.value = true
|
||
loadBindVmList()
|
||
}
|
||
|
||
const loadBindVmList = async () => {
|
||
bindVmLoading.value = true
|
||
try {
|
||
const params = { page: bindVmQuery.page, count: bindVmQuery.count }
|
||
if (bindTargetRow.value) {
|
||
const goodId = bindTargetRow.value.good?.id || bindTargetRow.value.goodId || 0
|
||
if (goodId) params.good_id = goodId
|
||
} else if (bindSelectedProduct.value) {
|
||
params.good_id = bindSelectedProduct.value.id
|
||
}
|
||
if (bindVmQuery.key) params.key = bindVmQuery.key
|
||
if (bindVmQuery.status) params.status = bindVmQuery.status
|
||
const res = await getUserVmList(params)
|
||
if (res?.data?.code === 200 && res?.data?.data) {
|
||
const d = res.data.data
|
||
bindVmList.value = d.data || (Array.isArray(d) ? d : [])
|
||
bindVmTotal.value = d.all_count ?? d.total ?? bindVmList.value.length
|
||
} else { bindVmList.value = []; bindVmTotal.value = 0 }
|
||
} catch { bindVmList.value = [] } finally { bindVmLoading.value = false }
|
||
}
|
||
|
||
const onBindVmClick = (vm) => {
|
||
if (bindTargetRow.value) {
|
||
submitBindVm(vm)
|
||
} else {
|
||
bindSelectedVm.value = vm
|
||
bindGoodsSelected.value = null
|
||
bindGoodsQuery.page = 1
|
||
bindGoodsQuery.user_id = ''
|
||
bindGoodsQuery._userName = ''
|
||
bindGoodsDialogVisible.value = true
|
||
loadBindGoodsList()
|
||
}
|
||
}
|
||
|
||
const handleBindGoodsUserSelect = (user) => {
|
||
bindGoodsQuery.user_id = String(user.user_id || user.id || '')
|
||
bindGoodsQuery._userName = user.user_name || user.name || ''
|
||
showBindGoodsUserSelector.value = false
|
||
loadBindGoodsList()
|
||
}
|
||
|
||
const bindCreateGoodsVisible = ref(false)
|
||
const bindCreateGoodsLoading = ref(false)
|
||
const bindCreateGoodsFormRef = ref(null)
|
||
const showBindCreateProductSelector = ref(false)
|
||
const showBindCreateUserSelector = ref(false)
|
||
const bindCreateGoodsForm = reactive({
|
||
good_id: 0, _goodName: '', user_id: 0, _userName: '',
|
||
_renewYuan: 0, _baseYuan: 0, expire_time: '', note: ''
|
||
})
|
||
const bindCreateGoodsRules = {
|
||
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' }]
|
||
}
|
||
|
||
const handleCreateFromBind = () => {
|
||
Object.assign(bindCreateGoodsForm, {
|
||
good_id: 0, _goodName: '', user_id: 0, _userName: '',
|
||
_renewYuan: 0, _baseYuan: 0, expire_time: '', note: ''
|
||
})
|
||
bindCreateGoodsVisible.value = true
|
||
}
|
||
|
||
const submitBindCreateGoods = async () => {
|
||
try { await bindCreateGoodsFormRef.value?.validate() } catch { return }
|
||
bindCreateGoodsLoading.value = true
|
||
try {
|
||
const payload = {
|
||
good_id: bindCreateGoodsForm.good_id,
|
||
user_id: bindCreateGoodsForm.user_id,
|
||
renew_price: Math.round((bindCreateGoodsForm._renewYuan || 0) * 100),
|
||
base_price: Math.round((bindCreateGoodsForm._baseYuan || 0) * 100),
|
||
note: bindCreateGoodsForm.note || ''
|
||
}
|
||
if (bindCreateGoodsForm.expire_time) payload.expire_time = formatToApiTime(bindCreateGoodsForm.expire_time)
|
||
const res = await createUserGoods(payload)
|
||
if (res?.data?.code === 200) {
|
||
ElMessage.success('创建用户商品成功')
|
||
bindCreateGoodsVisible.value = false
|
||
loadBindGoodsList()
|
||
} else {
|
||
ElMessage.error(extractApiError(res?.data, '创建失败'))
|
||
}
|
||
} catch (e) {
|
||
ElMessage.error(extractApiError(e?.response?.data, '创建失败'))
|
||
} finally { bindCreateGoodsLoading.value = false }
|
||
}
|
||
|
||
const loadBindGoodsList = async () => {
|
||
bindGoodsLoading.value = true
|
||
try {
|
||
const params = { page: bindGoodsQuery.page, count: bindGoodsQuery.count, tag: '云服务器' }
|
||
if (bindGoodsQuery.user_id) params.user_id = bindGoodsQuery.user_id
|
||
const res = await getUserGoodsList(params)
|
||
if (res?.data?.code === 200 && res?.data?.data) {
|
||
const d = res.data.data
|
||
bindGoodsList.value = d.data || (Array.isArray(d) ? d : [])
|
||
bindGoodsTotal.value = d.meta?.count ?? d.all_count ?? d.total ?? bindGoodsList.value.length
|
||
} else { bindGoodsList.value = []; bindGoodsTotal.value = 0 }
|
||
} catch { bindGoodsList.value = [] } finally { bindGoodsLoading.value = false }
|
||
}
|
||
|
||
const confirmGlobalBind = async () => {
|
||
if (!bindSelectedVm.value || !bindGoodsSelected.value) return
|
||
bindSubmitLoading.value = true
|
||
try {
|
||
const res = await bindUserVm({
|
||
user_goods_id: bindGoodsSelected.value.id,
|
||
vm_id: bindSelectedVm.value.id
|
||
})
|
||
if (res?.data?.code === 200) {
|
||
ElMessage.success('绑定虚拟机成功')
|
||
bindGoodsDialogVisible.value = false
|
||
loadBindVmList()
|
||
loadList()
|
||
} else {
|
||
ElMessage.error(extractApiError(res?.data, '绑定失败'))
|
||
}
|
||
} catch (e) {
|
||
ElMessage.error(extractApiError(e?.response?.data, '绑定失败'))
|
||
} finally { bindSubmitLoading.value = false }
|
||
}
|
||
|
||
const submitBindVm = async (vm) => {
|
||
if (!bindTargetRow.value) return
|
||
bindSubmitLoading.value = true
|
||
bindSubmitVmId.value = vm.id
|
||
try {
|
||
const res = await bindUserVm({
|
||
user_goods_id: bindTargetRow.value.id,
|
||
vm_id: vm.id
|
||
})
|
||
if (res?.data?.code === 200) {
|
||
ElMessage.success('绑定虚拟机成功')
|
||
loadBindVmList()
|
||
loadList()
|
||
} else {
|
||
ElMessage.error(extractApiError(res?.data, '绑定失败'))
|
||
}
|
||
} catch (e) {
|
||
ElMessage.error(extractApiError(e?.response?.data, '绑定失败'))
|
||
} finally { bindSubmitLoading.value = false; bindSubmitVmId.value = 0 }
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadList()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.user-vm-list { padding: 0; }
|
||
|
||
.main-container { border: 1px solid #e1e8ed; background: #ffffff; }
|
||
:deep(.el-card__body) { padding: 0; }
|
||
|
||
.filter-section { padding: 0; border-bottom: 1px solid #e1e8ed; background: #fafbfc; }
|
||
.filter-content { display: flex; justify-content: space-between; align-items: center; padding: 16px 20px; gap: 20px; flex-wrap: wrap; }
|
||
.search-form { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin: 0; }
|
||
.search-form :deep(.el-form-item) { margin-bottom: 0; margin-right: 0; }
|
||
.action-bar { display: flex; gap: 12px; flex-shrink: 0; flex-wrap: wrap; align-items: center; }
|
||
|
||
.table-section { padding: 0; }
|
||
|
||
.pagination { padding: 16px 20px; border-top: 1px solid #e1e8ed; background: #fafbfc; justify-content: flex-end; }
|
||
|
||
.selector-row { display: flex; align-items: center; width: 100%; gap: 8px; }
|
||
.selector-row .el-input { flex: 1; }
|
||
.unit-input-row { display: flex; align-items: center; gap: 6px; width: 100%; }
|
||
.unit-select { width: 90px; flex-shrink: 0; }
|
||
.unit-text { font-size: 13px; color: #606266; flex-shrink: 0; white-space: nowrap; min-width: 24px; }
|
||
.dialog-footer { display: flex; justify-content: flex-end; gap: 12px; padding: 0; }
|
||
.form-hint { font-size: 12px; color: #909399; margin-top: 4px; }
|
||
.args-spec-item { margin-bottom: 16px; padding-bottom: 16px; border-bottom: 1px solid #f0f0f0; }
|
||
.args-spec-item:last-child { border-bottom: none; }
|
||
.args-spec-label { font-size: 14px; font-weight: 500; color: #303133; margin-bottom: 8px; }
|
||
|
||
: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; }
|
||
|
||
@media (max-width: 768px) {
|
||
.filter-content { flex-direction: column; align-items: stretch; gap: 12px; }
|
||
.search-form { flex-direction: column; width: 100%; }
|
||
.search-form :deep(.el-form-item) { width: 100%; }
|
||
.action-bar { width: 100%; justify-content: flex-start; }
|
||
}
|
||
</style>
|