fix:修改json格式参数展示
Build and Deploy Vue3 / build (push) Successful in 1m17s
Build and Deploy Vue3 / deploy (push) Successful in 1m23s

This commit is contained in:
2026-02-02 18:22:58 +08:00
parent b4260fedb8
commit 5a31de64b3
+614 -36
View File
@@ -419,16 +419,18 @@
</el-form-item>
<!-- number 类型显示范围配置 -->
<template v-if="currentParam?.type === 'number'">
<el-divider content-position="left">数值范围配置</el-divider>
<el-divider content-position="left">数值范围配置phase</el-divider>
<el-form-item label="范围类型" prop="range_type">
<el-select v-model="paramValueForm.range_type" placeholder="请选择范围类型" style="width: 100%">
<el-option label="大于 (after)" value="after" />
<el-option label="小于 (before)" value="before" />
<el-option label="小于等于 (before)" value="before" />
<el-option label="大于等于 (after)" value="after" />
<el-option label="等于 (equal)" value="equal" />
</el-select>
<div class="form-tip">before: 数值 phase 时匹配 | after: 数值 phase 时匹配</div>
</el-form-item>
<el-form-item label="范围值" prop="attr_range">
<el-input-number v-model="paramValueForm.attr_range" placeholder="范围值" style="width: 100%" />
<el-form-item label="值" prop="attr_range">
<el-input-number v-model="paramValueForm.attr_range" :min="0" placeholder="范围值" style="width: 100%" />
<div class="form-tip">例如phase=100, rangeType=before 表示 0-100 范围</div>
</el-form-item>
</template>
<el-form-item label="排序索引" prop="index">
@@ -516,7 +518,7 @@
<el-dialog
v-model="planFormDialogVisible"
:title="planFormType === 'add' ? '新增套餐' : '编辑套餐'"
width="600px"
width="700px"
append-to-body
>
<el-form
@@ -529,20 +531,137 @@
<el-input v-model="planForm.name" placeholder="请输入套餐名称" />
</el-form-item>
<el-form-item label="说明" prop="note">
<el-input v-model="planForm.note" type="textarea" :rows="3" placeholder="请输入套餐说明" />
<el-input v-model="planForm.note" type="textarea" :rows="2" placeholder="请输入套餐说明" />
</el-form-item>
<el-form-item label="参数配置" prop="args">
<el-input
v-model="planForm.args"
type="textarea"
:rows="4"
placeholder='JSON格式,如:[{"arg_id":1,"name":"参数名","attr_id":1,"value":"值"}]'
/>
<div class="form-tip">参数配置为JSON数组格式</div>
<div class="args-config-container">
<!-- 参数选择下拉框 -->
<div class="args-select-row">
<el-select
v-model="selectedArgIds"
multiple
placeholder="请选择需要配置的参数"
style="width: 100%"
@change="onSelectedArgsChange"
>
<el-option
v-for="spec in planSpecList"
:key="spec.id"
:label="spec.name"
:value="spec.id"
/>
</el-select>
</div>
<!-- 已选参数的值配置区域 -->
<div v-if="selectedArgSpecs.length > 0" class="args-selector">
<div
v-for="spec in selectedArgSpecs"
:key="spec.id"
class="spec-item"
>
<div class="spec-label">{{ spec.name }}</div>
<div class="spec-values">
<!-- select 类型显示可选值 -->
<template v-if="spec.type === 'select' && spec.attrs && spec.attrs.length > 0">
<el-radio-group
v-model="selectedArgs[spec.id]"
size="small"
@change="updateArgsJson"
>
<el-radio-button
v-for="attr in spec.attrs"
:key="attr.id"
:value="attr.id"
>
{{ attr.name }}
</el-radio-button>
</el-radio-group>
</template>
<!-- number 类型数字输入 -->
<template v-else-if="spec.type === 'number'">
<div class="number-input-wrapper">
<el-input-number
v-model="selectedArgs[spec.id]"
:min="spec.min || 0"
:max="spec.max || 9999"
:step="spec.step || 1"
:step-strictly="true"
size="small"
@change="updateArgsJson"
/>
<span class="number-range">
(范围: {{ spec.min || 0 }} - {{ spec.max || 9999 }}步长: {{ spec.step || 1 }})
</span>
</div>
<!-- 显示匹配的价格区间 -->
<div v-if="spec.attrs && spec.attrs.length > 0 && selectedArgs[spec.id]" class="matched-attr-info">
<el-tag type="success" size="small">
匹配区间: {{ getMatchedAttrName(spec, selectedArgs[spec.id]) }}
</el-tag>
</div>
</template>
<!-- string 类型文本输入 -->
<template v-else>
<el-input
v-model="selectedArgs[spec.id]"
placeholder="请输入值"
size="small"
style="width: 200px"
@input="updateArgsJson"
/>
</template>
</div>
</div>
</div>
<el-empty v-else-if="planSpecList.length > 0" description="请先选择需要配置的参数" :image-size="60" />
<el-empty v-else description="暂无参数配置,请先为商品添加参数" :image-size="60" />
<!-- 查看JSON按钮 -->
<div class="args-actions" v-if="selectedArgSpecs.length > 0">
<el-button type="info" plain size="small" @click="showArgsPreview = true">
<el-icon><View /></el-icon>
查看配置JSON
</el-button>
<el-button type="warning" plain size="small" @click="clearArgsSelection">
<el-icon><Delete /></el-icon>
清空选择
</el-button>
</div>
</div>
</el-form-item>
<el-form-item label="额外参数" prop="extra_arg_ids">
<el-input v-model="planForm.extra_arg_ids" placeholder="额外参数ID,如:1,2,3" />
<div class="form-tip">多个参数ID用英文逗号分隔</div>
<el-form-item label="额外参数">
<div class="args-config-container">
<div class="form-tip" style="margin-bottom: 8px;">选择参数配置中未选择的参数作为额外参数只需参数ID不需要选择值</div>
<!-- 额外参数下拉选择 -->
<el-select
v-model="selectedExtraArgIds"
multiple
placeholder="请选择额外参数"
style="width: 100%"
@change="onSelectedExtraArgsChange"
>
<el-option
v-for="spec in extraSpecList"
:key="spec.id"
:label="`${spec.name} (ID: ${spec.id})`"
:value="spec.id"
/>
</el-select>
<!-- 已选额外参数展示 -->
<!-- <div v-if="selectedExtraArgIds.length > 0" class="extra-args-display">
<el-tag
v-for="argId in selectedExtraArgIds"
:key="argId"
closable
@close="removeExtraArg(argId)"
style="margin: 4px"
>
{{ getSpecNameById(argId) }} (ID: {{ argId }})
</el-tag>
</div> -->
<el-empty v-if="extraSpecList.length === 0" description="所有参数已在参数配置中选择" :image-size="40" />
</div>
</el-form-item>
<el-form-item label="排序索引" prop="index">
<el-input-number v-model="planForm.index" :min="0" style="width: 100%" />
@@ -562,6 +681,37 @@
</template>
</el-dialog>
<!-- 参数配置预览对话框 -->
<el-dialog
v-model="showArgsPreview"
title="参数配置预览"
width="500px"
append-to-body
>
<div class="args-preview">
<div class="preview-header">
<span>已选择 {{ Object.keys(selectedArgs).filter(k => selectedArgs[k] !== undefined && selectedArgs[k] !== '').length }} 个参数</span>
<el-tag :type="isArgsValid ? 'success' : 'warning'" size="small">
{{ isArgsValid ? '配置有效' : '部分参数未选择' }}
</el-tag>
</div>
<el-divider />
<div class="preview-list">
<div v-for="spec in planSpecList" :key="spec.id" class="preview-item">
<span class="preview-label">{{ spec.name }}:</span>
<span class="preview-value" :class="{ 'not-selected': !getSelectedValueDisplay(spec) }">
{{ getSelectedValueDisplay(spec) || '未选择' }}
</span>
</div>
</div>
<el-divider content-position="left">JSON 数据</el-divider>
<pre class="json-preview">{{ formatArgsJsonPreview() }}</pre>
</div>
<template #footer>
<el-button @click="showArgsPreview = false">关闭</el-button>
</template>
</el-dialog>
<!-- 商品分组选择器对话框 -->
<el-dialog
v-model="showGroupSelector"
@@ -622,7 +772,7 @@
import { ref, reactive, computed, onMounted, nextTick } from 'vue'
import { getFileDetail } from '@/api/admin/file'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Delete, Search, Refresh, Picture, ArrowRight, Loading } from '@element-plus/icons-vue'
import { Plus, Delete, Search, Refresh, Picture, ArrowRight, Loading, View } from '@element-plus/icons-vue'
import AvatarSelector from '@/components/admin/AvatarSelector.vue'
import { getProductList, createProduct, updateProduct, deleteProduct, getProductGroupList,
getProductTagList,
@@ -1371,9 +1521,9 @@ const handleEditParamValue = (row) => {
attr_id: row.id,
attr_name: row.name,
attr_value: row.value || '',
attr_price: (row.price / 100).toFixed(2) || 0,
attr_price: row.price || 0,
index: row.index || 0,
attr_range: row.range || 0,
attr_range: row.phase || 0, // API返回的字段是 phase
range_type: row.rangeType || 'equal'
})
}
@@ -1414,7 +1564,7 @@ const submitParamValueForm = () => {
if (currentParam.value.type === 'select') {
submitData.attr_value = paramValueForm.attr_value
}
// number 类型添加范围参数
// number 类型添加范围参数(提交用 attr_range,获取返回 phase
if (currentParam.value.type === 'number') {
submitData.attr_range = Number(paramValueForm.attr_range)
submitData.range_type = paramValueForm.range_type
@@ -1456,6 +1606,7 @@ const planForm = reactive({
note: '',
args: '',
extra_arg_ids: '',
extra_arg_ids_array: [],
index: 0,
disable: false
})
@@ -1464,6 +1615,269 @@ const planFormRules = {
name: [{ required: true, message: '请输入套餐名称', trigger: 'blur' }]
}
// 套餐参数选择相关
const planSpecList = ref([]) // 当前商品的参数列表
const selectedArgIds = ref([]) // 选中的参数ID列表
const selectedArgs = reactive({}) // 选中的参数值 { arg_id: value_id 或 value }
const showArgsPreview = ref(false) // 显示参数预览对话框
// 额外参数相关
const selectedExtraArgIds = ref([]) // 选中的额外参数ID列表
// 计算已选参数的spec列表
const selectedArgSpecs = computed(() => {
return planSpecList.value.filter(spec => selectedArgIds.value.includes(spec.id))
})
// 计算额外参数列表(参数配置中未选择的参数)
const extraSpecList = computed(() => {
return planSpecList.value.filter(spec => !selectedArgIds.value.includes(spec.id))
})
// 获取套餐表单用的参数列表
const fetchPlanSpecList = async () => {
if (!currentPlanProductId.value) return
try {
const res = await getProductParameterList({ good_id: currentPlanProductId.value })
if (res.data.code === 200) {
planSpecList.value = res.data.data || []
}
} catch (error) {
console.error('获取参数列表失败:', error)
planSpecList.value = []
}
}
// 参数选择变化
const onSelectedArgsChange = () => {
// 清除未选中参数的值
for (const key in selectedArgs) {
if (!selectedArgIds.value.includes(Number(key))) {
delete selectedArgs[key]
}
}
// 同时从额外参数中移除已选的参数
selectedExtraArgIds.value = selectedExtraArgIds.value.filter(
id => !selectedArgIds.value.includes(id)
)
updateArgsJson()
updateExtraArgIds()
}
// 额外参数选择变化
const onSelectedExtraArgsChange = () => {
updateExtraArgIds()
}
// 移除额外参数
const removeExtraArg = (argId) => {
selectedExtraArgIds.value = selectedExtraArgIds.value.filter(id => id !== argId)
updateExtraArgIds()
}
// 根据ID获取参数名称
const getSpecNameById = (specId) => {
const spec = planSpecList.value.find(s => s.id === specId)
return spec ? spec.name : `参数${specId}`
}
// 更新额外参数ID
const updateExtraArgIds = () => {
planForm.extra_arg_ids = selectedExtraArgIds.value.join(',')
planForm.extra_arg_ids_array = [...selectedExtraArgIds.value]
}
// 更新args JSON字符串
// 格式:{ arg_id: 参数id, name: 参数名称, attr_id: 参数值id, value/number: 参数值 }
const updateArgsJson = () => {
const argsArray = []
// 只处理已选择的参数
for (const specId of selectedArgIds.value) {
const spec = planSpecList.value.find(s => s.id === specId)
if (!spec) continue
const selectedValue = selectedArgs[spec.id]
if (selectedValue === undefined || selectedValue === '') continue
if (spec.type === 'select') {
// select 类型:找到选中的值对象
const attrObj = spec.attrs?.find(a => a.id === selectedValue)
if (attrObj) {
argsArray.push({
arg_id: spec.id,
name: spec.name,
attr_id: attrObj.id,
value: attrObj.value || ''
})
}
} else if (spec.type === 'number') {
// number 类型:根据数值找到对应的价格区间ID
const numValue = Number(selectedValue)
const matchedAttr = findMatchingNumberAttr(spec, numValue)
argsArray.push({
arg_id: spec.id,
name: spec.name,
attr_id: matchedAttr ? matchedAttr.id : 0,
number: numValue
})
} else {
// string 类型
argsArray.push({
arg_id: spec.id,
name: spec.name,
attr_id: 0,
value: String(selectedValue)
})
}
}
planForm.args = argsArray.length > 0 ? JSON.stringify(argsArray) : ''
}
// 根据数值找到匹配的价格区间
const findMatchingNumberAttr = (spec, numValue) => {
if (!spec.attrs || spec.attrs.length === 0) return null
// 按 index 排序
const sortedAttrs = [...spec.attrs].sort((a, b) => (a.index || 0) - (b.index || 0))
for (const attr of sortedAttrs) {
const phase = attr.phase || 0
const rangeType = attr.rangeType || 'before'
// rangeType: before 表示小于等于 phase
// rangeType: after 表示大于等于 phase
// rangeType: equal 表示等于 phase
if (rangeType === 'before' && numValue <= phase) {
return attr
} else if (rangeType === 'after' && numValue >= phase) {
return attr
} else if (rangeType === 'equal' && numValue === phase) {
return attr
}
}
// 如果没有匹配的,返回最后一个区间
return sortedAttrs[sortedAttrs.length - 1]
}
// 获取匹配的价格区间名称(用于模板显示)
const getMatchedAttrName = (spec, numValue) => {
const matchedAttr = findMatchingNumberAttr(spec, Number(numValue))
if (matchedAttr) {
const priceText = matchedAttr.price ? ` (+¥${(matchedAttr.price / 100).toFixed(2)})` : ''
return `${matchedAttr.name}${priceText}`
}
return '无匹配区间'
}
// 生成参数项的唯一ID(用于没有attrs的number和string类型)
const generateArgId = (argId, value) => {
// 使用参数ID和值的组合生成一个数字ID
const valueHash = String(value).split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)
return argId * 10000 + (valueHash % 10000)
}
// 更新额外参数JSON
// 清空参数选择
const clearArgsSelection = () => {
selectedArgIds.value = []
for (const key in selectedArgs) {
delete selectedArgs[key]
}
selectedExtraArgIds.value = []
planForm.args = ''
planForm.extra_arg_ids = ''
planForm.extra_arg_ids_array = []
}
// 获取选中值的显示文本
const getSelectedValueDisplay = (spec) => {
const selectedValue = selectedArgs[spec.id]
if (selectedValue === undefined || selectedValue === '') return null
if (spec.type === 'select') {
const attrObj = spec.attrs?.find(a => a.id === selectedValue)
return attrObj ? attrObj.name : null
}
return String(selectedValue)
}
// 检查参数配置是否有效
const isArgsValid = computed(() => {
// 至少选择了一个参数
return Object.keys(selectedArgs).some(k => selectedArgs[k] !== undefined && selectedArgs[k] !== '')
})
// 格式化预览JSON
const formatArgsJsonPreview = () => {
if (!planForm.args) return '[]'
try {
return JSON.stringify(JSON.parse(planForm.args), null, 2)
} catch {
return planForm.args
}
}
// 从已有的args JSON初始化选择状态
const initSelectedArgsFromJson = (argsJson, extraArgIds = []) => {
// 清空现有选择
clearArgsSelection()
// 解析 args 中包含的参数ID
const argsParamIds = []
if (argsJson) {
try {
const argsArray = typeof argsJson === 'string' ? JSON.parse(argsJson) : argsJson
if (Array.isArray(argsArray)) {
for (const arg of argsArray) {
// 通过 arg_id 找到对应的参数
const spec = planSpecList.value.find(s => s.id === arg.arg_id)
if (!spec) continue
// 添加到已选参数ID列表
argsParamIds.push(spec.id)
if (spec.type === 'select') {
// select 类型:优先使用 attr_id,兼容旧格式的 id
if (arg.attr_id) {
selectedArgs[spec.id] = arg.attr_id
} else if (arg.id) {
selectedArgs[spec.id] = arg.id
} else {
const attrObj = spec.attrs?.find(a => a.value === arg.value || a.name === arg.name)
if (attrObj) {
selectedArgs[spec.id] = attrObj.id
}
}
} else if (spec.type === 'number') {
// number 类型:优先使用 number 字段,兼容 value 字段
selectedArgs[spec.id] = Number(arg.number !== undefined ? arg.number : arg.value)
} else {
selectedArgs[spec.id] = arg.value
}
}
}
} catch (e) {
console.error('解析args失败:', e)
}
}
// 设置已选参数ID列表
selectedArgIds.value = argsParamIds
// 处理额外参数(排除已在args中的参数)
if (extraArgIds && extraArgIds.length > 0) {
selectedExtraArgIds.value = extraArgIds.filter(id => !argsParamIds.includes(id))
}
// 根据选择状态重新生成 JSON(确保初始状态就有 JSON 展示)
updateArgsJson()
}
// 打开套餐管理
const handlePlan = (row) => {
currentPlanProductId.value = row.id
@@ -1529,18 +1943,31 @@ const fetchPlanList = async () => {
}
// 新增套餐
const handleAddPlan = () => {
const handleAddPlan = async () => {
planFormType.value = 'add'
planFormDialogVisible.value = true
// 先获取参数列表
await fetchPlanSpecList()
// 清空选择状态
clearArgsSelection()
// 默认选择所有参数
selectedArgIds.value = planSpecList.value.map(spec => spec.id)
Object.assign(planForm, {
plan_id: undefined,
name: '',
note: '',
args: '',
extra_arg_ids: '',
extra_arg_ids_array: [],
index: 0,
disable: false
})
planFormDialogVisible.value = true
nextTick(() => {
planFormRef.value?.resetFields()
})
@@ -1549,32 +1976,39 @@ const handleAddPlan = () => {
// 编辑套餐
const handleEditPlan = async (row) => {
planFormType.value = 'edit'
// 先获取参数列表
await fetchPlanSpecList()
try {
const res = await getProductPlanDetail({ good_id: String(currentPlanProductId.value), plan_id: String(row.id) })
if (res.data.code === 200) {
const data = res.data.data
// 处理args字段:如果是字符串先解析,再格式化为漂亮的JSON
let argsValue = ''
if (data.args) {
try {
// 如果args是字符串,先解析为对象
const argsObj = typeof data.args === 'string' ? JSON.parse(data.args) : data.args
// 格式化为漂亮的JSON(带缩进)
argsValue = JSON.stringify(argsObj, null, 2)
} catch (e) {
// 解析失败则保持原值
argsValue = data.args
// 处理 extra_arg_ids
let extraArgIdsArray = []
if (data.extraArgIds) {
if (Array.isArray(data.extraArgIds)) {
extraArgIdsArray = data.extraArgIds
} else if (typeof data.extraArgIds === 'string') {
extraArgIdsArray = data.extraArgIds.split(',').filter(Boolean).map(Number)
}
}
Object.assign(planForm, {
plan_id: data.id,
name: data.name || '',
note: data.note || '',
args: argsValue,
extra_arg_ids: data.extraArgIds ? data.extraArgIds.join(',') : '',
args: data.args || '',
extra_arg_ids: extraArgIdsArray.join(','),
extra_arg_ids_array: extraArgIdsArray,
index: data.index || 0,
disable: data.disable || false
})
// 从已有的args初始化选择状态(包括额外参数)
initSelectedArgsFromJson(data.args, extraArgIdsArray)
planFormDialogVisible.value = true
}
} catch (error) {
@@ -1914,5 +2348,149 @@ const submitPlanForm = () => {
max-height: 450px;
overflow: hidden;
}
/* 参数配置选择器样式 */
.args-config-container {
width: 100%;
}
.args-selector {
border: 1px solid #e4e7ed;
border-radius: 4px;
padding: 12px;
background: #fafafa;
max-height: 300px;
overflow-y: auto;
}
.spec-item {
display: flex;
align-items: flex-start;
padding: 10px 0;
border-bottom: 1px dashed #e4e7ed;
transition: opacity 0.2s;
}
.spec-item:last-child {
border-bottom: none;
}
.spec-label {
width: 100px;
flex-shrink: 0;
font-weight: 500;
color: #606266;
padding-top: 4px;
}
/* 参数选择下拉行 */
.args-select-row {
margin-bottom: 12px;
}
/* 额外参数展示 */
.extra-args-display {
margin-top: 8px;
padding: 8px;
background: #f5f7fa;
border-radius: 4px;
}
.spec-values {
flex: 1;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
}
.spec-values :deep(.el-radio-group) {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.spec-values :deep(.el-radio-button__inner) {
padding: 6px 12px;
}
.number-input-wrapper {
display: flex;
align-items: center;
gap: 8px;
}
.number-range {
color: #909399;
font-size: 12px;
}
.matched-attr-info {
margin-top: 6px;
}
.args-actions {
margin-top: 12px;
display: flex;
gap: 8px;
}
/* 参数预览样式 */
.args-preview {
padding: 0;
}
.preview-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.preview-list {
max-height: 200px;
overflow-y: auto;
}
.preview-item {
display: flex;
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
}
.preview-item:last-child {
border-bottom: none;
}
.preview-label {
width: 120px;
flex-shrink: 0;
color: #606266;
font-weight: 500;
}
.preview-value {
color: #409eff;
font-weight: 500;
}
.preview-value.not-selected {
color: #c0c4cc;
font-style: italic;
}
.json-preview {
background: #f5f7fa;
border: 1px solid #e4e7ed;
border-radius: 4px;
padding: 12px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 12px;
color: #606266;
max-height: 200px;
overflow: auto;
white-space: pre-wrap;
word-break: break-all;
margin: 0;
}
</style>