提交 080fbef8 authored 作者: 王鹏飞's avatar 王鹏飞

feat: 增加商品规格管理功能,支持动态添加、删除规格及其值,并自动生成SKU列表

上级 d96644db
<script setup>
import { Plus, QuestionFilled } from '@element-plus/icons-vue'
import { Plus, Delete, QuestionFilled } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { deliveryMode, deliveryTime, orderStockCount } from '@/utils/dictionary'
const form = inject('form')
// 初始化规格列表
if (!form.info.specs) {
form.info.specs = []
}
// 初始化sku列表
if (!form.info.sku) {
form.info.sku = [{ price: '', stock: 0 }]
}
// 确保在没有规格时至少有一个SKU
onMounted(() => {
ensureSkuExists()
})
function ensureSkuExists() {
const hasValidSpecs = form.info.specs && form.info.specs.some((spec) => spec.name && spec.name.trim())
if (!hasValidSpecs && (!form.info.sku || form.info.sku.length === 0)) {
form.info.sku = [{ price: '', stock: 0 }]
}
}
// 添加规格
function addSpec() {
form.info.specs.push({
name: '',
values: [''],
})
}
// 删除规格
function removeSpec(index) {
form.info.specs.splice(index, 1)
generateSkuList()
}
// 添加规格值
function addSpecValue(specIndex) {
form.info.specs[specIndex].values.push('')
}
// 删除规格值
function removeSpecValue(specIndex, valueIndex) {
if (form.info.specs[specIndex].values.length > 1) {
form.info.specs[specIndex].values.splice(valueIndex, 1)
generateSkuList()
}
}
// 笛卡尔积算法:生成所有规格值的组合
function cartesianProduct(arrays) {
if (arrays.length === 0) return []
if (arrays.length === 1) return arrays[0].map((item) => [item])
const [first, ...rest] = arrays
const restProduct = cartesianProduct(rest)
const result = []
for (const item of first) {
for (const restItem of restProduct) {
result.push([item, ...restItem])
}
}
return result
}
// 生成SKU列表
function generateSkuList() {
// 过滤掉名称为空的规格和值为空的规格值
const validSpecs = form.info.specs.filter((spec) => spec.name && spec.name.trim())
// 如果没有有效规格,确保至少有一个SKU
if (validSpecs.length === 0) {
if (!form.info.sku || form.info.sku.length === 0) {
form.info.sku = [{ price: '', stock: 0 }]
} else if (form.info.sku.length === 1 && !form.info.sku[0].specs) {
// 保持单个SKU,但不添加specs字段
form.info.sku[0] = {
price: form.info.sku[0].price || '',
stock: form.info.sku[0].stock || 0,
}
} else {
// 如果之前有多个SKU(有规格时),现在没有规格了,只保留第一个或创建一个新的
const firstSku = form.info.sku[0]
form.info.sku = [
{
price: firstSku?.price || '',
stock: firstSku?.stock || 0,
},
]
}
return
}
// 获取所有规格的有效值数组
const specValuesArrays = validSpecs.map((spec) =>
spec.values.filter((val) => val && val.trim()).map((val) => val.trim())
)
// 如果任何规格没有有效值,确保至少有一个SKU
if (specValuesArrays.some((arr) => arr.length === 0)) {
if (!form.info.sku || form.info.sku.length === 0) {
form.info.sku = [{ price: '', stock: 0 }]
} else if (form.info.sku.length === 1 && !form.info.sku[0].specs) {
form.info.sku[0] = {
price: form.info.sku[0].price || '',
stock: form.info.sku[0].stock || 0,
}
}
return
}
// 生成笛卡尔积
const combinations = cartesianProduct(specValuesArrays)
// 生成SKU列表,保留已存在的价格和库存数据
const existingSkuMap = new Map()
form.info.sku.forEach((sku) => {
// 如果有specs字段,使用specs作为key;否则使用空数组作为key
const key = JSON.stringify(sku.specs || [])
existingSkuMap.set(key, { price: sku.price || '', stock: sku.stock || 0 })
})
form.info.sku = combinations.map((combo) => {
const key = JSON.stringify(combo)
const existing = existingSkuMap.get(key)
return {
specs: combo,
price: existing?.price || '',
stock: existing?.stock || 0,
}
})
}
// 计算已添加的规格数量
const specCount = computed(() => form.info.specs.length)
const maxSpecCount = 3 // 最多3个规格
// 判断是否有有效的规格
const hasSpecs = computed(() => {
return (
form.info.specs &&
form.info.specs.some((spec) => {
if (!spec.name || !spec.name.trim()) return false
return spec.values && spec.values.some((val) => val && val.trim())
})
)
})
// 监听规格变化,当没有规格时确保SKU存在
watch(hasSpecs, (newVal) => {
if (!newVal) {
ensureSkuExists()
}
})
// 批量设置相关
const batchPrice = ref('')
const batchStock = ref('')
// 批量设置价格
function batchSetPrice() {
if (batchPrice.value === '' || batchPrice.value === null || batchPrice.value === undefined) return
const price = batchPrice.value
form.info.sku.forEach((sku) => {
sku.price = price
})
batchPrice.value = ''
}
// 批量设置库存
function batchSetStock() {
if (batchStock.value === '' || batchStock.value === null || batchStock.value === undefined) return
const stock = Number(batchStock.value)
if (isNaN(stock) || stock < 0) {
ElMessage({ message: '请输入有效的库存数量', type: 'warning' })
return
}
form.info.sku.forEach((sku) => {
sku.stock = stock
})
batchStock.value = ''
}
</script>
<template>
......@@ -16,7 +202,57 @@ const form = inject('form')
</el-form-item>
<el-form-item label="商品规格">
<div class="form-tips">准确填写规格信息,有助于商品在搜索场景获取更多流量</div>
<el-button size="large" :icon="Plus">添加规格(1/3)</el-button>
<div class="spec-list">
<div v-for="(spec, specIndex) in form.info.specs" :key="specIndex" class="spec-item">
<div class="spec-item-header">
<el-input
v-model="spec.name"
placeholder="请输入规格名称(如:颜色、尺寸)"
style="flex: 1"
@blur="generateSkuList">
</el-input>
<el-button
type="danger"
plain
:icon="Delete"
circle
size="small"
@click="removeSpec(specIndex)"
style="margin-left: 12px"
title="删除规格">
</el-button>
</div>
<div class="spec-values">
<div v-for="(value, valueIndex) in spec.values" :key="valueIndex" class="spec-value-item">
<el-input v-model="spec.values[valueIndex]" placeholder="请输入规格值" @blur="generateSkuList">
</el-input>
<el-button
type="danger"
plain
:icon="Delete"
circle
size="small"
@click="removeSpecValue(specIndex, valueIndex)"
:disabled="spec.values.length <= 1"
style="margin-left: 8px"
title="删除规格值">
</el-button>
</div>
<el-button
type="primary"
plain
:icon="Plus"
size="small"
@click="addSpecValue(specIndex)"
style="margin-top: 8px">
添加规格值
</el-button>
</div>
</div>
<el-button type="primary" plain :icon="Plus" @click="addSpec" :disabled="specCount >= maxSpecCount">
添加规格({{ specCount }}/{{ maxSpecCount }}
</el-button>
</div>
</el-form-item>
<el-form-item label="发货时效" prop="info.delivery_time">
<div class="form-tips">
......@@ -27,15 +263,40 @@ const form = inject('form')
</el-radio-group>
</el-form-item>
<el-form-item label="价格与库存" required>
<el-table :data="form.info.sku" border :header-cell-style="{ backgroundColor: '#f5f7fa' }">
<div class="form-tips" v-if="hasSpecs">根据规格自动生成,请为每个SKU填写价格和库存</div>
<div class="form-tips" v-else>请填写商品价格和库存</div>
<div class="batch-set-container" v-if="form.info.sku && form.info.sku.length > 0">
<div class="batch-set-item">
<el-input v-model="batchPrice" placeholder="批量设置价格" type="number" style="width: 200px">
<template #prefix>¥</template>
</el-input>
<el-button type="primary" plain @click="batchSetPrice" style="margin-left: 8px"> 批量设置价格 </el-button>
</div>
<div class="batch-set-item">
<el-input v-model="batchStock" placeholder="批量设置库存" type="number" style="width: 200px">
<template #suffix></template>
</el-input>
<el-button type="primary" plain @click="batchSetStock" style="margin-left: 8px"> 批量设置库存 </el-button>
</div>
</div>
<el-table :data="form.info.sku" border :header-cell-style="{ backgroundColor: '#f5f7fa' }" style="width: 100%">
<el-table-column v-if="hasSpecs" label="规格组合" align="center" width="200">
<template #default="{ row }">
<span>{{ row.specs ? row.specs.join(' / ') : '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="price" label="价格" align="center">
<template #default="{ row }">
<el-input v-model="row.price" placeholder="请输入"></el-input>
<el-input v-model="row.price" placeholder="请输入价格" type="number">
<template #prefix>¥</template>
</el-input>
</template>
</el-table-column>
<el-table-column prop="stock" label="库存" align="center">
<template #default="{ row }">
<el-input v-model="row.stock" placeholder="请输入"></el-input>
<el-input v-model.number="row.stock" placeholder="请输入库存" type="number">
<template #suffix></template>
</el-input>
</template>
</el-table-column>
</el-table>
......@@ -59,3 +320,77 @@ const form = inject('form')
</el-form-item>
</div>
</template>
<style scoped>
.spec-list {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.spec-item {
flex: 1;
min-width: 300px;
padding: 16px;
border: 1px solid var(--el-border-color-lighter);
border-radius: 8px;
background-color: #fafafa;
}
.spec-item-header {
display: flex;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid var(--el-border-color-lighter);
}
.spec-values {
display: flex;
flex-direction: column;
gap: 8px;
}
.spec-value-item {
display: flex;
align-items: center;
}
.batch-set-container {
display: flex;
gap: 16px;
margin-bottom: 16px;
padding: 16px;
background-color: #f8f9fa;
border-radius: 8px;
border: 1px solid var(--el-border-color-lighter);
}
.batch-set-item {
display: flex;
align-items: center;
}
@media (max-width: 768px) {
.spec-list {
flex-direction: column;
}
.spec-item {
min-width: 100%;
}
.batch-set-container {
flex-direction: column;
gap: 12px;
}
.batch-set-item {
width: 100%;
}
.batch-set-item .el-input {
flex: 1;
}
}
</style>
......@@ -34,7 +34,8 @@ const form = reactive({
order_stock_count: '1',
shipping_template: '', // 运费模板
after_sales_policy: '', // 售后政策
sku: [{ price: '', stock: 0 }],
specs: [], // 规格列表
sku: [], // SKU列表(通过规格笛卡尔积生成)
},
})
provide('form', form)
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论