fix: 修改新增用户商品的配置项逻辑
Build and Deploy Vue3 / build (push) Successful in 4m9s
Build and Deploy Vue3 / deploy (push) Successful in 1m3s

This commit is contained in:
2026-04-06 18:44:11 +08:00
parent c07e09c151
commit f0e89695f4
36 changed files with 2078 additions and 190 deletions
+14 -14
View File
@@ -1113,7 +1113,7 @@ const viewMode = ref('tree')
// 查询参数
const queryParams = reactive({
page: 1,
count: 100,
count: 10,
tag: undefined,
level: undefined,
disable: undefined,
@@ -1328,7 +1328,7 @@ const loadProductsForGroup = async (groupId) => {
const res = await getProductList({
good_group_id: groupId,
page: 1,
count: 100,
count: 10,
delete: false
})
@@ -1410,7 +1410,7 @@ const toggleExpand = async (row) => {
const res = await getProductGroupList({
parent_id: group.id,
level: childLevel,
count: 100
count: 10
})
if (res.data.code === 200) {
const children = res.data.data.data || []
@@ -1540,7 +1540,7 @@ const selectedTagName = computed(() => {
const fetchTagOptionsForSelector = async () => {
tagSelectorLoading.value = true
try {
const params = { page: 1, count: 100 }
const params = { page: 1, count: 10 }
if (tagSelectorSearch.value) {
params.key = tagSelectorSearch.value
}
@@ -1571,7 +1571,7 @@ const fetchTagOptionsForSelector = async () => {
// 初始化获取所有标签
const fetchAllTagOptions = async () => {
try {
const res = await getProductGroupTagList({ page: 1, count: 100 })
const res = await getProductGroupTagList({ page: 1, count: 10 })
if (res.data.code === 200) {
const data = res.data.data
if (Array.isArray(data)) {
@@ -1920,10 +1920,10 @@ const handleEditProduct = (product, parentGroupId) => {
Object.assign(productForm, {
id: product.id,
name: product.name,
table: product.table,
tag: product.tag,
content: product.content,
name: product.name || '',
table: product.table || '',
tag: typeof product.tag === 'string' ? product.tag : '',
content: product.content || '',
cover_id: product.coverId,
good_group_id: groupId,
inventory_control: product.inventoryControl,
@@ -1945,10 +1945,10 @@ const submitProductForm = () => {
if (valid) {
try {
const submitData = {
name: productForm.name.trim(),
table: productForm.table.trim(),
tag: productForm.tag.trim(),
content: productForm.content.trim(),
name: (productForm.name || '').trim(),
table: (productForm.table || '').trim(),
tag: (typeof productForm.tag === 'string' ? productForm.tag : '').trim(),
content: (productForm.content || '').trim(),
cover_id: productForm.cover_id,
good_group_id: productForm.good_group_id,
inventory_control: productForm.inventory_control,
@@ -2153,7 +2153,7 @@ const submitForm = () => {
// ==================== 分组标签管理 ====================
const tagQueryParams = reactive({
page: 1,
count: 100,
count: 10,
key: ''
})
+3 -3
View File
@@ -981,7 +981,7 @@ const toggleGroupExpand = async (row) => {
const res = await getProductGroupList({
parent_id: row.id,
level: childLevel,
count: 100
count: 10
})
if (res.data.code === 200) {
const children = res.data.data.data || []
@@ -1060,7 +1060,7 @@ const fetchProductList = async () => {
const fetchGroupList = async () => {
try {
// 获取全部分组用于下拉列表
const res = await getProductGroupList({ page: 1, count: 100 })
const res = await getProductGroupList({ page: 1, count: 10 })
if (res.data.code === 200) {
groupOptions.value = res.data.data.data || []
if (groupOptions.value.length === 0) {
@@ -1069,7 +1069,7 @@ const fetchGroupList = async () => {
}
// 获取一级分组用于树形选择器
const treeRes = await getProductGroupList({ level: 1, count: 100 })
const treeRes = await getProductGroupList({ level: 1, count: 10 })
if (treeRes.data.code === 200) {
const rootItems = treeRes.data.data.data || []
groupTreeData.value = rootItems.map(item => ({
+269 -11
View File
@@ -57,14 +57,14 @@
</div>
<!-- 新增弹窗 -->
<el-dialog v-model="createVisible" title="新增用户商品" width="560px" destroy-on-close>
<el-dialog v-model="createVisible" title="新增用户商品" width="600px" destroy-on-close class="scrollable-dialog">
<el-form ref="createFormRef" :model="createForm" :rules="createRules" label-width="110px">
<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>
<el-button v-if="createForm.good_id" @click="createForm.good_id = 0; createForm._goodName = ''" style="margin-left:4px">清除</el-button>
<el-button v-if="createForm.good_id" @click="createForm.good_id = 0; createForm._goodName = ''; createForm._goodTag = ''; createForm.item_id = 0; createForm._itemName = ''; clearArgsConfig()" style="margin-left:4px">清除</el-button>
</div>
</el-form-item>
<el-form-item label="用户" prop="user_id">
@@ -86,11 +86,37 @@
<el-form-item label="套餐">
<div class="selector-row">
<el-input :model-value="createForm._planName || (createForm.good_plan_id ? `套餐 #${createForm.good_plan_id}` : '')"
readonly placeholder="可选" style="flex:1" />
readonly placeholder="可选选择后自动填入参数" style="flex:1" />
<el-button type="primary" @click="showPlanSelector = true" style="margin-left:8px">选择</el-button>
<el-button v-if="createForm.good_plan_id" @click="createForm.good_plan_id = 0; createForm._planName = ''" style="margin-left:4px">清除</el-button>
</div>
</el-form-item>
<!-- 订单参数配置 -->
<el-form-item label="订单参数">
<div class="selector-row">
<el-input :model-value="createForm.order_args ? `已配置 ${argsCount} 个参数` : ''"
readonly placeholder="可选点击配置参数" style="flex:1" />
<el-button type="primary" @click="openArgsDialog" :disabled="!createForm.good_id" style="margin-left:8px">配置</el-button>
<el-button v-if="createForm.order_args" @click="createForm.order_args = ''; clearArgsConfig()" style="margin-left:4px">清除</el-button>
</div>
<div v-if="!createForm.good_id" style="font-size:12px;color:#c0c4cc;margin-top:4px">请先选择商品</div>
</el-form-item>
<el-form-item label="归属项">
<div class="selector-row">
<el-input :model-value="createForm._itemName || (createForm.item_id ? `#${createForm.item_id}` : '')"
readonly placeholder="可选" style="flex:1" />
<el-button type="primary" @click="handleItemSelect" :disabled="!createForm.good_id" style="margin-left:8px">
{{ createForm._goodTag === '云服务器' ? '选择虚拟机' : '使用商品ID' }}
</el-button>
<el-button v-if="createForm.item_id" @click="createForm.item_id = 0; createForm._itemName = ''" style="margin-left:4px">清除</el-button>
</div>
<div v-if="!createForm.good_id" style="font-size:12px;color:#c0c4cc;margin-top:4px">请先选择商品</div>
<div v-else-if="createForm._goodTag === '云服务器'" style="font-size:12px;color:#909399;margin-top:4px">云服务器商品,点击选择用户虚拟机作为归属项</div>
<div v-else style="font-size:12px;color:#909399;margin-top:4px">普通商品,点击将商品ID赋值为归属项</div>
</el-form-item>
<el-form-item label="续费价格()">
<el-input-number v-model="createForm._renewYuan" :min="0" :precision="2" controls-position="right" style="width:100%" />
</el-form-item>
@@ -134,21 +160,99 @@
</template>
</el-dialog>
<ProductSelector v-model="showProductSelector" @confirm="p => { createForm.good_id = p.id; createForm._goodName = p.name }" />
<ProductSelector v-model="showProductSelector" @confirm="handleProductSelected" />
<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 }" />
<PlanSelector v-model="showPlanSelector" :good-id="createForm.good_id" @confirm="p => { createForm.good_plan_id = p.id; createForm._planName = p.name }" />
<PlanSelector v-model="showPlanSelector" :good-id="createForm.good_id" @confirm="handlePlanSelectedForCreate" />
<!-- 用户虚拟机选择弹窗(item_id 为云服务器时使用) -->
<el-dialog v-model="showVmListDialog" title="选择用户虚拟机" width="800px" append-to-body destroy-on-close>
<div class="filter-section" 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>
<el-button type="primary" @click="loadVmListForItem">搜索</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%">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column label="用户" min-width="120">
<template #default="{ row }">{{ row.user?.UserName || row.user?.username || '-' }}</template>
</el-table-column>
<el-table-column label="商品" min-width="140" show-overflow-tooltip>
<template #default="{ row }">{{ row.good?.name || '-' }}</template>
</el-table-column>
<el-table-column label="归属项ID" width="100">
<template #default="{ row }">{{ row.itemId || row.item_id || '-' }}</template>
</el-table-column>
<el-table-column label="到期时间" width="170">
<template #default="{ row }">{{ formatExpireTime(row.expireTime || row.expire_time) }}</template>
</el-table-column>
</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"
@size-change="s => { vmListQuery.count = s; vmListQuery.page = 1; loadVmListForItem() }"
@current-change="p => { vmListQuery.page = p; loadVmListForItem() }" />
</div>
<template #footer>
<el-button @click="showVmListDialog = false">取消</el-button>
<el-button type="primary" :disabled="!vmListSelected" @click="confirmVmForItem">确定选择</el-button>
</template>
</el-dialog>
<!-- 订单参数配置弹窗 -->
<el-dialog v-model="showArgsDialog" title="配置订单参数" width="600px" append-to-body destroy-on-close class="scrollable-dialog">
<div v-if="argsSpecLoading" style="text-align:center;padding:20px;color:#909399">加载参数中...</div>
<div v-else-if="argsSpecList.length === 0" style="text-align:center;padding:20px;color:#909399">该商品暂无参数配置</div>
<div v-else>
<div v-for="spec in argsSpecList" :key="spec.id" style="margin-bottom:16px;padding-bottom:16px;border-bottom:1px solid #f0f0f0">
<div style="font-size:14px;font-weight:500;color:#303133;margin-bottom:8px">
{{ 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">
<el-input-number v-model="argsValues[spec.id]" :min="spec.min || 0" :max="spec.max || 9999" :step="spec.step || 1" :step-strictly="true" @change="buildArgsJson" style="width:200px" />
<span style="font-size:12px;color:#909399">范围: {{ spec.min || 0 }} ~ {{ spec.max || 9999 }},步长: {{ 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="createForm.order_args" style="margin-top:8px">
<div style="font-size:12px;color:#909399;margin-bottom:6px">生成的参数 JSON</div>
<el-input v-model="createForm.order_args" type="textarea" :rows="4" readonly style="font-family:monospace;font-size:12px" />
</div>
</div>
<template #footer>
<el-button @click="showArgsDialog = false">取消</el-button>
<el-button type="primary" @click="showArgsDialog = false">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ref, reactive, computed, onMounted, watch } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Refresh, Search } from '@element-plus/icons-vue'
import { getUserGoodsList, createUserGoods, updateUserGoods, deleteUserGoods } from '@/api/admin/userVm'
import { getUserGoodsList, createUserGoods, updateUserGoods, deleteUserGoods, getUserVmList } from '@/api/admin/userVm'
import { extractApiError } from '@/utils/kvmErrorUtil'
import { formatToApiTime } from '@/utils/tool'
import { getProductParameterList, getProductPlanDetail } from '@/api/admin/product'
import ProductSelector from '@/components/admin/ProductSelector.vue'
import UserSelector from '@/components/UserSelector/index.vue'
import OrderSelector from '@/components/admin/OrderSelector.vue'
@@ -188,7 +292,86 @@ const loadList = async () => {
const handleSearch = () => { query.page = 1; loadList() }
// ---- 详情 ----
// ---- 订单参数配置 ----
const argsSpecList = ref([])
const argsSpecLoading = ref(false)
const argsValues = reactive({})
const showArgsDialog = ref(false)
const argsCount = computed(() => {
try { const arr = JSON.parse(createForm.order_args || '[]'); return Array.isArray(arr) ? arr.length : 0 } catch { return 0 }
})
const openArgsDialog = () => {
if (!createForm.good_id) return
showArgsDialog.value = true
if (argsSpecList.value.length === 0) loadArgsSpec(createForm.good_id)
}
const clearArgsConfig = () => {
argsSpecList.value = []
for (const k in argsValues) delete argsValues[k]
createForm.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 || []
// 初始化 number 类型的默认值
for (const spec of argsSpecList.value) {
if (spec.type === 'number' && argsValues[spec.id] === undefined) {
argsValues[spec.id] = spec.min || 0
}
}
}
} catch { argsSpecList.value = [] } finally { argsSpecLoading.value = false }
}
const buildArgsJson = () => {
const result = []
for (const spec of argsSpecList.value) {
const val = argsValues[spec.id]
if (val === undefined || val === null || val === '') continue
if (spec.type === 'select') {
const attr = spec.attrs?.find(a => a.id === val)
if (attr) result.push({ arg_id: spec.id, name: spec.name, attr_id: attr.id, value: attr.value, number: 0 })
} else if (spec.type === 'number') {
result.push({ arg_id: spec.id, name: spec.name, attr_id: 0, value: '', number: val })
} else {
result.push({ arg_id: spec.id, name: spec.name, attr_id: 0, value: String(val), number: 0 })
}
}
createForm.order_args = result.length > 0 ? JSON.stringify(result) : ''
}
// 选择套餐后自动填入参数
const handlePlanSelectedForCreate = async (plan) => {
createForm.good_plan_id = plan.id
createForm._planName = plan.name
// 解析套餐的 args 字段自动填入参数值
if (plan.args) {
try {
const planArgs = typeof plan.args === 'string' ? JSON.parse(plan.args) : plan.args
if (Array.isArray(planArgs)) {
for (const arg of planArgs) {
if (arg.arg_id) {
if (arg.attr_id) argsValues[arg.arg_id] = arg.attr_id
else if (arg.number !== undefined) argsValues[arg.arg_id] = arg.number
else if (arg.value) argsValues[arg.arg_id] = arg.value
}
}
buildArgsJson()
}
} catch { /* */ }
}
}
// 选择商品后加载参数
// watch 在 createForm 声明后定义,见下方
const handleDetail = (row) => {
router.push({ path: '/user-goods/vm-detail', query: { id: row.id } })
}
@@ -202,17 +385,80 @@ const showUserSelector = ref(false)
const showOrderSelector = ref(false)
const showPlanSelector = ref(false)
const createForm = reactive({
good_id: 0, _goodName: '', user_id: 0, _userName: '',
good_id: 0, _goodName: '', _goodTag: '', user_id: 0, _userName: '',
order_id: 0, _orderName: '', good_plan_id: 0, _planName: '',
_renewYuan: 0, _baseYuan: 0, note: '', expire_time: ''
item_id: 0, _itemName: '',
_renewYuan: 0, _baseYuan: 0, note: '', expire_time: '',
order_args: ''
})
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' }]
}
// 商品选择确认处理
const handleProductSelected = (p) => {
createForm.good_id = p.id
createForm._goodName = p.name
createForm._goodTag = p.tag || ''
createForm.item_id = 0
createForm._itemName = ''
}
// ---- 归属项选择(item_id ----
const showVmListDialog = ref(false)
const vmListForItem = ref([])
const vmListLoading = ref(false)
const vmListTotal = ref(0)
const vmListSelected = ref(null)
const vmListQuery = reactive({ page: 1, count: 10, key: '' })
const handleItemSelect = () => {
if (!createForm.good_id) return
if (createForm._goodTag === '云服务器') {
vmListSelected.value = null
vmListQuery.page = 1
vmListQuery.key = ''
showVmListDialog.value = true
loadVmListForItem()
} else {
createForm.item_id = createForm.good_id
createForm._itemName = `商品 #${createForm.good_id}`
ElMessage.success('已将商品ID赋值为归属项')
}
}
const loadVmListForItem = async () => {
vmListLoading.value = true
try {
const params = { page: vmListQuery.page, count: vmListQuery.count }
if (vmListQuery.key) params.key = vmListQuery.key
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
} else { vmListForItem.value = []; vmListTotal.value = 0 }
} catch { vmListForItem.value = []; vmListTotal.value = 0 } finally { vmListLoading.value = false }
}
const confirmVmForItem = () => {
if (!vmListSelected.value) return
const vm = vmListSelected.value
createForm.item_id = vm.id
createForm._itemName = `虚拟机 #${vm.id}${vm.good?.name ? ` (${vm.good.name})` : ''}`
showVmListDialog.value = false
}
// 选择商品后加载参数
watch(() => createForm.good_id, (id) => {
if (id) loadArgsSpec(id)
else clearArgsConfig()
})
const handleCreate = () => {
Object.assign(createForm, { good_id: 0, _goodName: '', user_id: 0, _userName: '', order_id: 0, _orderName: '', good_plan_id: 0, _planName: '', _renewYuan: 0, _baseYuan: 0, note: '', expire_time: '' })
Object.assign(createForm, { good_id: 0, _goodName: '', _goodTag: '', user_id: 0, _userName: '', order_id: 0, _orderName: '', good_plan_id: 0, _planName: '', item_id: 0, _itemName: '', _renewYuan: 0, _baseYuan: 0, note: '', expire_time: '', order_args: '' })
clearArgsConfig()
createVisible.value = true
}
@@ -224,10 +470,12 @@ const submitCreate = () => {
const payload = {
good_id: createForm.good_id, user_id: createForm.user_id,
order_id: createForm.order_id, good_plan_id: createForm.good_plan_id,
item_id: createForm.item_id || 0,
note: createForm.note,
renew_price: Math.round((createForm._renewYuan || 0) * 100),
base_price: Math.round((createForm._baseYuan || 0) * 100)
}
if (createForm.order_args) payload.order_args = createForm.order_args
if (createForm.expire_time) payload.expire_time = formatToApiTime(createForm.expire_time)
const res = await createUserGoods(payload)
if (res?.data?.code === 200) { ElMessage.success('新增成功'); createVisible.value = false; loadList() }
@@ -288,4 +536,14 @@ onMounted(loadList)
.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: 70vh;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: none;
}
:global(.scrollable-dialog .el-dialog__body::-webkit-scrollbar) {
display: none;
}
</style>