From 5a31de64b3cab27d6ae96fb69018618de7519990 Mon Sep 17 00:00:00 2001 From: 2256907009 <2256907009@qq.com> Date: Mon, 2 Feb 2026 18:22:58 +0800 Subject: [PATCH] =?UTF-8?q?fix:=E4=BF=AE=E6=94=B9json=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/product/ProductList.vue | 650 ++++++++++++++++++++++++++++-- 1 file changed, 614 insertions(+), 36 deletions(-) diff --git a/src/views/product/ProductList.vue b/src/views/product/ProductList.vue index e6e1d7e..dd87fb9 100644 --- a/src/views/product/ProductList.vue +++ b/src/views/product/ProductList.vue @@ -419,16 +419,18 @@ @@ -516,7 +518,7 @@ - + - -
参数配置为JSON数组格式
+
+ +
+ + + +
+ + +
+
+
{{ spec.name }}
+
+ + + + + + +
+
+
+ + + + +
+ + + 查看配置JSON + + + + 清空选择 + +
+
- - -
多个参数ID用英文逗号分隔
+ +
+
选择参数配置中未选择的参数作为额外参数(只需参数ID,不需要选择值)
+ + + + + + + +
@@ -562,6 +681,37 @@ + + +
+
+ 已选择 {{ Object.keys(selectedArgs).filter(k => selectedArgs[k] !== undefined && selectedArgs[k] !== '').length }} 个参数 + + {{ isArgsValid ? '配置有效' : '部分参数未选择' }} + +
+ +
+
+ {{ spec.name }}: + + {{ getSelectedValueDisplay(spec) || '未选择' }} + +
+
+ JSON 数据 +
{{ formatArgsJsonPreview() }}
+
+ +
+ { 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; +}