docs(readme): 更新文档说明
Build and Deploy Vue3 / build (push) Successful in 1m29s
Build and Deploy Vue3 / deploy (push) Successful in 1m14s

- 添加项目使用指南
- 完善API接口描述
- 修正错误的配置示例
This commit is contained in:
shiran
2026-04-23 17:43:31 +08:00
parent 2e073c2b87
commit c0daa6ed11
6 changed files with 2734 additions and 1366 deletions
@@ -3,10 +3,16 @@
<el-dialog
:model-value="visible"
title="商品参数管理"
width="900px"
width="1000px"
top="6vh"
class="param-manager-dialog"
@update:model-value="$emit('update:visible', $event)"
>
<div class="filter-section" style="border: none; padding: 0 0 16px 0;">
<div class="param-manager-toolbar">
<div class="toolbar-info">
<el-icon><Collection /></el-icon>
<span> {{ parameterList.length }} 个参数</span>
</div>
<div class="action-bar">
<el-button type="primary" @click="handleAddParameter">
<el-icon><Plus /></el-icon>新增参数
@@ -16,48 +22,131 @@
</el-button>
</div>
</div>
<el-table
v-loading="paramLoading"
:data="parameterList"
style="width: 100%"
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
>
<el-table-column prop="id" label="参数ID" width="80" />
<el-table-column prop="name" label="参数名称" min-width="120" />
<el-table-column prop="type" label="参数类型" width="100">
<template #default="{ row }">
<el-tag :type="getArgTypeTag(row.type)">{{ getArgTypeText(row.type) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="数值配置" min-width="220">
<template #default="{ row }">
<template v-if="row.type === 'number'">
<div class="number-config">
<div>步进: {{ row.step || '-' }} | 范围: {{ row.min ?? '-' }} ~ {{ row.max ?? '-' }}
<template v-if="hasUnit(row)"> ({{ getBaseUnit(getArgKey(row)) }})</template>
</div>
<div v-if="hasUnit(row)" class="unit-info">
<el-tag size="small" type="success">{{ getArgKey(row) }}</el-tag>
<el-tag size="small" type="warning">{{ getParamDefaultUnit(row) }}</el-tag>
<div v-loading="paramLoading" class="parameter-cards-wrap">
<div v-if="!parameterList.length && !paramLoading" class="param-empty-state">
<el-empty description="暂无参数数据,点击右上角新增参数" :image-size="80" />
</div>
<div v-else class="parameter-cards-grid">
<div v-for="param in parameterList" :key="param.id" class="param-card">
<!-- 卡片头 -->
<div class="param-card-header">
<div class="param-card-title">
<el-tag :type="getArgTypeTag(param.type)" effect="dark" size="small" class="title-type-tag">
{{ getArgTypeText(param.type) }}
</el-tag>
<span class="param-name" :title="param.name">{{ param.name }}</span>
<el-tag v-if="param.must" type="danger" size="small" effect="plain" class="title-must-tag">必选</el-tag>
<span class="param-id">#{{ param.id }}</span>
</div>
<div class="param-card-actions">
<el-button type="primary" link @click="handleEditParameter(param)">
<el-icon><Edit /></el-icon>编辑
</el-button>
<el-button type="danger" link @click="handleDeleteParameter(param)">
<el-icon><Delete /></el-icon>删除
</el-button>
</div>
</div>
<!-- 权限 / 配置 元信息 -->
<div class="param-card-meta">
<el-tag size="small" :type="param.userAdd ? 'success' : 'info'" effect="plain" class="meta-tag">
<el-icon><ShoppingCart /></el-icon>
{{ param.userAdd ? '允许单独购买' : '不可单独购买' }}
</el-tag>
<el-tag size="small" :type="param.useUserGroupDiscount ? 'success' : 'info'" effect="plain" class="meta-tag">
<el-icon><User /></el-icon>
{{ param.useUserGroupDiscount ? '用户组优惠' : '无用户组优惠' }}
</el-tag>
<el-tag size="small" :type="param.useUserDiscount ? 'success' : 'info'" effect="plain" class="meta-tag">
<el-icon><Ticket /></el-icon>
{{ param.useUserDiscount ? '用户优惠' : '无用户优惠' }}
</el-tag>
</div>
<!-- number 类型配置 -->
<div v-if="param.type === 'number'" class="param-card-number-config">
<div class="number-config-item">
<span class="config-label">步进</span>
<span class="config-value">{{ param.step || '-' }}</span>
</div>
<div class="number-config-item">
<span class="config-label">范围</span>
<span class="config-value">{{ param.min ?? '-' }} ~ {{ param.max ?? '-' }}</span>
</div>
<div v-if="hasUnit(param)" class="number-config-item">
<span class="config-label">单位</span>
<el-tag size="small" type="success" effect="plain">{{ getArgKey(param) }}</el-tag>
<el-tag size="small" type="warning" effect="plain" style="margin-left: 4px">
{{ getBaseUnit(getArgKey(param)) }}
</el-tag>
</div>
</div>
<!-- 参数值列表 -->
<div class="param-card-values">
<div class="values-header">
<span class="values-title">
<el-icon><List /></el-icon>
参数值{{ (param.attrs || []).length }}
<span v-if="(param.attrs || []).length > 1" class="drag-tip-inline">
<el-icon><Rank /></el-icon>可拖动排序
</span>
</span>
<el-button type="primary" size="small" @click="handleAddParamValue(param)">
<el-icon><Plus /></el-icon>添加参数值
</el-button>
</div>
<div v-if="!(param.attrs || []).length" class="values-empty">
暂无参数值
</div>
<div v-else class="values-list">
<div
v-for="(attr, idx) in (param.attrs || [])"
:key="attr.id"
class="value-row"
:class="[
isDraggingAttr(param.id, idx) ? 'is-dragging' : '',
dragOverClass(param.id, idx)
]"
draggable="true"
@dragstart="onAttrDragStart($event, param, idx)"
@dragover="onAttrDragOver($event, param, idx)"
@dragleave="onAttrDragLeave($event, param, idx)"
@drop="onAttrDrop($event, param, idx)"
@dragend="onAttrDragEnd"
>
<el-icon class="value-drag-handle"><Rank /></el-icon>
<span class="value-order">#{{ idx }}</span>
<span class="value-name" :title="attr.name">{{ attr.name }}</span>
<span class="value-range">
<el-tag v-if="param.type === 'number'" size="small" type="info">
{{ getRangeTypeText(attr.rangeType) }}
{{ (attr.phase != null && attr.phase !== '') ? attr.phase : 0 }}
<template v-if="hasUnit(param)"> {{ getBaseUnit(getArgKey(param)) }}</template>
</el-tag>
<span v-else-if="param.type === 'select'" class="value-raw">{{ attr.value || '-' }}</span>
<span v-else class="value-raw">{{ attr.value || '-' }}</span>
</span>
<span class="value-price">
<el-icon><Money /></el-icon>
¥{{ (attr.price / 100).toFixed(2) }}
</span>
<div class="value-actions">
<el-button type="primary" link @click="handleEditParamValue(attr, param)">
<el-icon><Edit /></el-icon>
</el-button>
<el-button type="danger" link @click="handleDeleteParamValue(attr, param)">
<el-icon><Delete /></el-icon>
</el-button>
</div>
</div>
</div>
</template>
<span v-else class="text-muted">-</span>
</template>
</el-table-column>
<el-table-column label="操作" width="250" fixed="right">
<template #default="{ row }">
<div class="action-buttons">
<el-button type="primary" link @click="handleEditParameter(row)">编辑</el-button>
<el-button type="success" link @click="handleViewParamValues(row)">查看参数值</el-button>
<el-button type="danger" link @click="handleDeleteParameter(row)">删除</el-button>
</div>
</template>
</el-table-column>
<template #empty>
<el-empty description="暂无参数数据" :image-size="80" />
</template>
</el-table>
</div>
</div>
</div>
</el-dialog>
<!-- 商品参数表单对话框 -->
@@ -154,56 +243,6 @@
</template>
</el-dialog>
<!-- 参数值管理对话框 -->
<el-dialog v-model="paramValuesDialogVisible" title="参数值管理" width="800px" append-to-body>
<div class="values-header">
<div>
<span>参数{{ currentParam?.name }}</span>
<el-tag v-if="hasUnit(currentParam)" size="small" type="success" style="margin-left: 8px">
{{ getArgKey(currentParam) }} · 基础单位: {{ getBaseUnit(getArgKey(currentParam)) }}
</el-tag>
</div>
<el-button type="primary" @click="handleAddParamValue">
<el-icon><Plus /></el-icon>添加参数值
</el-button>
</div>
<el-table
v-loading="paramValuesLoading"
:data="paramValueList"
style="width: 100%; margin-top: 20px"
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
>
<el-table-column prop="id" label="值ID" width="80" />
<el-table-column prop="name" label="值名称" min-width="120" />
<el-table-column label="值/范围" min-width="180">
<template #default="{ row }">
<template v-if="currentParam?.type === 'select'">{{ row.value || '-' }}</template>
<template v-else-if="currentParam?.type === 'number'">
<el-tag size="small" type="info">
{{ getRangeTypeText(row.rangeType) }} {{ (row.phase != null && row.phase !== '') ? row.phase : 0 }}
</el-tag>
</template>
<template v-else>{{ row.value || '-' }}</template>
</template>
</el-table-column>
<el-table-column prop="index" label="排序" width="80" />
<el-table-column label="价格" width="100">
<template #default="{ row }">¥{{ (row.price / 100).toFixed(2) }}</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<div class="action-buttons">
<el-button type="primary" link @click="handleEditParamValue(row)">编辑</el-button>
<el-button type="danger" link @click="handleDeleteParamValue(row)">删除</el-button>
</div>
</template>
</el-table-column>
<template #empty>
<el-empty description="暂无参数值" :image-size="60" />
</template>
</el-table>
</el-dialog>
<!-- 参数值表单对话框 -->
<el-dialog
v-model="paramValueFormDialogVisible"
@@ -216,56 +255,67 @@
<div class="tk-section">
<div class="tk-section-title">参数值信息</div>
<el-form-item label="值名称" prop="attr_name">
<el-input v-model="paramValueForm.attr_name" placeholder="请输入值名称" />
<div class="name-input-row">
<el-input v-model="paramValueForm.attr_name" placeholder="请输入值名称" style="flex: 1" />
<el-button
v-if="currentParam?.type === 'number'"
type="primary"
plain
:disabled="!canGenerateValueName"
@click="generateValueName"
>
<el-icon><MagicStick /></el-icon>
<span style="margin-left: 4px">生成</span>
</el-button>
</div>
</el-form-item>
<el-form-item v-if="currentParam?.type === 'select'" label="参数值" prop="attr_value">
<el-input v-model="paramValueForm.attr_value" placeholder="请输入参数值" />
</el-form-item>
<el-form-item label="排序索引" prop="index">
<el-input-number v-model="paramValueForm.index" :min="0" placeholder="排序索引" style="width: 100%" />
<el-form-item v-if="currentParam?.type === 'number'" label="数值范围" prop="attr_range">
<div class="range-config-row">
<el-select v-model="paramValueForm.range_type" class="range-type-select">
<el-option label="" value="before">
<span class="range-opt-symbol"></span>
<span class="range-opt-desc">小于</span>
</el-option>
<el-option label="" value="after">
<span class="range-opt-symbol"></span>
<span class="range-opt-desc">大于</span>
</el-option>
<el-option label="" value="equal">
<span class="range-opt-symbol"></span>
<span class="range-opt-desc">等于</span>
</el-option>
</el-select>
<el-input-number
v-model="paramValueForm.attr_range_display"
:min="valueDisplayMin"
:max="valueDisplayMax"
:step="valueDisplayStep"
:step-strictly="true"
placeholder="数值"
controls-position="right"
class="range-value-input"
/>
<el-select
v-if="hasUnit(currentParam)"
v-model="paramValueForm.display_unit"
class="range-unit-select"
@change="onValueUnitChange"
>
<el-option v-for="u in currentParamUnits" :key="u" :label="u" :value="u" />
</el-select>
</div>
<div v-if="hasUnit(currentParam)" class="form-tip">
实际存储: {{ computedBaseValue }} {{ getBaseUnit(getArgKey(currentParam)) }}
范围: {{ currentParam?.min ?? 0 }} ~ {{ currentParam?.max ?? '-' }} {{ getBaseUnit(getArgKey(currentParam)) }}步长: {{ currentParam?.step || 1 }}
</div>
</el-form-item>
<el-form-item label="价格" prop="attr_price">
<el-input-number v-model="paramValueForm.attr_price" :min="0" placeholder="请输入价格" style="width: 100%" />
</el-form-item>
</div>
<template v-if="currentParam?.type === 'number'">
<div class="tk-section">
<div class="tk-section-title">数值范围配置</div>
<el-form-item label="范围类型" prop="range_type">
<el-select v-model="paramValueForm.range_type" placeholder="请选择范围类型" style="width: 100%">
<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: 数值 &lt; phase 时匹配 | after: 数值 &gt; phase 时匹配</div>
</el-form-item>
<el-form-item label="阈值" prop="attr_range">
<div class="unit-input-row">
<el-input-number
v-model="paramValueForm.attr_range_display"
:min="valueDisplayMin"
:max="valueDisplayMax"
:step="valueDisplayStep"
:step-strictly="true"
placeholder="范围阈值"
style="flex: 1"
/>
<el-select
v-if="hasUnit(currentParam)"
v-model="paramValueForm.display_unit"
style="width: 100px"
@change="onValueUnitChange"
>
<el-option v-for="u in currentParamUnits" :key="u" :label="u" :value="u" />
</el-select>
</div>
<div v-if="hasUnit(currentParam)" class="form-tip">
实际存储值: {{ computedBaseValue }} {{ getBaseUnit(getArgKey(currentParam)) }}
范围: {{ currentParam?.min ?? 0 }} ~ {{ currentParam?.max ?? '-' }} {{ getBaseUnit(getArgKey(currentParam)) }}步长: {{ currentParam?.step || 1 }}
</div>
</el-form-item>
</div>
</template>
</el-form>
<template #footer>
<div class="tk-dialog-footer">
@@ -279,7 +329,11 @@
<script setup>
import { ref, reactive, computed, watch, nextTick } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Refresh } from '@element-plus/icons-vue'
import {
Plus, Refresh, Rank, MagicStick,
Edit, Delete, Collection, List, Money,
ShoppingCart, User, Ticket
} from '@element-plus/icons-vue'
import {
getProductParameterList,
getProductParameterDetail,
@@ -293,7 +347,7 @@ import {
import {
getAvailableUnits, getArgKeyOptions, hasUnit, getArgKey,
getBaseUnit, getDefaultDisplayUnit, getParamDefaultUnit, getParamUnits,
toBaseUnit, fromBaseUnit, formatValueWithUnit
toBaseUnit, fromBaseUnit
} from '@/utils/dynamicUnit'
const props = defineProps({
@@ -382,9 +436,6 @@ const onEnableUnitChange = (val) => {
}
}
const paramValuesDialogVisible = ref(false)
const paramValuesLoading = ref(false)
const paramValueList = ref([])
const currentParam = ref(null)
const paramValueFormDialogVisible = ref(false)
@@ -436,19 +487,145 @@ const computedBaseValue = computed(() => {
return Math.round(toBaseUnit(paramValueForm.attr_range_display || 0, paramValueForm.display_unit, argKey))
})
const formatPhaseDisplay = (phaseValue) => {
if (phaseValue === undefined || phaseValue === null) return '-'
if (!hasUnit(currentParam.value)) return String(phaseValue)
const argKey = getArgKey(currentParam.value)
const displayUnit = getParamDefaultUnit(currentParam.value)
const displayVal = fromBaseUnit(phaseValue, displayUnit, argKey)
return formatValueWithUnit(displayVal, displayUnit)
}
const onValueUnitChange = () => {
// Recalculate: keep the base value, recalculate display value
}
const canGenerateValueName = computed(() => {
if (currentParam.value?.type !== 'number') return false
const v = paramValueForm.attr_range_display
return v !== null && v !== undefined && v !== ''
})
const generateValueName = () => {
if (!canGenerateValueName.value) return
const val = paramValueForm.attr_range_display
const unit = hasUnit(currentParam.value) ? (paramValueForm.display_unit || '') : ''
const numText = `${val}${unit}`
let name = ''
switch (paramValueForm.range_type) {
case 'before':
name = `${numText} 以下`
break
case 'after':
name = `${numText} 以上`
break
case 'equal':
default:
name = numText
break
}
paramValueForm.attr_name = name
}
const dragSrc = ref({ paramId: null, index: null })
const dragOver = ref({ paramId: null, index: null, position: null })
const isReordering = ref(false)
const isDraggingAttr = (paramId, idx) => {
return dragSrc.value.paramId === paramId && dragSrc.value.index === idx
}
const dragOverClass = (paramId, idx) => {
if (dragOver.value.paramId !== paramId || dragOver.value.index !== idx) return ''
return dragOver.value.position === 'top' ? 'drag-over-top' : 'drag-over-bottom'
}
const onAttrDragStart = (e, param, idx) => {
dragSrc.value = { paramId: param.id, index: idx }
if (e.dataTransfer) {
e.dataTransfer.effectAllowed = 'move'
try { e.dataTransfer.setData('text/plain', String(idx)) } catch (_) {}
}
}
const onAttrDragOver = (e, param, idx) => {
if (dragSrc.value.paramId !== param.id) return
if (dragSrc.value.index === idx) return
e.preventDefault()
if (e.dataTransfer) e.dataTransfer.dropEffect = 'move'
const rect = e.currentTarget.getBoundingClientRect()
const position = e.clientY - rect.top < rect.height / 2 ? 'top' : 'bottom'
if (
dragOver.value.paramId !== param.id ||
dragOver.value.index !== idx ||
dragOver.value.position !== position
) {
dragOver.value = { paramId: param.id, index: idx, position }
}
}
const onAttrDragLeave = (e, param, idx) => {
if (
dragOver.value.paramId === param.id &&
dragOver.value.index === idx &&
!e.currentTarget.contains(e.relatedTarget)
) {
dragOver.value = { paramId: null, index: null, position: null }
}
}
const onAttrDrop = async (e, param, idx) => {
e.preventDefault()
if (dragSrc.value.paramId !== param.id) return
const src = dragSrc.value.index
const position = dragOver.value.position
let dst = idx
if (position === 'bottom') dst = idx + 1
dragSrc.value = { paramId: null, index: null }
dragOver.value = { paramId: null, index: null, position: null }
if (src === null || src === dst || src + 1 === dst) return
await reorderAttrs(param, src, dst)
}
const onAttrDragEnd = () => {
dragSrc.value = { paramId: null, index: null }
dragOver.value = { paramId: null, index: null, position: null }
}
const reorderAttrs = async (param, src, dst) => {
if (isReordering.value) return
isReordering.value = true
const newAttrs = [...(param.attrs || [])]
const [moved] = newAttrs.splice(src, 1)
const insertAt = dst > src ? dst - 1 : dst
newAttrs.splice(insertAt, 0, moved)
param.attrs = newAttrs
const updates = []
newAttrs.forEach((item, i) => {
if (Number(item.index) !== i) {
updates.push({ item, newIndex: i })
}
})
if (!updates.length) {
isReordering.value = false
return
}
try {
await Promise.all(updates.map(({ item, newIndex }) => {
const payload = {
good_id: Number(props.goodId),
arg_id: Number(param.id),
attr_id: item.id,
attr_name: item.name,
attr_value: item.value || '',
attr_price: Number(item.price) || 0,
index: newIndex
}
if (param?.type === 'number') {
payload.attr_range = item.phase ?? 0
payload.range_type = item.rangeType || 'equal'
}
return updateProductParameterValue(payload)
}))
ElMessage.success('排序已更新')
} catch (err) {
ElMessage.error('排序更新失败')
} finally {
isReordering.value = false
refreshParamAttrs(param)
}
}
const getArgTypeText = (type) => {
const typeMap = { 'string': '字符串', 'number': '数字', 'select': '选择' }
return typeMap[type] || '未知'
@@ -468,7 +645,22 @@ const fetchParameterList = async () => {
try {
const res = await getProductParameterList({ good_id: props.goodId })
if (res.data.code === 200) {
parameterList.value = res.data.data || []
const list = res.data.data || []
await Promise.all(list.map(async (param) => {
try {
const detail = await getProductParameterDetail({ good_id: props.goodId, arg_id: param.id })
if (detail.data.code === 200) {
const attrs = detail.data.data.attrs || []
attrs.sort((a, b) => (Number(a.index) || 0) - (Number(b.index) || 0))
param.attrs = attrs
} else {
param.attrs = []
}
} catch (_) {
param.attrs = []
}
}))
parameterList.value = list
}
} catch (error) {
ElMessage.error('获取参数列表失败')
@@ -477,6 +669,18 @@ const fetchParameterList = async () => {
}
}
const refreshParamAttrs = async (param) => {
if (!param) return
try {
const res = await getProductParameterDetail({ good_id: props.goodId, arg_id: param.id })
if (res.data.code === 200) {
const list = res.data.data.attrs || []
list.sort((a, b) => (Number(a.index) || 0) - (Number(b.index) || 0))
param.attrs = list
}
} catch (_) {}
}
const handleAddParameter = () => {
paramFormType.value = 'add'
paramFormDialogVisible.value = true
@@ -566,41 +770,31 @@ const submitParamForm = () => {
})
}
const handleViewParamValues = (row) => {
currentParam.value = row
paramValuesDialogVisible.value = true
fetchParamValuesList()
}
const fetchParamValuesList = async () => {
if (!props.goodId || !currentParam.value) return
paramValuesLoading.value = true
try {
const res = await getProductParameterDetail({ good_id: props.goodId, arg_id: currentParam.value.id })
if (res.data.code === 200) paramValueList.value = res.data.data.attrs || []
} catch (error) { ElMessage.error('获取参数值列表失败') }
finally { paramValuesLoading.value = false }
}
const handleAddParamValue = () => {
const handleAddParamValue = (param) => {
currentParam.value = param
paramValueFormType.value = 'add'
paramValueFormDialogVisible.value = true
const defaultUnit = hasUnit(currentParam.value) ? getParamDefaultUnit(currentParam.value) : ''
const defaultUnit = hasUnit(param) ? getParamDefaultUnit(param) : ''
const attrs = param.attrs || []
const nextIndex = attrs.length
? Math.max(...attrs.map((it) => Number(it.index) || 0)) + 1
: 0
nextTick(() => {
paramValueFormRef.value?.resetFields()
Object.assign(paramValueForm, { attr_id: undefined, attr_name: '', attr_value: '', attr_price: 0, index: 0, attr_range: 0, attr_range_display: 0, display_unit: defaultUnit, range_type: 'equal' })
Object.assign(paramValueForm, { attr_id: undefined, attr_name: '', attr_value: '', attr_price: 0, index: nextIndex, attr_range: 0, attr_range_display: 0, display_unit: defaultUnit, range_type: 'equal' })
})
}
const handleEditParamValue = (row) => {
const handleEditParamValue = (row, param) => {
currentParam.value = param
paramValueFormType.value = 'edit'
paramValueFormDialogVisible.value = true
const baseValue = row.phase || 0
let displayValue = baseValue
let displayUnit = ''
if (hasUnit(currentParam.value)) {
const argKey = getArgKey(currentParam.value)
displayUnit = getParamDefaultUnit(currentParam.value)
if (hasUnit(param)) {
const argKey = getArgKey(param)
displayUnit = getParamDefaultUnit(param)
displayValue = fromBaseUnit(baseValue, displayUnit, argKey)
}
Object.assign(paramValueForm, {
@@ -611,13 +805,13 @@ const handleEditParamValue = (row) => {
})
}
const handleDeleteParamValue = (row) => {
const handleDeleteParamValue = (row, param) => {
ElMessageBox.confirm(`确认删除参数值 ${row.name} 吗?`, '警告', {
confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'
}).then(async () => {
try {
const res = await deleteProductParameterValue({ good_id: props.goodId, attr_id: row.id })
if (res.data.code === 200) { ElMessage.success('删除成功'); fetchParamValuesList() }
if (res.data.code === 200) { ElMessage.success('删除成功'); refreshParamAttrs(param) }
} catch (error) { ElMessage.error('删除失败') }
}).catch(() => {})
}
@@ -651,7 +845,11 @@ const submitParamValueForm = () => {
}
if (paramValueFormType.value === 'edit') submitData.attr_id = paramValueForm.attr_id
const res = paramValueFormType.value === 'add' ? await addProductParameterValue(submitData) : await updateProductParameterValue(submitData)
if (res.data.code === 200) { ElMessage.success(paramValueFormType.value === 'add' ? '添加成功' : '修改成功'); paramValueFormDialogVisible.value = false; fetchParamValuesList() }
if (res.data.code === 200) {
ElMessage.success(paramValueFormType.value === 'add' ? '添加成功' : '修改成功')
paramValueFormDialogVisible.value = false
refreshParamAttrs(currentParam.value)
}
} catch (error) { ElMessage.error(error.response?.data?.message || '操作失败') }
}
})
@@ -663,17 +861,309 @@ watch(() => props.visible, (val) => {
</script>
<style scoped>
.filter-section { padding: 0; background: transparent; }
/* 通用 */
.action-bar { display: flex; gap: 12px; flex-shrink: 0; flex-wrap: wrap; align-items: center; }
.action-buttons { display: flex; gap: 4px; align-items: center; flex-wrap: nowrap; }
.action-buttons .el-button { padding: 4px 8px; }
.text-muted { color: #c0c4cc; font-size: 12px; }
.number-config { color: #909399; font-size: 13px; }
.number-config div { line-height: 1.4; }
.unit-info { display: flex; align-items: center; gap: 8px; margin-top: 4px; }
.unit-text { font-size: 12px; color: #666; }
.values-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.dialog-footer { display: flex; justify-content: flex-end; gap: 12px; padding: 0; }
.form-tip { font-size: 12px; color: #909399; margin-top: 4px; }
.unit-input-row { display: flex; align-items: center; gap: 8px; width: 100%; }
/* 顶部工具栏 */
.param-manager-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 0 16px 0;
border-bottom: 1px solid #ebeef5;
margin-bottom: 16px;
}
.toolbar-info {
display: inline-flex;
align-items: center;
gap: 6px;
color: #606266;
font-size: 14px;
}
.toolbar-info .el-icon { color: #409eff; font-size: 16px; }
/* 参数卡片网格 */
.parameter-cards-wrap {
min-height: 200px;
max-height: 68vh;
overflow-y: auto;
padding-right: 4px;
}
.parameter-cards-grid {
display: grid;
grid-template-columns: 1fr;
gap: 16px;
}
.param-empty-state {
padding: 40px 0;
}
/* 参数卡片 */
.param-card {
border: 1px solid #ebeef5;
border-radius: 10px;
background: #fff;
padding: 16px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
transition: box-shadow 0.2s ease, border-color 0.2s ease;
}
.param-card:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
border-color: #dcdfe6;
}
.param-card-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
}
.param-card-title {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
min-width: 0;
}
.title-type-tag { flex-shrink: 0; }
.param-name {
font-size: 16px;
font-weight: 600;
color: #303133;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.title-must-tag { flex-shrink: 0; }
.param-id {
color: #909399;
font-size: 12px;
flex-shrink: 0;
}
.param-card-actions {
display: flex;
gap: 4px;
flex-shrink: 0;
}
/* 元信息标签行 */
.param-card-meta {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 12px;
}
.meta-tag {
display: inline-flex !important;
align-items: center;
gap: 4px;
}
.meta-tag .el-icon { font-size: 12px; }
/* number 类型配置信息 */
.param-card-number-config {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 18px;
padding: 10px 12px;
margin-bottom: 12px;
background: #f7f9fc;
border-radius: 6px;
border-left: 3px solid #409eff;
}
.number-config-item {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
}
.config-label {
color: #909399;
}
.config-value {
color: #303133;
font-weight: 600;
font-family: 'Monaco', 'Menlo', 'Courier New', monospace;
}
/* 参数值列表 */
.param-card-values {
border-top: 1px dashed #ebeef5;
padding-top: 12px;
}
.values-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.values-title {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: #606266;
font-weight: 600;
}
.values-title .el-icon { color: #409eff; }
.drag-tip-inline {
display: inline-flex;
align-items: center;
gap: 3px;
margin-left: 10px;
font-size: 12px;
color: #909399;
font-weight: normal;
}
.drag-tip-inline .el-icon { color: #909399; font-size: 12px; }
.values-empty {
padding: 14px 12px;
text-align: center;
color: #c0c4cc;
font-size: 13px;
background: #fafafa;
border-radius: 6px;
border: 1px dashed #e4e7ed;
}
.values-list {
display: flex;
flex-direction: column;
gap: 4px;
}
.value-row {
display: grid;
grid-template-columns: 24px 40px 1.4fr 1.4fr auto auto;
align-items: center;
gap: 10px;
padding: 8px 10px;
background: #fafafa;
border: 1px solid transparent;
border-radius: 6px;
cursor: grab;
transition: background-color 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
}
.value-row:hover {
background: #ecf5ff;
border-color: #d9ecff;
}
.value-row:active { cursor: grabbing; }
.value-row.is-dragging {
opacity: 0.45;
background: #ecf5ff;
}
.value-row.drag-over-top {
box-shadow: inset 0 2px 0 0 #409eff;
}
.value-row.drag-over-bottom {
box-shadow: inset 0 -2px 0 0 #409eff;
}
.value-drag-handle {
color: #c0c4cc;
font-size: 15px;
cursor: grab;
}
.value-row:hover .value-drag-handle { color: #409eff; }
.value-order {
font-size: 12px;
color: #909399;
font-family: 'Monaco', 'Menlo', monospace;
text-align: center;
}
.value-name {
color: #303133;
font-size: 13px;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.value-range { min-width: 0; }
.value-raw {
color: #606266;
font-size: 13px;
font-family: 'Monaco', 'Menlo', monospace;
}
.value-price {
display: inline-flex;
align-items: center;
gap: 3px;
color: #f56c6c;
font-weight: 600;
font-size: 13px;
padding: 2px 8px;
background: #fef0f0;
border-radius: 4px;
white-space: nowrap;
}
.value-price .el-icon { font-size: 12px; }
.value-actions {
display: flex;
gap: 2px;
}
.value-actions .el-button { padding: 4px 6px; }
/* 值名称 + 生成按钮 */
.name-input-row { display: flex; align-items: center; gap: 8px; width: 100%; }
/* 数值范围单行配置 */
.range-config-row {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
}
.range-config-row .range-type-select { width: 96px; flex: 0 0 96px; }
.range-config-row .range-value-input { flex: 1; min-width: 0; }
.range-config-row .range-unit-select { width: 100px; flex: 0 0 100px; }
.range-config-row :deep(.el-select .el-input__inner) {
font-size: 16px;
font-weight: 600;
color: #409eff;
text-align: center;
}
.range-opt-symbol {
display: inline-block;
font-weight: 600;
font-size: 16px;
color: #409eff;
width: 24px;
}
.range-opt-desc {
margin-left: 12px;
color: #8492a6;
font-size: 12px;
}
/* 对话框内滚动区域自定义滚动条 */
.parameter-cards-wrap::-webkit-scrollbar { width: 6px; }
.parameter-cards-wrap::-webkit-scrollbar-track { background: transparent; }
.parameter-cards-wrap::-webkit-scrollbar-thumb {
background: #dcdfe6;
border-radius: 3px;
}
.parameter-cards-wrap::-webkit-scrollbar-thumb:hover { background: #c0c4cc; }
/* 响应式:窄屏时参数值行改为纵向 */
@media (max-width: 768px) {
.value-row {
grid-template-columns: 24px 1fr auto;
grid-template-rows: auto auto;
}
.value-order { display: none; }
.value-range { grid-column: 2 / 3; }
.value-price { grid-column: 3 / 4; grid-row: 1 / 2; }
.value-actions { grid-column: 3 / 4; grid-row: 2 / 3; }
}
</style>