4180f73c53
- 订单列表重构为卡片式布局并新增筛选功能 - 设置管理支持struct/struct_list类型配置 - 新增短信签名和模板独立管理页面 - 通知渠道新增短信渠道配置 - 产品参数管理优化 Co-authored-by: Cursor <cursoragent@cursor.com>
1170 lines
41 KiB
Vue
1170 lines
41 KiB
Vue
<template>
|
||
<!-- 商品参数管理主对话框 -->
|
||
<el-dialog
|
||
:model-value="visible"
|
||
title="商品参数管理"
|
||
width="1000px"
|
||
top="6vh"
|
||
class="param-manager-dialog"
|
||
@update:model-value="$emit('update:visible', $event)"
|
||
>
|
||
<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>新增参数
|
||
</el-button>
|
||
<el-button type="success" @click="fetchParameterList">
|
||
<el-icon><Refresh /></el-icon>刷新
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<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>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</el-dialog>
|
||
|
||
<!-- 商品参数表单对话框 -->
|
||
<el-dialog
|
||
v-model="paramFormDialogVisible"
|
||
:title="paramFormType === 'add' ? '新增商品参数' : '编辑商品参数'"
|
||
width="600px"
|
||
append-to-body
|
||
class="tk-dialog"
|
||
>
|
||
<el-form ref="paramFormRef" :model="paramForm" :rules="paramRules" label-width="100px">
|
||
<div class="tk-section">
|
||
<div class="tk-section-title">基本信息</div>
|
||
<el-form-item label="参数名称" prop="arg_name">
|
||
<el-input v-model="paramForm.arg_name" placeholder="请输入参数名称" />
|
||
</el-form-item>
|
||
<el-form-item label="参数类型" prop="arg_type">
|
||
<el-radio-group v-model="paramForm.arg_type">
|
||
<el-radio label="string">字符串</el-radio>
|
||
<el-radio label="number">数字</el-radio>
|
||
<el-radio label="select">选择</el-radio>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
<el-form-item label="是否必选" prop="must">
|
||
<el-switch v-model="paramForm.must" :active-value="true" :inactive-value="false" active-text="必选" inactive-text="可选" />
|
||
</el-form-item>
|
||
</div>
|
||
<div class="tk-section">
|
||
<div class="tk-section-title">权限控制</div>
|
||
<el-form-item label="允许单独购买">
|
||
<el-switch v-model="paramForm.user_add" active-text="允许" inactive-text="不允许" />
|
||
<div style="font-size: 12px; color: #909399; margin-top: 4px">购买后是否允许单独追加购买</div>
|
||
</el-form-item>
|
||
<el-form-item label="用户组优惠">
|
||
<el-switch v-model="paramForm.use_user_group_discount" active-text="允许" inactive-text="不允许" />
|
||
<div style="font-size: 12px; color: #909399; margin-top: 4px">是否允许使用用户组优惠</div>
|
||
</el-form-item>
|
||
<el-form-item label="用户优惠">
|
||
<el-switch v-model="paramForm.use_user_discount" active-text="允许" inactive-text="不允许" />
|
||
<div style="font-size: 12px; color: #909399; margin-top: 4px">是否允许使用用户优惠(代金券与优惠码)</div>
|
||
</el-form-item>
|
||
</div>
|
||
<template v-if="paramForm.arg_type === 'number'">
|
||
<div class="tk-section">
|
||
<div class="tk-section-title">数值参数配置</div>
|
||
<el-form-item label="步进值" prop="arg_step">
|
||
<div class="unit-input-row">
|
||
<el-input-number v-model="paramForm.step_display" :min="1" placeholder="步进值" style="flex: 1" />
|
||
<el-select v-if="paramForm.enable_unit && paramForm.arg_key" :model-value="paramForm.step_unit" style="width: 100px" @change="(v) => onFieldUnitChange('step', v)">
|
||
<el-option v-for="u in formUnits" :key="u" :label="u" :value="u" />
|
||
</el-select>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="最小值" prop="arg_min">
|
||
<div class="unit-input-row">
|
||
<el-input-number v-model="paramForm.min_display" placeholder="最小值" style="flex: 1" />
|
||
<el-select v-if="paramForm.enable_unit && paramForm.arg_key" :model-value="paramForm.min_unit" style="width: 100px" @change="(v) => onFieldUnitChange('min', v)">
|
||
<el-option v-for="u in formUnits" :key="u" :label="u" :value="u" />
|
||
</el-select>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="最大值" prop="arg_max">
|
||
<div class="unit-input-row">
|
||
<el-input-number v-model="paramForm.max_display" placeholder="最大值" style="flex: 1" />
|
||
<el-select v-if="paramForm.enable_unit && paramForm.arg_key" :model-value="paramForm.max_unit" style="width: 100px" @change="(v) => onFieldUnitChange('max', v)">
|
||
<el-option v-for="u in formUnits" :key="u" :label="u" :value="u" />
|
||
</el-select>
|
||
</div>
|
||
<div v-if="paramForm.enable_unit && paramForm.arg_key" class="form-tip">
|
||
实际提交({{ getBaseUnit(paramForm.arg_key) }}): 步进={{ calcBase(paramForm.step_display, paramForm.step_unit) }} | 最小={{ calcBase(paramForm.min_display, paramForm.min_unit) }} | 最大={{ calcBase(paramForm.max_display, paramForm.max_unit) }}
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="启用动态单位">
|
||
<el-switch v-model="paramForm.enable_unit" active-text="启用" inactive-text="禁用" @change="onEnableUnitChange" />
|
||
<div style="font-size: 12px; color: #909399; margin-top: 4px">启用后可在输入框右侧切换单位,提交时自动转为基础单位</div>
|
||
</el-form-item>
|
||
<el-form-item v-if="paramForm.enable_unit" label="参数键" prop="arg_key">
|
||
<el-select v-model="paramForm.arg_key" placeholder="选择参数键" style="width: 100%" @change="onArgKeyChange">
|
||
<el-option v-for="opt in argKeyOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||
</el-select>
|
||
<div style="font-size: 12px; color: #909399; margin-top: 4px">用于识别参数类型,基础单位:
|
||
<strong v-if="paramForm.arg_key">{{ getBaseUnit(paramForm.arg_key) }}</strong>
|
||
<span v-else>请先选择</span>
|
||
</div>
|
||
</el-form-item>
|
||
</div>
|
||
</template>
|
||
</el-form>
|
||
<template #footer>
|
||
<div class="tk-dialog-footer">
|
||
<el-button @click="paramFormDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" @click="submitParamForm">确定</el-button>
|
||
</div>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 参数值表单对话框 -->
|
||
<el-dialog
|
||
v-model="paramValueFormDialogVisible"
|
||
:title="paramValueFormType === 'add' ? '添加参数值' : '编辑参数值'"
|
||
width="550px"
|
||
append-to-body
|
||
class="tk-dialog"
|
||
>
|
||
<el-form ref="paramValueFormRef" :model="paramValueForm" :rules="paramValueRules" label-width="100px">
|
||
<div class="tk-section">
|
||
<div class="tk-section-title">参数值信息</div>
|
||
<el-form-item label="值名称" prop="attr_name">
|
||
<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 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>
|
||
</el-form>
|
||
<template #footer>
|
||
<div class="tk-dialog-footer">
|
||
<el-button @click="paramValueFormDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" @click="submitParamValueForm">确定</el-button>
|
||
</div>
|
||
</template>
|
||
</el-dialog>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, computed, watch, nextTick } from 'vue'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import {
|
||
Plus, Refresh, Rank, MagicStick,
|
||
Edit, Delete, Collection, List, Money,
|
||
ShoppingCart, User, Ticket
|
||
} from '@element-plus/icons-vue'
|
||
import {
|
||
getProductParameterList,
|
||
getProductParameterDetail,
|
||
createProductParameter,
|
||
updateProductParameter,
|
||
deleteProductParameter,
|
||
addProductParameterValue,
|
||
updateProductParameterValue,
|
||
deleteProductParameterValue
|
||
} from '@/api/admin/product'
|
||
import {
|
||
getAvailableUnits, getArgKeyOptions, hasUnit, getArgKey,
|
||
getBaseUnit, getDefaultDisplayUnit, getParamDefaultUnit, getParamUnits,
|
||
toBaseUnit, fromBaseUnit
|
||
} from '@/utils/dynamicUnit'
|
||
|
||
const props = defineProps({
|
||
visible: { type: Boolean, default: false },
|
||
goodId: { type: Number, default: null }
|
||
})
|
||
const emit = defineEmits(['update:visible'])
|
||
|
||
const argKeyOptions = getArgKeyOptions()
|
||
|
||
const paramLoading = ref(false)
|
||
const parameterList = ref([])
|
||
|
||
const paramFormDialogVisible = ref(false)
|
||
const paramFormType = ref('add')
|
||
const paramFormRef = ref(null)
|
||
const paramForm = reactive({
|
||
arg_id: undefined,
|
||
arg_name: '',
|
||
arg_type: 'string',
|
||
must: false,
|
||
arg_step: 1,
|
||
arg_min: 0,
|
||
arg_max: 100,
|
||
step_display: 1,
|
||
min_display: 0,
|
||
max_display: 100,
|
||
step_unit: '',
|
||
min_unit: '',
|
||
max_unit: '',
|
||
user_add: false,
|
||
use_user_group_discount: false,
|
||
use_user_discount: false,
|
||
enable_unit: false,
|
||
arg_key: ''
|
||
})
|
||
const paramRules = {
|
||
arg_name: [{ required: true, message: '请输入参数名称', trigger: 'blur' }],
|
||
arg_type: [{ required: true, message: '请选择参数类型', trigger: 'change' }]
|
||
}
|
||
|
||
const formUnits = computed(() => {
|
||
if (!paramForm.arg_key) return []
|
||
return getAvailableUnits(paramForm.arg_key)
|
||
})
|
||
|
||
const calcBase = (displayVal, unit) => {
|
||
if (!paramForm.enable_unit || !paramForm.arg_key || !unit) return displayVal
|
||
return Math.round(toBaseUnit(displayVal || 0, unit, paramForm.arg_key))
|
||
}
|
||
|
||
const onFieldUnitChange = (field, newUnit) => {
|
||
const key = paramForm.arg_key
|
||
const oldUnit = paramForm[`${field}_unit`]
|
||
const oldDisplay = paramForm[`${field}_display`] || 0
|
||
const baseVal = oldUnit ? toBaseUnit(oldDisplay, oldUnit, key) : oldDisplay
|
||
paramForm[`${field}_unit`] = newUnit
|
||
paramForm[`${field}_display`] = fromBaseUnit(baseVal, newUnit, key)
|
||
}
|
||
|
||
const onArgKeyChange = () => {
|
||
const key = paramForm.arg_key
|
||
const defaultUnit = key ? getDefaultDisplayUnit(key) : ''
|
||
paramForm.step_unit = defaultUnit
|
||
paramForm.min_unit = defaultUnit
|
||
paramForm.max_unit = defaultUnit
|
||
if (key && defaultUnit) {
|
||
paramForm.step_display = fromBaseUnit(paramForm.arg_step, defaultUnit, key)
|
||
paramForm.min_display = fromBaseUnit(paramForm.arg_min, defaultUnit, key)
|
||
paramForm.max_display = fromBaseUnit(paramForm.arg_max, defaultUnit, key)
|
||
} else {
|
||
paramForm.step_display = paramForm.arg_step
|
||
paramForm.min_display = paramForm.arg_min
|
||
paramForm.max_display = paramForm.arg_max
|
||
}
|
||
}
|
||
|
||
const onEnableUnitChange = (val) => {
|
||
if (!val) {
|
||
paramForm.step_display = paramForm.arg_step
|
||
paramForm.min_display = paramForm.arg_min
|
||
paramForm.max_display = paramForm.arg_max
|
||
paramForm.step_unit = ''
|
||
paramForm.min_unit = ''
|
||
paramForm.max_unit = ''
|
||
}
|
||
}
|
||
|
||
const currentParam = ref(null)
|
||
|
||
const paramValueFormDialogVisible = ref(false)
|
||
const paramValueFormType = ref('add')
|
||
const paramValueFormRef = ref(null)
|
||
const paramValueForm = reactive({
|
||
attr_id: undefined,
|
||
attr_name: '',
|
||
attr_value: '',
|
||
attr_price: 0,
|
||
index: 0,
|
||
attr_range: 0,
|
||
attr_range_display: 0,
|
||
display_unit: '',
|
||
range_type: 'equal'
|
||
})
|
||
const paramValueRules = {
|
||
attr_name: [{ required: true, message: '请输入值名称', trigger: 'blur' }]
|
||
}
|
||
|
||
const currentParamUnits = computed(() => {
|
||
if (!hasUnit(currentParam.value)) return []
|
||
return getParamUnits(currentParam.value)
|
||
})
|
||
|
||
const valueDisplayMin = computed(() => {
|
||
if (!hasUnit(currentParam.value)) return 0
|
||
const argKey = getArgKey(currentParam.value)
|
||
const baseMin = currentParam.value?.min ?? 0
|
||
return fromBaseUnit(baseMin, paramValueForm.display_unit, argKey)
|
||
})
|
||
const valueDisplayMax = computed(() => {
|
||
if (!hasUnit(currentParam.value)) return 9999999
|
||
const argKey = getArgKey(currentParam.value)
|
||
const baseMax = currentParam.value?.max
|
||
if (baseMax === undefined || baseMax === null) return 9999999
|
||
return fromBaseUnit(baseMax, paramValueForm.display_unit, argKey)
|
||
})
|
||
const valueDisplayStep = computed(() => {
|
||
if (!hasUnit(currentParam.value)) return 1
|
||
const argKey = getArgKey(currentParam.value)
|
||
const baseStep = currentParam.value?.step || 1
|
||
return fromBaseUnit(baseStep, paramValueForm.display_unit, argKey)
|
||
})
|
||
|
||
const computedBaseValue = computed(() => {
|
||
if (!hasUnit(currentParam.value)) return paramValueForm.attr_range_display
|
||
const argKey = getArgKey(currentParam.value)
|
||
return Math.round(toBaseUnit(paramValueForm.attr_range_display || 0, paramValueForm.display_unit, argKey))
|
||
})
|
||
|
||
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) / 100 || 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] || '未知'
|
||
}
|
||
const getArgTypeTag = (type) => {
|
||
const tagMap = { 'string': 'primary', 'number': 'success', 'select': 'warning' }
|
||
return tagMap[type] || 'info'
|
||
}
|
||
const getRangeTypeText = (type) => {
|
||
const typeMap = { 'after': '≥', 'before': '≤', 'equal': '=' }
|
||
return typeMap[type] || type || '-'
|
||
}
|
||
|
||
const fetchParameterList = async () => {
|
||
if (!props.goodId) return
|
||
paramLoading.value = true
|
||
try {
|
||
const res = await getProductParameterList({ good_id: props.goodId })
|
||
if (res.data.code === 200) {
|
||
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('获取参数列表失败')
|
||
} finally {
|
||
paramLoading.value = false
|
||
}
|
||
}
|
||
|
||
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
|
||
Object.assign(paramForm, {
|
||
arg_id: undefined, arg_name: '', arg_type: 'string', must: false,
|
||
arg_step: 1, arg_min: 0, arg_max: 100,
|
||
step_display: 1, min_display: 0, max_display: 100,
|
||
step_unit: '', min_unit: '', max_unit: '',
|
||
user_add: false, use_user_group_discount: false, use_user_discount: false,
|
||
enable_unit: false, arg_key: ''
|
||
})
|
||
nextTick(() => { paramFormRef.value?.resetFields() })
|
||
}
|
||
|
||
const handleEditParameter = (row) => {
|
||
paramFormType.value = 'edit'
|
||
paramFormDialogVisible.value = true
|
||
const enableUnit = row.enableUnit || false
|
||
const argKey = row.argKey || ''
|
||
const baseStep = row.step || 1
|
||
const baseMin = row.min || 0
|
||
const baseMax = row.max || 100
|
||
let defaultUnit = ''
|
||
let stepDisplay = baseStep
|
||
let minDisplay = baseMin
|
||
let maxDisplay = baseMax
|
||
if (enableUnit && argKey) {
|
||
defaultUnit = getDefaultDisplayUnit(argKey)
|
||
stepDisplay = fromBaseUnit(baseStep, defaultUnit, argKey)
|
||
minDisplay = fromBaseUnit(baseMin, defaultUnit, argKey)
|
||
maxDisplay = fromBaseUnit(baseMax, defaultUnit, argKey)
|
||
}
|
||
Object.assign(paramForm, {
|
||
arg_id: row.id, arg_name: row.name, arg_type: row.type, must: row.must || false,
|
||
arg_step: baseStep, arg_min: baseMin, arg_max: baseMax,
|
||
step_display: stepDisplay, min_display: minDisplay, max_display: maxDisplay,
|
||
step_unit: defaultUnit, min_unit: defaultUnit, max_unit: defaultUnit,
|
||
user_add: row.userAdd ?? row.user_add ?? false,
|
||
use_user_group_discount: row.useUserGroupDiscount ?? row.use_user_group_discount ?? false,
|
||
use_user_discount: row.useUserDiscount ?? row.use_user_discount ?? false,
|
||
enable_unit: enableUnit, arg_key: argKey
|
||
})
|
||
}
|
||
|
||
const handleDeleteParameter = (row) => {
|
||
ElMessageBox.confirm(`确认删除参数 ${row.name} 吗?`, '警告', {
|
||
confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'
|
||
}).then(async () => {
|
||
try {
|
||
const res = await deleteProductParameter({ good_id: props.goodId, arg_id: row.id })
|
||
if (res.data.code === 200) { ElMessage.success('删除成功'); fetchParameterList() }
|
||
} catch (error) { ElMessage.error('删除失败') }
|
||
}).catch(() => {})
|
||
}
|
||
|
||
const submitParamForm = () => {
|
||
paramFormRef.value?.validate(async (valid) => {
|
||
if (valid) {
|
||
try {
|
||
const submitData = {
|
||
good_id: Number(props.goodId),
|
||
arg_name: paramForm.arg_name,
|
||
arg_type: paramForm.arg_type,
|
||
must: paramForm.must === true,
|
||
user_add: paramForm.user_add === true,
|
||
use_user_group_discount: paramForm.use_user_group_discount === true,
|
||
use_user_discount: paramForm.use_user_discount === true
|
||
}
|
||
if (paramForm.arg_type === 'number') {
|
||
if (paramForm.enable_unit && paramForm.arg_key) {
|
||
submitData.arg_step = calcBase(paramForm.step_display, paramForm.step_unit)
|
||
submitData.arg_min = calcBase(paramForm.min_display, paramForm.min_unit)
|
||
submitData.arg_max = calcBase(paramForm.max_display, paramForm.max_unit)
|
||
submitData.enable_unit = true
|
||
submitData.arg_key = paramForm.arg_key
|
||
} else {
|
||
submitData.arg_step = Number(paramForm.step_display)
|
||
submitData.arg_min = Number(paramForm.min_display)
|
||
submitData.arg_max = Number(paramForm.max_display)
|
||
}
|
||
}
|
||
if (paramFormType.value === 'edit') submitData.arg_id = paramForm.arg_id
|
||
const res = paramFormType.value === 'add' ? await createProductParameter(submitData) : await updateProductParameter(submitData)
|
||
if (res.data.code === 200) { ElMessage.success(paramFormType.value === 'add' ? '新增成功' : '修改成功'); paramFormDialogVisible.value = false; fetchParameterList() }
|
||
} catch (error) { ElMessage.error(error.response?.data?.message || '操作失败') }
|
||
}
|
||
})
|
||
}
|
||
|
||
const handleAddParamValue = (param) => {
|
||
currentParam.value = param
|
||
paramValueFormType.value = 'add'
|
||
paramValueFormDialogVisible.value = true
|
||
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: nextIndex, attr_range: 0, attr_range_display: 0, display_unit: defaultUnit, range_type: 'equal' })
|
||
})
|
||
}
|
||
|
||
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(param)) {
|
||
const argKey = getArgKey(param)
|
||
displayUnit = getParamDefaultUnit(param)
|
||
displayValue = fromBaseUnit(baseValue, displayUnit, argKey)
|
||
}
|
||
Object.assign(paramValueForm, {
|
||
attr_id: row.id, attr_name: row.name, attr_value: row.value || '',
|
||
attr_price: row.price / 100 || 0, index: row.index || 0,
|
||
attr_range: baseValue, attr_range_display: displayValue,
|
||
display_unit: displayUnit, range_type: row.rangeType || 'equal'
|
||
})
|
||
}
|
||
|
||
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('删除成功'); refreshParamAttrs(param) }
|
||
} catch (error) { ElMessage.error('删除失败') }
|
||
}).catch(() => {})
|
||
}
|
||
|
||
const submitParamValueForm = () => {
|
||
paramValueFormRef.value?.validate(async (valid) => {
|
||
if (valid) {
|
||
try {
|
||
const submitData = { good_id: Number(props.goodId), arg_id: Number(currentParam.value.id), attr_name: paramValueForm.attr_name, index: Number(paramValueForm.index), attr_price: Number(paramValueForm.attr_price) }
|
||
if (currentParam.value.type === 'select') submitData.attr_value = paramValueForm.attr_value
|
||
if (currentParam.value.type === 'number') {
|
||
let rangeValue
|
||
if (hasUnit(currentParam.value)) {
|
||
rangeValue = computedBaseValue.value
|
||
} else {
|
||
rangeValue = Number(paramValueForm.attr_range_display)
|
||
}
|
||
const baseMin = currentParam.value.min ?? 0
|
||
const baseMax = currentParam.value.max
|
||
const baseStep = currentParam.value.step || 1
|
||
if (rangeValue < baseMin || (baseMax !== undefined && baseMax !== null && rangeValue > baseMax)) {
|
||
ElMessage.warning(`阈值超出范围 (${baseMin} ~ ${baseMax ?? '∞'} ${hasUnit(currentParam.value) ? getBaseUnit(getArgKey(currentParam.value)) : ''})`)
|
||
return
|
||
}
|
||
if (baseStep > 0 && (rangeValue - baseMin) % baseStep !== 0) {
|
||
ElMessage.warning(`阈值必须符合步长 ${baseStep} 的要求`)
|
||
return
|
||
}
|
||
submitData.attr_range = rangeValue
|
||
submitData.range_type = paramValueForm.range_type
|
||
}
|
||
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
|
||
refreshParamAttrs(currentParam.value)
|
||
}
|
||
} catch (error) { ElMessage.error(error.response?.data?.message || '操作失败') }
|
||
}
|
||
})
|
||
}
|
||
|
||
watch(() => props.visible, (val) => {
|
||
if (val && props.goodId) fetchParameterList()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* 通用 */
|
||
.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; }
|
||
.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>
|